쌍문동 척척박사
Spring / ReactJS / PS

영속성 컨텍스트

영속성 컨텍스트

JPA에서 가장 중요한 요소를 2가지 뽑자면 객체-RDB 매핑 (ORM)영속성 컨텍스트 (Persistence Context)라 볼 수 있다. ORM이 이번 포스트에서는 영속성 컨텍스트에 대한 내용을 다뤄보겠다.


영속성 컨텍스트란?

JPA는 DB로부터 가져온 데이터를 객체 형태로 관리하는데, 여기서 엔티티 (Entity)는 DB 테이블과 매핑되는 클래스를 말한다.

1
2
3
4
5
6
7
@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

영속성 컨텍스트는 엔티티를 영구 저장하는 환경이라는 뜻으로, 실제 구현체가 아닌 논리적인 개념이다. 쉽게 말해 엔티티를 좀 더 효율적이고 안정적으로 다루기 위한 환경으로, 엔티티 매니저를 통해 영속성 컨텍스트에 접근하게 된다.

2.png

영속성 컨텍스트를 통해 JPA의 구동 방식을 확인할 수 있다.

  • 어플리케이션 로딩 시점에 JPA 설정 정보 (persistence.xml)를 조회해 EntityManagerFactory 한 개를 생성한다.
  • EntityManagerFactory는 데이터 변경 request가 발생하면 트랜잭션 단위마다 EntityManager를 생성한다.
  • 트랜잭션 안에서 데이터 변경이 이루어지고, 변경이 완료되면 EntityManager를 삭제한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();

// JPA에서 데이터 변경은 모두 Transaction 안에서 수행되야 함
tx.begin();
try {
    Item item = Item.builder()
            .id(10L)
            .name("book")
            .price(10000)
            .build();
    em.persist(item);
    item.changePrice(5000);

    tx.commit();
} catch (Exception e){
    tx.rollback();
} finally {
    em.close();
}
emf.close();

영속성 컨텍스트의 장점

1. 1차 캐시

3.png

영속성 컨텍스트는 내부에 1차 캐시를 갖고 있어 데이터 조회 시 1차 캐시부터 조회한 다. 만약 요청 데이터가 1차 캐시에 없다면 JPA는 DB에 접근해 데이터를 가져온 뒤 캐시에도 추가한다. 하지만 요청 비즈니스가 끝나면 EntityManager가 삭제되면서 캐시에 있는 데이터도 날아가기 때문에 성능에 큰 영향은 없다.

2. 영속 Entity의 동일성 보장 (Identity)

영속성 컨텍스트는 1차 캐시를 통해 DB가 아닌 어플리케이션 차원에서도 반복 가능한 읽기 (Repeatable Read) 등급의 트랜잭션 격리 수준을 제공한다.

1
2
3
Member memberA = em.find(Member.class, 53L);
Member memberB = em.find(Member.class, 53L);
System.out.println(memberA == memberB); // true

3. 트랜잭션을 지원하는 쓰기 지연 (Transactional Write-Behind)

JPA는 트랜잭션 내에서 여러 개의 쿼리문이 시행될 때, 쿼리들을 쓰기 지연 SQL 저장소에 저장했다가 한꺼번에 실행한다. (Flush)

4.png

1
2
3
em.persist(memberA); // SQL 실행 X
em.persist(memberB); // SQL 실행 X
transaction.commit(); // SQL 한꺼번에 실행

4. 변경 감지 (Dirty Checking)

Flush 시행 시 JPA는 1차 캐시 내 엔티티의 스냅샷 (최초 시점)과 엔티티의 현재 상태를 비교해 변경사항을 감지한다. 만약 변경 사항이 감지되면 자동으로 쓰기 지연 SQL 저장소에 업데이트 쿼리문이 생성되어 다른 쿼리와 함께 한꺼번에 실행된다.

5.png

따라서 데이터 변경은 데이터 삽입, 삭제와 다르게 EntityManager에서 수정 쿼리를 생성하지 않아도 자동으로 변경사항이 반영된다.

1
2
3
Member member = em.find(Member.class, 150L);
member.changeName("Junyoung");
// em.update(item); -> X

관련 포스트 : Dirty Checking과 Merge

5. 지연 로드 (Lazy Loading)

Flush

위에서 설명했듯이 flush는 영속성 컨텍스트의 변경사항을 DB에 한꺼번에 반영하는 것을 말한다. 기본적으로 flush 시 쿼리들 간에는 순서가 보장되기에 commit을 하면 flush가 호출되어 차례대로 쿼리가 실행된다. 하지만 아직 commit이 되지 않았는데도 flush가 호출되는 경우가 있다. 바로 JPQL을 사용할 때다.

관련 포스트 : 객체지향 쿼리 언어 JPQL

1
2
3
4
5
em.persist(memberA);
em.persist(memberB);
// flush 실행!
query = em.createQuery("select m from Member m", Member.class);
List<Member> members= query.getResultList();

기본적인 데이터 삽입/수정/삭제 쿼리의 경우 문제가 없다. 하지만 데이터 조회의 경우 데이터 변경 사항이 먼저 적용되지 않으면, JPQL을 통해 복잡한 JOIN문을 처리할 때 데이터 불일치가 발생할 수 있다. 따라서 EntityManager는 JPQL을 만나면 그 전에 쓰기 지연 SQL 저장소에 쌓인 쿼리문을 모두 실행한 뒤 JPQL을 실행한다.

단, DB로부터 엔티티를 가져올 때 이미 1차 캐시에 동일한 식별자의 엔티티가 있으면 가져온 엔티티를 버리고 1차 캐시에 있는 엔티티를 선택한다. 따라서 엔티티 동일성이 유지되게 된다. 참고로 flush 시 쓰기 지연 저장소는 비워지지만, 1차 캐시에는 변동이 없다.

엔티티의 생명 주기

6.png

엔티티의 생명 주기는 4가지로 나뉜다.

  • 비영속 (new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
  • 영속 (managed) : 영속성 컨텍스트에 관리되는 상태
  • 준영속 (detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제 (removed) : 삭제됨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 비영속
Member member = Member.builder()
        .id(13L)
        .name("Junyoung")
        .build();

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

// 영속. 영속성 컨텍스트에서 관리함
em.persist(member);
// 준영속. 영속성 컨텍스트에서 분리됨
em.detach(member);
// 삭제됨
em.remove(member);

준영속 상태는 더 이상 JPA에서 관리되지 않는 상태로, 엔티티를 준영속 상태로 만드는 방법은 3가지가 있다.

  • em.detach(entity) : 특정 엔티티만 준영속 상태로 전환
  • em.clear() : 영속성 컨텍스트를 완전히 초기화
  • em.close() : 영속성 컨텍스트를 종료
1
2
3
Member member = em.find(Member.class, 150L); // find 쿼리 실행됨
member.changeName("Junyoung");
em.detach(member); // update 쿼리 실행 안됨

참고 자료

자바 ORM 표준 JPA 프로그래밍

comments powered by Disqus