[JPA] Entity Manager & Persistence Context

Casper·2023년 11월 30일
0

Entity Manager Factory & Entity Manager

Entity Manager는 JPA 에서 매핑한 엔티티를 CRUD 하는 모든 작업을 처리합니다.
Entity Manager Factory는 이러한 Entity Manager를 생성하는 곳이며 일반적으로 연결된 DB당 하나씩 생성합니다.
Factory를 생성하는 비용은 상당히 크기 때문에 생성 후 어플리케이션 전체에서 공유되도록 설계되어 있으며, 반면에 Entity Manager의 생성 비용은 거의 들지 않습니다.
또한 Entity Manager Factory는 Thread-safe하게 설계되어있지만 Entity Manager는 동시 접근 시 문제가 발생할 수 있으므로 공유가 되어서는 안됩니다.
Hibernate를 포함한 JPA 구현체들은 Entity Manager Factory를 생성할 때 Connection Pool을 함께 생성하며, Entity Manager는 생성된 후에 보통 트랜잭션을 시작할 때 해당 커넥션을 획득합니다.


Persistence Context (영속성 컨텍스트)

JPA를 이해하는 데 가장 중요한 용어이며 실질적으로 보이지 않아 헷갈릴 수 있지만 '엔티티를 영구 저장하는 논리적인 영역'을 의미합니다.
Entity Manager로 엔티티를 저장, 조회하면 해당 영속성 컨텍스트에 저장합니다.
영속성 컨텍스트 또한 엔티티 매니저가 생성될 때마다 생성이 되며 엔티티 매니저를 통해 영속성 컨텍스트에 접근합니다.
경우에 따라 여러 엔티티 매니저가 동일한 영속성 컨텍스트에 접근 또한 가능합니다.


엔티티의 생명주기

비영속 (New / Transient)

영속성 컨텍스트나 DB와 전혀 관계가 없는 순수한 객체 상태.

Member member = new Member();

영속 (Managed)

엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장한 상태.
이제 영속성 컨텍스트에 의해 관리가 되는 상태이며 영속성 컨텍스트의 이점을 적용받게 됩니다.
해당 이점은 아래에 작성하겠습니다.

em.persist(member)

준영속 (Detached)

특정 엔티티를 영속성 컨텍스트에서 제거하거나 (em.detach)
영속성 컨텍스트를 닫거나 (em.close)
영속성 컨텍스트를 초기화 하는 경우 (em.clear)
엔티티가 영속성 컨텍스트에서 분리되어 더 이상 관리되지 않는 상태입니다.
비영속 상태에 가깝지만 영속 상태였기 때문에 식별자의 존재는 보장이 됩니다.
나중에 정리할 지연 로딩 또한 지원이 되지 않습니다.

em.detach(member)
em.close()
em.clear()

삭제 (Removed)

엔티티를 영속성 컨텍스트와 DB에서 삭제한 상태.

em.remove(member)

영속성 컨텍스트 이점

1차 캐시


영속성 컨텍스트는 내부에 캐시를 갖고 있으며 해당 캐시를 1차 캐시라 부르며 영속 상태의 엔티티는 모두 이곳에 저장됩니다.
1차 캐시의 키는 식별자 값이며 테이블의 PK와 매핑되어 있습니다.
em.find()를 통해 엔티티를 조회하면 우선적으로 메모리에 1차 캐시를 조회하며 캐시에 존재하지 않으면 그 때 DB를 통해 조회합니다.
하지만 해당 캐시는 보통 사용자 요청마다 생성이 되는 영속성 컨텍스트 내에 있기 때문에 엄청 복잡한 비즈니스 로직이 아니라면 큰 성능상의 효과는 없는 편입니다.

동일성 보장

영속성 컨텍스트는 자바 컬렉션 내의 객체와 같이 식별자가 같다면 반복해서 조회하더라도 동일한 엔티티를 반환합니다.

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b);

트랜잭션을 지원하는 쓰기 지연

em.persist(memberA);
em.persist(memberB);
// 여기 까지는 '쓰기 지연 저장소'에만 저장이 되며 실제 INSERT SQL을 보내지 않음
transaction.commit();
// 트랜잭션 커밋과 동시에 저장소에 모아놓은 쿼리를 한번에 날림

변경 감지

기존 SQL 수정 쿼리의 문제점은 테이블의 컬럼이 늘어날 수록 각각에 맞는 수정 쿼리가 많아지는 것은 물론이고 비즈니스 로직을 분석하기 위해 SQL을 계속 확인해야 하기 때문에 직간접적으로 비즈니스 로직이 SQL에 의존하게 된다는 점이 문제점입니다.
이를 보완하기 위해 JPA는 엔티티의 처음 저장할 때 최초 상태를 담은 스냅샷을 함께 저장해둡니다.
그리고 영속성 컨텍스트의 내용을 DB에 반영하는 'Flush'라는 이벤트가 발생하면 1차 캐시에 저장된 스냅샷과 엔티티가 다르다면 수정 쿼리를 작성하여 '쓰기 지연 SQL 저장소'에 저장을 하고 이를 포함하여 저장된 쿼리를 모두 날리게 됩니다.
JPA의 기본 전략은 엔티티의 모든 필드를 업데이트 하는 것이며 이는 하나의 쿼리를 미리 생성하고 재사용할 수 있는 장점이 있습니다.
하지만 데이터 전송량이 부담되는 상황이라면 특정 컬럼만 업데이트할 수 있도록 변경도 가능합니다. (Dynamic Update)

지연 로딩

성능 최적화에 중요한 개념이므로 별도로 정리하겠습니다.


Flush

위에서 설명드린 것처럼 영속성 컨텍스트의 변경 내역을 DB에 반영하는 이벤트이며 해당 이벤트가 발생하면 모든 영속 상태의 엔티티를 스냅샷과 비교하여 수정 쿼리를 쓰기 지연 저장소에 등록하고 저장된 쿼리를 DB에 전달합니다.
플러시하는 방법은 아래와 같습니다.

직접 호출

em.flush()를 통해 강제로 플러시하는 방법이며 테스트할 때 제외하고는 거의 사용하지 않습니다.

트랜잭션 커밋

트랜잭션이 커밋되기 전에 플러시를 통해 DB에 쿼리가 전달이 되어야 데이터 변경이 이루어지기 때문에 JPA는 트랜잭션 커밋 전에 자동으로 플러시를 수행합니다.

JPQL 실행

JPQL에서 조회를 할 경우 플러시를 하지 않고 영속 상태일 경우는 JPQL에서 해당 엔티티를 조회하지 못하기 때문에 이를 보완하기 위하여 JPA는 JPQL실행 전 자동으로 플러시를 진행합니다.

profile
Emotional Developer

0개의 댓글