01. 영속성 컨텍스트의 개념
JPA에서 가장 중요한 것은 객체와 관계형 데이터베이스의 매핑하기, 그리고 영속성 컨텍스트라고 할 수 있다.
EntityManagerFactory와 EntityManager의 동작
사용자로부터 요청이 들어오면 웹 어플리케이션에서, EntityManagerFactory가 EntityManager를 생성하고, EntityManager는 커넥션 풀에 있는 Connection 객체를 사용하여 DB에 접근한다.
영속성 컨텍스트란?
- JPA를 이해하는데 가장 중요한 용어 중 하나로,
엔티티를 영구 저장하는 환경
이라는 뜻이다. - 논리적인 개념으로 눈에 보이지 않는다.
EntityManager.persist(entity);
처럼, EntityMananger를 통해 영속성 컨텍스트에 접근이 가능하다. (이것은 Entity를 DB에 저장하는 것이 아니라, 영속성 컨텍스트에 저장한다는 뜻이다.)
환경
- J2SE 환경은 EntityManager와 PersistenceContext가 1:1 관계이다.
- J2EE 환경은 스프링 프레임워크와 같은 컨테이너 환경을 말하고, EntityManager와 PersistenceContext가 N:1 관계이다.
02. 엔티티의 생명주기와 예시 코드
엔티티의 생명주기는 크게 4가지로 나뉜다.
- 비영속 (new/transient)은 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태이다.
- 영속 (managed)는 영속성 컨텍스트에 의해 관리되는 상태이다.
- 준영속 (detached)는 영속성 컨텍스트에 저장되었다가 분리된 상태이다.
- 삭제 (removed)는 삭제된 상태이다.
예시 코드
예시 코드들은 아래 doSomething 부분에 작성될 코드들이니 참고해주시기 바랍니다.
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction(); // 트랜잭션 받기
tx.begin(); // 트랜잭션 시작
try {
// doSomething
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
비영속
- 현재 DB에는 아무것도 들어가 있지 않은 상태이다.
- 아래 코드는 객체를 생성만 한 상태(=비영속)로, 영속성 컨텍스트에 member 객체가 들어가있지 않다. 실행 로그르 보면 어떠한 쿼리문도 날아가지 않은 것을 볼 수 있다. 따라서 당연히 DB에는 아무것도 저장되지 않는다.
Member member = new Member();
member.setId(100L);
member.setName("HelloJPA");
영속
- 현재 DB에는 아무것도 들어가 있지 않은 상태이다.
- 아래 코드는
em.persist(member)
를 통해 객체를 저장한 상태(=영속)로 만든 것이다. member는 영속 컨텍스트에 들어가 EntityManager에 의해 관리된다. member는 DB에 저장된다. before과 after를 찍어봄으로써, 쿼리문은em.persist(member);
에서 날아가는게 아니라,tx.commit()
으로 커밋 되었을 때 날아감을 알 수 있다.
Member member = new Member();
member.setId(101L);
member.setName("HelloJPA");
System.out.println(" --- before --- ");
em.persist(member);
System.out.println(" --- after --- ");
준영속과 삭제 (이 부분은 간단하게 코드로만 알아보겠다.)
em.detach(member); // 영속성 컨텍스트에서 분리 (=준영속 상태)
em.remove(member); // 객체를 삭제한 상태 (=삭제)
03. 영속성 컨텍스트의 이점
영속성 컨텍스트의 이점은 다음과 같다.
1차 캐시, 동일성(identity)의 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지 (Dirty Checking), 지연 로딩 (Lazy Loading)
04. 조회 예시 코드
엔티티 1차 캐시에서 조회, 예시 코드
- 위 코드(em.persist(member);)로 엔티티를 영속한 상태이다. 1차 캐시의 @Id에는 키인 "member1"이, Entity에는 Entity객체 그 자체인 member가 들어가있는 상태이다. 이 상태에서 1차 캐시에서 조회하는 코드는 다음과 같다. (같은 트랜잭션 내에서 실행해야 하므로 해당 예시를 진행하는 전체 코드를 작성했다. 물론 try문의 doSomething 부분에 작성해야 한다.)
em.persist(member);
로 인한 insert 쿼리문만 나가고, select 쿼리문은 볼 수 없다. 이것은 조회 시, 바로 DB에서 찾는게 아니라 영속성 컨텍스트의 1차 캐시에서 찾음을 의미한다.
Member member = new Member();
member.setId(101L);
member.setName("HelloJPA");
System.out.println(" --- before --- ");
em.persist(member);
System.out.println(" --- after --- ");
Member findMember = em.find(Member.class, 101L); // 1차 캐시에서 조회된다.
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.name = " + findMember.getName());
엔티티 DB에서 조회, 예시 코드
- 바로 위에서 작성한 코드로 인해 Id가 101이고 name이 HelloJPA인 member가 들어가 있는 상태이다.
- 다음은 동일한 객체를 조회하는 코드이고, 실행 로그에서 select 쿼리문이 한번만 나오는 것을 볼 수 있다. findMember1은 select 쿼리문으로 DB에서 가져오고, findMember2는 findMember1에서 가져온 것을 이용해 1차 캐시에서 가져온다.
Member findMember1 = em.find(Member.class, 101L);
Member findMember2 = em.find(Member.class, 101L);
최종적으로 조회와 관련한 동작 방식을 정리해보면 다음과 같다.
- 1차 캐시에 저장된 경우, 1차 캐시에서 조회한다.
- 1차 캐시에 저장되지 않은 경우, DB에서 조회 후 1차 캐시에 저장하고 해당 Entity를 반환한다.
05. 영속 엔티티의 동일성 보장과 예시 코드
영속 엔티티의 동일성 보장
- 1차 캐시로 반복 가능한 읽기 등급의 트랜잭션 격리 수준을 DB가 아닌 애플리케이션 차원에서 제공한다.
예시 코드 : 위에서 작성한 member1과 member2로 확인해보자.
Member findMember1 = em.find(Member.class, 101L);
Member findMember2 = em.find(Member.class, 101L);
System.out.println(findMember1 == findMember2);
06. 트랜잭션을 지원하는 쓰기 지연과 예시 코드
EntityManager 는 데이터 변경 시 트랜잭션을 시작해야 한다.
- persist 할 때, 쿼리문을 DB에 보내는게 아니라, 커밋하는 순간 DB에 쿼리문을 보낸다. 실행 로그를 보면 구분선 이후에 쿼리문이 나타나는 것을 볼 수 있다.
Member member1 = new Member(150L, "A");
Member member2 = new Member(160L, "B");
em.persist(member1);
em.persist(member2);
System.out.println("-----");
이 부분은 이해를 확실히 하기 위해 자료를 첨부하여 설명을 덧붙이겠다. persist를 하면 1차 캐시에 저장
이 되고 쓰기 지연 SQL 저장소
에 쿼리문이 생성되어 저장된다. 이후 트랜잭션을 커밋하면 쓰기 지연 SQL 저장소에 있던 쿼리문들이 한꺼번에(여기서는 두 개의 insert문) 순차적으로 flush, commit 된다.
batch_size
- 추가적으로, META-INF/persistence.xml 파일에서 <property name="hibernate.jdbc.batch\_size" value="10"/> 로 배치 사이즈를 정해주면 한번에 날아가는 쿼리문의 개수를 지정할 수 있다고 한다.
07. 변경 감지와 예시 코드
변경 감지
- JPA는 자바 컬렉션처럼 다루는 것이다. 우리가 컬렉션에서 setter로 값을 바꾸면 바로 적용이 되듯이, JPA도 마찬가지로, 따로 update 쿼리문을 작성할 필요가 없다.
예시 코드
- 엔티티를 찾아서 persist나, update를 하지 않아도 DB에 값이 갱신된다.
Member member = em.find(Member.class, 150L);
member.setName("ZZZZ");
변경 감지 (Dirty Checking)
이 부분도 보다 정확한 이해를 위해 자료를 첨부하겠다. 값을 읽어온 최초의 상태를 스냅샷을 찍어둔다. 트랜잭션이 커밋되는 시점에 Entity와 스냅샷을 비교하여 update 쿼리를 쓰기 지연 SQL 저장소에 작성하고 DB에 반영하여 커밋한다. 엔티티 삭제도 동일한 매커니즘이다.
08. 삭제와 예시 코드
예시 코드 : 매커니즘은 엔티티 수정과 동일하므로 코드만 간단하게 알아보겠다.
Member memberA = em.find(Member.class, “memberA");
em.remove(memberA);
그 외 추가적인 내용들
- 1차 캐시는 데이터베이스 한 트랜잭션 내에서만 효과가 있다.
- 2차 캐시는 JPA, Hibernate에서 공유하는 캐시이다.
- JPA는 조회하면 무조건 영속성 컨텍스트 1차 캐시에 올려놓는다.
쿼리문은 commit 되는 시점에 나가며, em.persist(object)는 영속성 컨텍스트에서 관리됨을 의미한다.