3. 영속성 관리
3-1. 엔티티 매니저 팩토리와 엔티티 매니저
EntityManagerFactory
- 데이터베이스를 하나만 사용할 경우, 일반적으로 EntityManagerFactory 를 하나만 생성한다.
- 생성 시, 비용이 아주 많이 든다 → 하나만 만들어서 애플리케이션 전체에서 공유하도록 설계되어 있다.
- 아래 코드를 통해 생성할 수 있으며, Persistence.createEntityManager(”jpabook”) 호출 시 META-INF/persistence.xml에 있는 정보를 바탕으로 EntityManagerFactory를 생성한다.
EntityManagerFactory emf = Persistence.createEntityManager("jpabook");
<persistence-unit name="jpabook">
<properties>
<!-- 필수 속성 -->
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/jpabook"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
<!-- 옵션 -->
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
<property name="hibernate.use_sql_comments" value="false" />
<property name="hibernate.id.new_generator_mappings" value="true" />
</properties>
</persistence-unit>
→ 인자로 “jpabook”을 준 이유는 persistence-unit이름을 “jpabook”으로 정의했기 때문이다.
EntityManager
- EntityManagerFactory를 통해 생성
- 생성 시 비용이 거의 들지 않는다.
EntityManager em = emf.createEntityManager();
- 데이터베이스 연결이 꼭 필요한 시점까지 커넥션을 얻지 않는다. 보통 트랜잭션을 시작할 때 커넥션을 획득한다.
3-2. 영속성 컨텍스트
Persistence Context
- 엔티티를 영구 저장하는 환경
- 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
- persist() 메소드는 엔티티 매니저를 사용해서 엔티티를 영속성 컨텍스트에 저장한다.
- 엔티티 매니저를 생성할 때 하나 만들어진다. 엔티티 매니저를 통해서 영속성 컨텍스트에 접근할 수 있고, 영속성 컨텍스를 관리할 수 있다.
- 여러 엔티티 매니저가 같은 영속성 컨텍스트에 접근할 수도 있다. → 자세한 내용은 11장
3-3. 엔티티의 생명주기
엔티티의 상태 4가지
비영속 (new, transient) 영속성 컨텍스트와 전혀 관계가 없는 상태, 순수한 객체 상태
영속 (managed) | 영속성 컨텍스트에 저장된 상태, 영속성 컨텍스트가 관리하는 상태 |
준영속 (detached) | 영속성 컨텍스트에 저장되었다가 분리된 상태 |
삭제 (removed) | 삭제된 상태, 엔티티를 영속성 컨텍스트와 데이터베이스를 삭제한 상태 |
엔티티의 생명주기
3-4. 엔티티 컨텍스트의 특징
특징
- 식별자 값을 통해 엔티티를 구분한다. → 영속 상태는 식별자 값이 반드시 있어야 하며, 없을 경우 예외가 발생한다.
- 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 저장된 엔티티를 데이터베이스 반영한다.
- 엔티티 컨텍스트가 엔티티를 관리하면 다양한 이점이 있다.
- 1차 캐시 (first level cache)
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
- 변경 감지 (dirty checking)
- 지연 로딩 (lazy loading)
지금부터 엔티티를 조회/등록/수정/삭제 하면서 영속성 컨텍스트의 이점을 알아보자.
엔티티 조회
영속성 컨텍스트는 내부에 1차 캐시를 두고 있다.
→ 1차 캐시는 key = @Id로 매핑한 식별자(데이터베이스의 PK), value = 엔티티 인스턴스인 map
조회 코드
em.find(entityClass, primaryKey);
동작 방식
- 1차 캐시에서 엔티티를 찾는다.
- 1차 캐시에 엔티티가 없다면, 데이터베이스에서 조회한다.
- 조회한 데이터로 엔티티를 생성해서 1차 캐시에 저장한다.
- 조회한 엔티티를 반환한다.
1차 캐시(first level cache)의 이점
- 성능 향상
- → 1차 캐시에 엔티티가 존재한다면, 데이터베이스에서 조회할 필요가 없다.
- 엔티티의 동일성 보장
- → 같은 엔티티를 여러 번 조회하여도, 1차 캐시에 있는 같은 인스턴스를 반환한다.
엔티티 등록
등록 코드
EntityTransaction transaction = em.getTransaction();
transaction.begin();
// sql을 데이터베이스에 보내지 않는다.
em.persist(entityInstance);
// 트랜잭셕 커밋 시, 데이터베이스에 insert sql을 보낸다.(flush)
transaction.commit();
동작 방식
- 앤티티 매니저는 트랜잭션을 커밋하기 전까지 1차 캐시에 엔티티를 저장하고, 쓰기 지연 SQL 저장소에 insert sql을 저장한다.
- 트랜잭션을 커밋하면, flush를 한다. (transactional write-behind)→ 쓰기 지연 SQL 저장소에 모인 쿼리를 데이터베이스에 보낸다.
- → 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업
- 데이터베이스 트랜잭션을 커밋한다.
트랜잭션을 지원하는 쓰기 지연(transactional write-behind)의 이점
- 모아둔 쿼리를 데이터베이스에 한 번에 전달하므로 성능 향상
엔티티 수정
엔티티 조회 후, 데이터를 변경하면 변경사항을 데이터베이스에 자동으로 반영한다.
수정 코드
Member memberA = em.find(Member.class, "memberA");
memberA.setUsername("hi");
memberA.setAge(10);
동작 방식
- 엔티티를 영속성 컨텍스트에 보관 시, 최초 상태를 스냅샷으로 저장해둔다.
- 데이터 변경 후, 트랜잭션을 커밋하면 flush가 호출된다.
- 스냅샷과 엔티티를 비교하여 변경된 엔티티를 찾는다. (dirty checking)
- 변경된 엔티티가 있으면 수정 쿼리를 생성하여 쓰기 지연 저장소에 저장한다.
- 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
- 데이터베이스 트랜잭션을 커밋한다.
변경 감지(dirty checking) 특징
- 영속 상태의 엔티티에만 적용된다.
- 기본적으로 엔티티의 모든 필드를 업데이트한다.→ 데이터베이스에 동일한 쿼리를 보내면, 데이터베이스는 이전에 한 번 파싱된 쿼리를 재사용할 수 있다.→ 수정된 필드만 update하도록 설정할 수 있으나, 필드가 30개 이상되는 것은 설계가 잘못 되었을 가능성이 높다. (@org.hibernate.annotation.DynamicUpdate)
→ 필드가 30개 이상일 때는 모든 필드를 업데이트하는 기본 방식이 느릴 수 있다.
→ 수정 쿼리가 항상 같기 때문에, 애플리케이션 로딩 시점에 미리 수정 쿼리를 생성해두고 재사용할 수 있다.
엔티티 삭제
삭제 코드
Member memberA = em.find(Member.class, "memberA");
em.remove(memberA)
동작 방식
- em.remove() 호출 시, 영속성 컨텍스트에서 엔티티가 제거되고, 삭제 쿼리를 쓰기 지연 저장소에 저장한다.
- 트랜잭션을 커밋하면 flush가 호출된다.
- 데이터베이스에 삭제 쿼리를 전달한다.
- 데이터베이스 트랜잭션을 커밋한다.
3-5. 플러시
flush() 동작 방식
- dirty checking 하여, 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 저장한다.
- 쓰기 지연 SQL 저장소의 쿼리(등록, 수정, 삭제)를 데이터베이스에 전송한다.
영속성 컨텍스트를 flush 하는 방법
- em.flush() → 직접 호출
- 트랜잭션 커밋 → flush가 자동 호출된다.
- JPQL 쿼리 실행 → flush가 자동 호출된다.
flush mode 옵션
- FlushModeType.AUTO: 커밋이나 쿼리를 실행할 때 플러시 (default)
- FlushModeType.COMMIT: 커밋할 때만 플러시
em.setFlushMode(FlushModeType.COMMIT);
3-6. 준영속 (Detached)
영속 상태의 엔티티를 준영속 상태로 만드는 방법
- em.detach(entity): 특정 엔티티를 준영속 상태로 전환한다.
- em.clear(): 영속성 컨텍스트를 초기화한다.
- em.close(): 영속성 컨텍스트를 종료한다.
detach()
특정 엔티티를 준영속 상태로 만든다.
특정 엔티티를 관리하기 위한 모든 정보(1차 캐시, 쓰기 지연 SQL 저장소에 있는 정보들)가 제거된다.
clear()
해당 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만든다.
close()
해당 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만든다.
merge()
준영속/비영속 상태의 엔티티를 영속 상태로 만든다.
준영속/비영속 상태의 엔티티의 정보를 가지고 새로운 영속 상태의 엔티티를 만들어 반환한다.
동작방식
- 파라미터로 넘어온 준영속/비영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다.
- 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고 1차 캐시에 저장한다.
- 조회한 영속 엔티티에 준영속/비영속 엔티티의 값을 채워 넣는다.
- 영속 엔티티를 반환한다.