[JPA] 영속성 컨텍스트의 이점
08 Jun 2022해당 포스트는 인프런 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 듣고 정리한 글입니다.
영속성 컨텍스트
영속성 컨텍스트의 이점
- 1차 캐시
- 동일성(identity) 보장
- 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
- 변경 감지 (Dirty Checking)
- 지연 로딩(Lazy Loading)
엔티티 조회, 1차 캐시
1차 캐시
/* 엔티티를 생성한 상태 (비영속) */
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
/* 엔티티를 영속 및 1차 캐시에 저장 */
entityManager.persist(member);
위와 같이 Member 객체를 생성한 후 해당 객체를 엔티티에 영속 할 경우, JPA의 영속 컨텍스트는 member1
를 key 로 가진 member
entity를 1차 캐시로 생성한다.
/* 1차 캐시에서 조회 */
Member findMember = entityManager.find(Member.class, "member1");
영속 상태에 놓여있는 객체 EntityManager
를 통해 동일한 Id값을 입력해 불러올 경우 해당 객체는 DB에서 조회해오는 것이 아닌, 1차 캐시에서 조회해 온다.
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("hello");
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction entityTransaction = entityManager.getTransaction();
entityTransaction.begin();
try {
/* 비영속 객체 생성 */
Member member = new Member();
member.setId(102L);
member.setName("Hello JPA");
System.out.println("=== PERSISTENT BEFORE ===");
/* 객체 생성 영속 */
entityManager.persist(member);
System.out.println("=== PERSISTENT AFTER ===");
Member findMember = entityManager.find(Member.class, 102L);
log.debug("findMember.id : " + findMember.getId());
log.debug("findMember.name : " + findMember.getName());
entityTransaction.commit();
} catch (Exception e) {
entityTransaction.rollback();
} finally {
entityManager.close();
}
=== PERSISTENT BEFORE ===
=== PERSISTENT AFTER ===
23:19:52.847 [main] DEBUG com.dhaudgkr.jpastart.hellojpa.JpaPersistent - findMember.id : 102
23:19:52.847 [main] DEBUG com.dhaudgkr.jpastart.hellojpa.JpaPersistent - findMember.name : Hello JPA
23:19:52.860 [main] DEBUG org.hibernate.SQL -
/* insert com.dhaudgkr.jpastart.hellojpa.entity.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
DB에서 조회해 올시
만약 영속 상태에 있지 않은 객체를 DB를 통해 불러 올 경우는 어떻게 될까
Member findMember2 = entityManager.find(Member.class, "member2");
위의 이미지와 같이, 1차 캐시를 통해 먼저 조회 한 후 없을 경우 DB를 통해 조회해 온다. 이때 조회된 정보는 영속 컨텍스트의 1차 캐시에 저장된다.
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("hello");
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction entityTransaction = entityManager.getTransaction();
entityTransaction.begin();
try {
/* DB 에서 조회 */
log.debug("memberA 조회 - BEFORE");
Member findMemberA = entityManager.find(Member.class, 102L);
log.debug("memberA 조회 - BEFORE");
/* 1차 캐시에서 조회 */
log.debug("memberB 조회 - AFTER");
Member findMemberB = entityManager.find(Member.class, 102L);
log.debug("memberB 조회 - AFTER");
} catch (Exception e) {
entityTransaction.rollback();
} finally {
entityManager.close();
}
23:30:11.286 [main] DEBUG com.dhaudgkr.jpastart.hellojpa.JpaPersistent - memberA 조회 - BEFORE
23:30:11.300 [main] DEBUG org.hibernate.SQL -
select
member0_.id as id1_0_0_,
member0_.name as name2_0_0_
from
Member member0_
where
member0_.id=?
23:30:11.330 [main] DEBUG com.dhaudgkr.jpastart.hellojpa.JpaPersistent - memberA 조회 - AFTER
23:30:11.330 [main] DEBUG com.dhaudgkr.jpastart.hellojpa.JpaPersistent - memberB 조회 - BEFORE
23:30:11.330 [main] DEBUG com.dhaudgkr.jpastart.hellojpa.JpaPersistent - memberB 조회 - AFTER
하지만, 1차 캐시로 DB를 덜 조회한다는 이점이 있지만, entityManager는 하나의 트랙잭션 단위에서만 활동하기 때문에, 하나의 요청이 끝나면 , 영속 컨텍스트 및 캐시를 삭제 하므로 시스템에 큰 이점을 주지는 않는다.
영속 엔티티의 동일성 보장
Member a = entityManager.find(Member.class, "member1");
Member b = entityManager.find(Member.class, "member1");
System.out.println(a == b); /* 동일성 비교 true */
1차 캐시로 인해, 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다.
엔티티 등록
트랜잭션을 지원하는 쓰기 지연
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction entityTransaction = entityManager.getTransaction();
/* 엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다. */
entityTransaction.begin(); /* 트랜잭션 시작 */
entityManager.persist(memberA);
entityManager.persist(memberB);
/* 해당 INSERT SQL을 데이터베이스에 보내지 않고, 영속성 컨텍스트에 쌓아둔다. */
/* 커밋하는 순간 데이텁제이스에서 INSERT SQL을 보낸다. */
entityTransaction.commit(); /* 트랜잭션 커밋 */
entityManager.persist()
를 통해 entity를 저장할 경우 1차캐시와 동시에 쓰기 지연 SQL 저장소에해당 entity에 대한 INSERT SQL을 만들어 저장한다.
이후 entityTransaction.commit()
을 진행할 경우 DB로 SQL을 날린다.
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction entityTransaction = entityManager.getTransaction();
entityTransaction.begin();
try {
log.debug("memberA persist - BEFORE");
Member memberA = new Member(103L, "memberA");
entityManager.persist(memberA);
log.debug("memberA persist - AFTER");
log.debug("memberB persist - BEFORE");
Member memberB = new Member(104L, "memberB");
entityManager.persist(memberB);
log.debug("memberB persist - AFTER");
entityTransaction.commit();
} catch (Exception e) {
entityTransaction.rollback();
} finally {
entityManager.close();
}
23:35:35.375 [main] DEBUG com.dhaudgkr.jpastart.hellojpa.JpaPersistent - memberA persist - BEFORE
23:35:35.381 [main] DEBUG org.hibernate.event.internal.AbstractSaveEventListener - Generated identifier: 103, using strategy: org.hibernate.id.Assigned
23:35:35.419 [main] DEBUG com.dhaudgkr.jpastart.hellojpa.JpaPersistent - memberA persist - AFTER
23:35:35.419 [main] DEBUG com.dhaudgkr.jpastart.hellojpa.JpaPersistent - memberB persist - BEFORE
23:35:35.419 [main] DEBUG org.hibernate.event.internal.AbstractSaveEventListener - Generated identifier: 104, using strategy: org.hibernate.id.Assigned
23:35:35.422 [main] DEBUG com.dhaudgkr.jpastart.hellojpa.JpaPersistent - memberB persist - AFTER
23:35:35.465 [main] DEBUG org.hibernate.SQL -
/* insert com.dhaudgkr.jpastart.hellojpa.entity.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
23:35:35.474 [main] DEBUG org.hibernate.SQL -
/* insert com.dhaudgkr.jpastart.hellojpa.entity.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
Entity를 생성할 때 빈 생성자를 만들어야 하는데 JPA(영속성 컨텍스트)가 접근하기 위해서는 해당 생성자가 필요하다 접근제어자는 public 외에 아무거나 줘도 상관이없지만, 캡슐화를 위해
@NoArgsConstructor(access = AccessLevel.PROTECTED)
를 권장한다.
<property name="hibernate.jdbc.batch_size" value="10"/>
persistence.xml에 해당 옵션을 추가 할 경우 대량작업을 한번에 처리해야 하는 배치성 작업에 대한 SQL을 한번에 날릴 수 있는 JDBC batch 기능을 사용 할 수 있다.
엔티티 수정
변경감지
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction entityTransaction = entityManager.getTransaction();
entityTransaction.begin(); /* 트랜잭션 시작 */
/* 영속 엔티티 조회 */
Member memberA = entityManager.find(Member.class, "memberA");
/* 영속 엔티티 데이터 수정 */
member.setUsername("hi");
member.setAge(29);
entityTransaction.commit(); /* 트랜잭션 커밋 */
엔티티를 조회해 해당 객체를 변경할 경우 JPA에서 엔티티의 변경에 대한 변경감지(Dirty Checking) 가 이루어져 entityManager.update(memberA)와 같은 코드가 있지 않아도, UPDATE쿼리를 생성해서 날린다. 자바의 Collection객체와 같이 생각해주면 좋다.
flush()
가 된 데이터를 1차캐시의 스냅샷 공간에 저장하고 해당 트랜잭션이 커밋 되는 시점에 1차 캐시 내부에 있는 스냅샷 데이터와, 변경된 데이터를 비교하여 값이 변경 될 경우 UPDATE SQL을 만들어 DB에 날린다.
entity를 수정 할 경우 entity 내부에서 변경감지가 일어나 알아서 UPDATE SQL을 작성하므로, entityManager.persist()
를 통해 값을 넣어주면 안된다.