JPA의 엔티티매니저가 1차캐시, 쓰기지연, 영속성 관리 등의 역할을 하기 위해 어떻게 동작하는지 살펴보자
엔티티 매니저 팩토리는 엔티티 매니저를 생성하는 클래스입니다. 일반적으로 하나의 데이터베이스를 사용하는 애플리케이션은, 하나의 엔티티 매니저 팩토리를 생성하고 이를 통해 엔티티 매니저를 생성합니다.
엔티티 매니저 팩토리는 생성시 매우 큰 비용을 지불해야 하므로, 애플리케이션당 하나를 사용하면 되고, Thread-safe하므로 다른 스레드와 공유해도 괜찮습니다.
반면 엔티티매니저의 경우, 생성 비용이 거의 들지 않지만 여러 스레드가 동시에 접근할 경우, 동시성 문제가 발생하므로, 스레드간 공유하면 안됩니다.
또한, 엔티티 매니저는 데이터베이스 연결이 필요한 시점까지 커넥션이 없는 상태로 유지하다가, 트랜잭션이 시작되는 등, 데이터베이스 연결이 필요한 시점이 되면 커넥션을 획득합니다.
엔티티는 영속성 컨텍스트라는 엔티티 저장소에 영속되어 관리됩니다. 엔티티는 영속 상태에 따라 4가지의 상태가 있고, 생명주기는 아래와 같습니다.
엔티티를 영구 저장하는 환경으로, 엔티티 매니저를 통해 엔티티를 저장하거나 조회했을 때, 엔티티 매니저는 엔티니를 영속성 컨텍스트에 보관 및 관리합니다. 객체와 관계형 데이터베이스 사이의 패러다임 불일치 문제나 성능 개선을 위해 아래와 같은 특징을 갖습니다.
영속성 컨텍스트 내부에는 Map 자료구조가 존재합니다. @Id로 매핑한 값을 Key로 사용하고, 엔티티 인스턴스를 Value로 합니다.
만약 멤버 인스턴스가 생성되어 영속성 컨텍스트에 영속된 상태라면, 해당 엔티티에 대해 식별자로 조회했을 때 동일성이 보장되는 멤버 인스턴스를 데이터베이스 조회 없이 조회해주는 캐싱 기능을 제공합니다. 이를 1차캐시라고 합니다.
String memberId = "member1";
Member member = new Member();
member.setId(memberId);
em.persist(member);
Member emMember = em.find(Member.class, memberId);
//동일성 보장, Insataces has same identity 출력
if(emMember == member) System.out.println("Insataces has same identity");
1차캐시에 엔티티가 저장되어있는경우의 workflow입니다.
위의 경우와 달리, 멤버 엔티티가 1차캐시에 등록되어있지 않다면, SQL문으로 데이터를 조회하고, 엔티티를 생성한 뒤 1차캐시에 저장해 이를 반환한다.
//데이터베이스에 존재하지만, 영속상태가 아닌 엔티티 조회
Member emMember = em.find(Member.class, "member2");
따라서 영속성 컨텍스트는 1차캐시 기능을 통해 총 두 가지의 이점을 얻습니다.
엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 전달하지 않고, 쓰기지연 저장소에 쌓아놓고, 트랜잭션이 커밋되는 순간 데이터베이스에 쿼리를 전달합니다. 이것을 트랜잭션을 지원하는 쓰기지연이라고 합니다.
(1) Commit 전까지 영속성 컨텍스트에 쿼리 저장, 1차캐시에 엔티티 저장
(2) 트랜잭션 커밋 시, DB에 flush.
SQL을 사용한 엔티티 수정 작업은 많은 양의 쿼리 반복과 SQL의존성을 갖게됩니다.
JPA는 역시 트랜잭션을 지원하는 쓰기지연을 이용해 객체지향적인 방법으로 해결합니다.
이 때, 스냅샷이라는 새로운 개념이 등장하는데요, 스냅샷은 엔티티가 영속성 컨텍스트에 처음 저장될 때의 값을 기억해놓은 것입니다. 영속성 컨텍스트는 이를 기반으로, 플러시가 호출되었을 때 엔티티와 스냅샷을 비교해 Update 쿼리를 생성하고, 데이터베이스에 전송, 커밋합니다.
JPA 변경감지의 특징 : JPA가 생성하는 Update Query는 기본적으로 엔티티의 모든 필드를 업데이트합니다.
왜 이렇게 하는걸까요? 책에서는 재사용 측면에서 두 가지 정도의 장점을 설명합니다.
수정된 필드만을 이용해 쿼리를 전송하고 싶다면, @DynamicUpdate어노테이션을 사용하면 됩니다.
member.setName("Mklee");
em.flush();
# Name 필드만 변경할것으로 예상
UPDATE MEMBER
SET NAME = ?,
WHERE ID = ?;
#모든 필드를 수정
UPDATE MEMBER
SET
NAME=?
AGE=?
ADDRESS=?
...
WHERE
ID=?;
Member memberA = em.find(Member.class, "memberA");
em.remove(memberA);
앞선 생성/수정과정과 마찬가지로, 삭제쿼리 역시 쓰기지연 저장소에 추가됩니다.
영속성 컨텍스트에서 즉시 엔티티가 삭제되고, 삭제쿼리가 플러시되면 데이터베이스에서도 삭제됩니다.
플러시는 영속성 컨텍스트의 변경내용을 데이터베이스에 반영합니다.( 동기화 )
동작과정
플러시를 일으키는 방법
플러시 모드 옵션
엔티티가 준영속”detached” 상태라는 것은, 영속성 컨텍스트에서 분리되어 관리대상에서 벗어남을 의미하며, 앞서 설명한 영속성 컨텍스트의 기능들을 활용할 수 없음을 의미합니다.
영속성 컨텍스트에서 분리되는 방법: 준영속 엔티티를 만드는 방법
준영속과 비영속은 매우 가까운 상태입니다. 하지만, 준영속 상태는 이미 영속된적이 있는 엔티티로, 식별자 값이 존재합니다.
또한 준영속 → 영속상태로 가는 동작을 merge라고 합니다. 책에서는 길게 설명하지만, 짧게 요약하자면,
“merge 와 persist는 매우 비슷하지만, merge는 식별자 값을 제외한 값을 영속성 컨텍스트에 복사 후 리턴한다” 입니다.
복습질문 정답
엔티티 매니저 팩토리는 엔티티 매니저를 생성하는 객체, DB 커넥션 풀을 관리하고,
JPA의 엔티티메니저는 엔티티를 저장/수정/삭제/조회 하는 등, 엔티티와 관련된 많은 일을 처리하는 엔티티 관리자입니다(이름과 같은 역할).
또한 영속성 컨텍스트에 엔티티를 저장하면서 캐싱/변경감지/쓰기지연 등, CRUD의 성능을 개선합니다.
성능상의 이점과 엔티티 간 동일성을 보장해주는 장점이 있습니다.
트랜잭션을 커밋할때까지 엔티티를 1차캐시에 저장해놓고, 쓰기지연 SQL저장소에 쓰기 SQL을 저장해놓는다. 이후 트랜잭션이 커밋되면 flush작업과 함께, 쓰기 SQL을 데이터베이스에 전달하는것을 쓰기지연이라고 하고,
더 알아보기 정답