Post

Springboot Jpa - 영속성 관리 - 내부 동작 방식

JPA의 영속성이 무엇인지 그리고 어떻게 동작되는지 알아보자.

Springboot Jpa - 영속성 관리 - 내부 동작 방식

영속성 컨텍스트

JPA에서 가장 중요한 2가지

  • 객체와 관계형 데이터베이스 매핑하기 database의 설계와 객체의 설계를 어떻게 해서 JPA로 어떻게 매핑해서 사용할지에 대한 내용(정적)
  • 영속성 컨텍스트 실제 JPA가 내부에서 어떻게 동작하는지를 알 수 있는 핵심적인 내용

Entity Manager Factory, Entity Manager

Build source

Entity Manager Factory
Entity Manager Factory를 통해서 Entity Manager를 생성할 수 있다.
Entity Manager
내부적으로 database connection을 이용해 database를 이용해 접근하게 된다.

영속성 컨텍스트(PersistenceContext)

영속성 컨텍스트(PersistenceContext)
JPA를 이해하는데 가장 중요한 단어로, “엔티티를 영구 저장하는 환경”이라는 의미다.
EntityManager.persist(entity)는 DB에 저장한다는 의미가 아닌, entity의 영속성 컨텍스트라는 곳에 저장한다는 의미다.
논리적인 개념으로 눈에 보이지 않는다.
Entity Manager를 통해서 영속성 컨텍스트에 접근할 수 있다.
Entity Manager 생성시 내부에 영속성 컨텍스트 라는 논리적인 공간이 Entity Manager와 1:1로 매칭된다.

Entity의 생명주기

Build source

  1. 비영속(new / transient) 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태다. Entity가 새로 생성되었을 때, 비영속 상태다.
  2. 영속(managed) 영속성 컨텍스트에 관리되는 상태 persist()호출시, 영속상태가 된다.
  3. 준영속(detached) 영속성 컨텍스트에 저장되었다가 분리된 상태
  4. 삭제(removed) 삭제된 상태

비영속(new / transient)

1
2
3
User user = new User();
user.setId("user1");
user.setName("이름");

객체를 생성하고 값만 초기화 하는, 아무것도 하지 않은 상태를 의미한다.

영속(managed)

1
2
3
4
5
6
7
8
9
User user = new User();
user.setId("user1");
user.setName("이름");

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

// 객체를 저장한 상태(영속)
em.persist(user);

persist()를 호출하면, entityManager내에 영속성 컨텍스트 라는 곳에 user가 들어가면서, 영속 상태가 된다. persist()를 호출시 바로 database에 접근해 데이터의 변경 값을 저장하지 않는다.

준영속(detached)

1
em.detach(user);

detach()를 호출하면, User엔티티를 entityManager 내의 영속성 컨텍스트에서 분리한 것으로, 준영속 상태가 된다.

삭제(removed)

1
em.remove(user);

remove()를 호출하면, User엔티티를 Database에서 지우겠다는 의미다.

영속성 컨텍스트의 이점

영속성 컨텍스트가 필요한 이유

1차 캐시

영속성 컨택스트는 내부에 1차 캐시라는 내용을 들고 있는다. 단, 이 1차 캐시는 EntityManager 단위로 있기 때문에 application 단위로 공유되지 않는다. JPA에서 application 단위의 공유되는 캐시는 2차 캐시라고 한다. 1차 캐시는 트랜잭션 격리 수준을 application 차원에서 제공해, JPA를 이용해 코드 작성시 코드를 객체 지향적으로 코드를 작성할 수 있게 해주는데에 더 큰 의미가 있다.

1차 캐시의 구조
1
2
3
4
5
6
User user = new User();
user.setId("user1");
user.setName("이름");

// 객체를 저장한 상태(영속)
em.persist(user);

Build source 코드에 대한 구조


영속 컨텍스트가 entityManager는 아니지만 일단 지금 단계에서는 이렇게 이해해도 된다.

@id
db에서 PK로 지정한 값이 key로 지정된다.
Entity
객체 자체가 값이된다.
캐시에서 조회
  • find()를 실행할 때, 바로 DB에 접근해 값을 찾는게 아니라 영속 컨텍스트의 1차 캐시에서 먼저 조회함
1
2
3
4
5
6
User user = new User();
user.setId("user1");
user.setName("이름");

em.persist(user);
User findUser = em.find(User.class, "user1");

Build source 코드에 대한 구조


  • 만약, 1차 캐시에 없다면 DB에 접근해 값을 가져오고, 1차 캐시에 저장한뒤 반환한다.
1
2
3
4
5
6
7
8
9
User user = new User();
user.setId("user1");
user.setName("이름");

em.persist(user);
User findUser = em.find(User.class, "user1");

// 1차 캐시에 없는 값 조회
User findUser = em.find(User.class, "user2");

Build source 코드에 대한 구조


동일성(identity) 보장

1
2
3
4
User user1 = em.find(User.class, "user1");
User user2 = em.find(User.class, "user2");

System.out.println(user1 == user2);// true

마치 자바의 collection에서 값을 꺼내 비교하는 것과 같이, 하나의 트랜잭션 내에서 JPA를 통해서 조회한 값이 ID가 같다면 ==연산자를 통해 비교시 true를 반환한다.

1차 캐시로 반복 가능한 읽기(REPEATABLE READ)등급의 트랜잭션 격리 수준을 DB가 아닌 application 차원에서 제공한다.
반복 가능한 읽기
트랜잭션이 시작되고 끝날 때까지 같은 데이터를 여러 번 조회해도 값이 변하지 않는 것을 보장하는 격리 수준을 의미한다.
1차 캐시는 DB에서 처음 조회한 값을 가지고 있으며, 트랜잭션이 끝나기 전까지는 1차 캐시에서 값을 먼저 조회하며, 같은 데이터를 조회하면 같은 값이 나와야 한다.
1
2
3
4
5
User user1 = em.find(User.class, 1L);
System.out.println(user1.getName());

User user2 = em.find(User.class, 1L);
System.out.println(user2.getName());
  • 1 DB에서 가져와 1차 캐시에 저장됨
  • 2 A 출력
  • 4 다시 조회 → 같은 객체 반환 (1차 캐시 사용)
  • 5 A 출력, 처음 조회한 값 유지

관련된 내용은 이후에 더 자세히 나오니, 먼저 반복 가능한 읽기(REPEATABLE READ)가 어떤 의미인지만 이해하자.

트랜잭션을 지원하는 쓰기 지연(transactional-behind)

1
2
3
4
5
6
7
8
9
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();

transaction.begin();

em.persist(user1);
em.persist(user2);

transacstion.commit();
  • 4트랜잭션 시작을 의미
    entityManager는 데이터 변경시 트랜잭션을 시작해야 한다.
  • 6~7이 부분에서 insert query를 DB에 보내지 않는다.
  • 9커밋하는 순간 DB에 insert query를 보낸다.

persist()호출 시 코드의 6~7라인의 동작

commit()호출 시 코드의 9라인의 동작


영속성 컨텍스트 내에는 1차 캐시 외에도, 쓰기 지연 query 저장소 라는 곳이 있다.

persist() 호출시,

  1. 객체가 1차 캐시에 들어간다.
  2. 객체를 분석해 insert query를 생성한다.
  3. 생성된 insert query를 쓰기 지연 query 저장소에 쌓아둔다.
  4. commit()이 호출될 때까지 1~3과정까지만 진행된다.
  5. 이후 commit()이 호출된다면, 쌓아둔 query를 DB에 보낸다.
    이 때, 한번에 보내는 값을 조절하는 설정은 persistence.xml<property name="hibernate.jdbc.batch_size" value="10"/>을 추가하면 된다.

변경 감지(Dirty Checking)

JPA는 마치 DB를 collection에서 객체를 꺼내 값을 변경한다고 변경 이후 collection에 다시 넣지 않아도 되는 것처럼, JPA에서 조회한 값을 변경한 뒤 따로 저장하지 않아도 알아서 변경 값을 체크해 update query를 생성해 보낸다.

  • 엔티티 수정
1
2
3
4
5
6
7
8
9
10
11
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();

transaction.begin();

User user1 = em.find(User.class, "user1");

user1.setName("공수정");
user1.setAge(1);

transacstion.commit();
  • 4트랜잭션 시작
  • 6영속 엔티티 조회
  • 8~9영속 엔티티 데이터 수정
  • 11commit() 호출시, JPA가 데이터를 비교해 변경점이 있다면, 알아서 update query를 작성하고 보낸다.

persist()호출 시 코드의 11라인의 동작

commit() 호출시,

  1. 내부적으로 flush()를 호출한다.
  2. 엔티티와 스냅샷을 비교한다.
  3. 변경점이 있는 객체에 대해서만 update query를 만들어서 쓰기 지연 query 저장소에 넣는다.
  4. 쓰기 지연 query 저장소에 있는 query를 모두 DB에 반영한다.
  5. 반영한 뒤 커밋한다.
스냅샷
엔티티의 변경을 감지하기 위해 유지되는 원본 상태의 복사본이다.
find() 또는 persist() 를 호출하면 JPA가 내부적으로 생성한다.
스냅샷은 Map<String, Object>형태로 대략 snapshot.put(1L, Map.of("id", 1L, "name", "A", "age", 20))와 같이 필드와 값만 저장된다.

스냅샷과 객체를 비교하는 것과 객체끼리의 비교는 얼마나 다를까?

  • 객체끼리 비교: 원본객체의 전체 값(필드,값 외에도 프록시, JVM, Hibernate 내부 관리 정보 등이 있음)을 복사해야 해서 메모리가 증가하고, 변경되지 않은 필드까지 비교해 성능이 저하된다.
  • 스냅샷과 비교: 필드 값만 저장하므로 메모리 사용량을 최소화할 수 있고, 필드의 값을 순차적으로 비교하다가 변경된 값이 있으면 변경 전후의 값을 한번에 파악해 update 대상으로 설정한다.
  • 엔티티 삭제

위의 엔티티 수정과 같은 방식으로 동작하는데, update query가 아닌 delete query가 나가게 된다.

1
2
3
4
5
User user1 = em.find(User.class, "user1");

em.remove(user1);

transacstion.commit();
  • 8~9영속 엔티티 데이터 삭제
  • 11엔티티 수정과 같은 방식으로 commit() 호출시, delete query를 작성하고 보낸다.

지연 로딩(Lazy Loading)

프록시와 관련된 내용으로, 이후에 자세히 학습해보자.

플러시(Flush)

영속성 컨텍스트의 변경내용(=쌓아두었던 insert, update, delete query)을 DB에 반영하는 것을 의미한다. 플러시를 한다고해서 영속성 컨텍스트를 비우지 않는다. 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화 하는 것이다. 플러시라는 매커니즘이 동작할 수 있는 이유는 트랜잭션이라는 개념이 있기때문에 가능한 것이다.

플러시 발생

  1. 변경 감지한다.
  2. 수정된 엔티티를 쓰기 지연 query 저장소에 등록한다.
  3. 쓰기 지연 query 저장소의 쿼리를 DB에 전송한다.

영속성 컨텍스트를 플러시하는 방법

  • em.flush(): 직접 호출, 잘 사용하지는 않지만 알고 있으면 테스트시 등에 사용할 수 있다.
  • 트랜잭션 commit(): 플러시 자동 호출
  • JPQL 쿼리 실행: 플러시 자동 호출
    만약 자동으로 실행되지 않는다면 entityManager를 통해 데이터 수정을 한뒤 JPQL을 사용하는 경우에, entityManager를 통해 데이터 수정한 부분은 아직 DB에 반영되지 않아 의도와는 다른 JPQL 결과를 볼 수 있다. 그런 경우를 방지하고자, JPQL 사용시에는 플러시가 자동적으로 호출된다.

플러시 모드 옵션

플러시가 동작하는 모드를 변경할 수 있는데, 직접 사용할 일은 없으나 이해하고 있으면 좋다.

em.setFlushMode(FlushModeType.COMMIT)

  • FlushModeType.AUTO
    커밋이나 쿼리를 실행할 때 플러시(기본값)
  • FlushModeType.COMMIT
    커밋할 때만 플러시, 쿼리 실행시에는 플러시를 하지 않는다.

준영속 상태

영속 상태의 엔티티가 영속성 컨텍스트에서 분리되는 것을 의미한다. 분리가 되면, 영속성 컨텍스트가 제공하는 기능(1차캐시, 동일성, 쓰기 지연, 변경감지 등)을 사용 못한다.

준영속 상태로 만드는 방법

  • em.detach(entity): 특정 엔티티만 준영속 상태로 전환
    1
    2
    3
    4
    
      User user1= em.find(User.class, "user1");
      user1.setName("AAA");
      em.detach(user1);
      transaction.commit();
    

    detach 예제 위와 같은 경우에도, update query가 반영되지 않는다.

  • em.clear(): 영속성 컨텍스트를 완전히 초기화
    1
    2
    3
    4
    
      User user1= em.find(User.class, "user1");
      user1.setName("AAA");
      em.clear();
      transaction.commit();
    

    em.clear() 예제 위와 같은 경우에도, update query가 반영되지 않는다.

  • em.close(): 영속성 컨텍스트를 종료

정리

JPA에서 가장 중요한 2가지

  • 객체와 관계형 DB 매핑하기: 정적인 과정
  • 영속성 컨텍스트: 실제 동작하는 매커니증
영속성 컨텍스트
엔티티를 영구 저장하는 환경으로 논리적인 개념(=눈에 보이지 않는)이다.

J2SE 환경 Entity Manager 와 영속성 컨텍스트가 1:1

J2EE, springframework 같은 컨테이너 환경 Entity Manager 와 영속성 컨텍스트가 N:1


영속성 컨텍스트의 상태

  1. 비영속
  2. 영속: persist()로 영속 컨텍스트에 넣거나, find()로 DB에서 가져온 상태
  3. 준영속: 영속성 컨텍스트에서 분리한 상태
  4. 삭제: 객체를 삭제한 상태
영속성 컨텍스트의 이점
1차 캐시 - 동일 트랜잭션 내에서 조회했던 내용은, 1차 캐시로 가지고 있으며 사용한다.
동일성 보장 - 1차 캐시에서 가져와 사용하기 때문에, 동일성이 보장되어 ==연산자로 비교시 true반환
트랜잭션을 지원하는 쓰기 지연 - 버퍼와 비슷한 개념으로, commit() 호출전까지 query를 모아, 호출시 한번에 반영함
변경 감지 - 변경이 된 내용이 있으면 해당 내용만 반영쿼리를 작성해, commit() 호출시 반영
지연 로딩 - 굉장히 중요한 내용이라서 이후에 따로 작성할 예전
플러시
2가지 옵션이 존재하고, 직접할 수 도 있다.
영속성 컨텍스트를 비우지 않는다.
영속성 컨텍스트의 변경내용을 DB에 동기화 하는 작업이다.
트랜잭션이라는 작업 단위가 중요 - 영속성 컨텍스트와 트랜잭션의 주기를 같게 설계해야 문제가 없다.
This post is licensed under CC BY 4.0 by the author.