먼저 영속성 컨텍스트를 생성하고 관리하려면 엔티티 매니저 팩토리와 엔티티 매니저가 필요하다.
Entity Manager Factory (이하 emf)
- 이름 그대로 엔티티 매니저를 만드는 공장인데 생산 비용이 굉장히 크다.
- JPA를 동작시키기 위한 기반 객체 생성
- DB 커넥션 풀 생성
- DB를 하나만 사용하는 애플리케이션은 일반적으로 emf를 하나만 생성한다.
- 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드 간에 공유가 가능하다.
Entity Manager (이하 em)
- emf가 공장이라면 em은 공장을 관리하는 매니저이다.
- em을 사용하여 엔티티를 DB에 CRUD할 수 있다.
- em은 내부에 DB 커넥션을 유지하면서 DB와 통신한다.
- em을 가상의 DB로 생각할 수 있다.
- DB 커넥션과 밀접한 관계를 가지므로 여러 스레드가 동시에 접근하면 동시성 문제가 발생한다.
emf에서는 다수의 em을 생성한다. em은 DB 연결이 꼭 필요한 시점까지 커넥션을 얻지 않는다.
보통 트랜잭션을 시작할 때 커넥션을 획득한다.
// emf 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("xxx");
// em 생성
EntityManager em = emf.createEntityManager();
// 트랜잭션 생성 - JPA는 항상 트랜잭션 안에서 데이터를 변경해야 한다.
EntityTransaction tx = em.getTransaction();
try {
tx.begin(); // 트랜잭션 시작
logic(em); // 비즈니스 로직
tx.commit(); // 트랜잭션 커밋
} catch (Exception e) {
tx.rollback(); // 예외 발생 시 트랜잭션 롤백
} finally {
em.close(); // em 자원해제
}
emf.close(); // emf 자원해제
그래서 영속성 컨텍스트란?
- 엔티티를 영구 저장하는 환경이다.
- em으로 엔티티를 저장하거나 조회하면 em은 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
- 영속성 컨텍스트는 em을 생성할 때 하나 만들어진다.
엔티티의 생명주기
엔티티 객체가 영속성 컨텍스트와 어떤 관계인지에 따라 생명주기가 변경된다.
- 비영속 : 영속성 컨텍스트와 전혀 관계가 없는 상태
- 영속 : 영속성 컨텍스트에 저장된 상태
- 준영속 : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제 : 삭제된 상태
// 엔티티 객체를 생성한 상태 (비영속)
Member member = new Member();
member.setId("memberId");
member.setId("회원");
// 엔티티 객체를 em을 통해 DB에 저장한 상태 - 영속성 컨텍스트에 의해 관리됨 (영속)
em.persist(member)
// 엔티티 객체를 영속성 컨텍스트에서 분리 (준영속)
em.detach(member)
em.clear(member)
em.close(member)
// 엔티티를 영속성 컨텍스트와 DB에서 삭제
em.remove(member)
영속성 컨텍스트 특징
- 영속 상태는 식별자 값(@Id)이 반드시 필요하다.
- persist(), find() 등의 쿼리를 쓰기 지연 SQL 저장소에 저장 후, 트랜잭션 커밋 시 DB에 반영한다.
- 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩의 장점이 있다.
1차 캐시
- persist() 는 1차 캐시에 엔티티 객체를 저장한다.
- find()를 호출하면 먼저 메모리에 있는 1차 캐시에서 엔티티를 찾고, 없다면 DB에서 조회한다.
- 그리고 1차 캐시에 저장한 후 영속 상태의 엔티티를 반환한다.
동일성 보장
Member firstMember = em.find(Member.class, "member1");
Member secondMember = em.find(Member.class, "member1");
- 위의 코드를 실행했을 때 동일성을 비교한다면 결과는 참일 것이다.
- 반복 호출해도 영속성 컨텍스트는 1차 캐시에 있는 같은 엔티티 인스턴스를 반환하기 때문이다.
1차 캐시를 통해 REPEATTABLE READ(반복 가능한 읽기) 등급의 트랜잭션 격리 수준을
애플리케이션 차원에서 제공한다는 장점이 있다.
쓰기 지연 (transactional write-behind)
- em은 트랜잭션을 커밋하기 직전까지 DB에 엔티티를 저장하지 않고, 내부 쿼리 저장소에 SQL을 모아둔다.
- 트랜잭션을 커밋할 때 저장소의 쿼리를 DB에 보내는데 이것을 트랜잭션을 지원하는 쓰기 지연이라 한다.
- 쓰기 지연을 통해 성능 향상, 트랜잭션 일관성 보장, DB 부하 감소의 이점을 가질 수 있다.
변경 감지
- 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해두는데 이것을 스냅샷이라 한다.
- flush 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾고, 적절한 update 쿼리를 전달한다.
- 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다.
- JPA의 기본 전략으로 엔티티의 모든 필드를 업데이트 한다.
- 데이터 전송량이 증가하는 단점이 있다.
- 애플리케이션 로딩 시점에 수정 쿼리(A)를 미리 생성해두고 재사용할 수 있다.
- 동일한 쿼리를 보내면 한 번 파싱된 쿼리(A)를 재사용할 수 있다.
// 영속 엔티티 조회 - 최초 상태 저장(스냅샷)
Member memberA = em.find(Member.class, "memberA");
// 스냅샷과 비교하여 변경된 엔티티 감지
memberA.setUsername("example");
memberA.setAge(10);