JPA를 이해하려면 영속성 컨텍스트를 이해해야한다. JPA를 이해하는데 있어서 가장 중요한 두가지가 있다.
- 객체와 관계형 데이터베이스 매핑
- DB를 어떻게 설계하고 객체를 어떻게 설계해서 중간에서 JPA를 어떻게 매핑해서사용할 것인지- 영속성 컨텍스트
- JPA가 실제로 내부에서 어떻게 동작하는지
이번엔 JPA에서 가장 중요한 개념인 영속성 컨텍스트에 대해서 정리해보겠다.
JPA를 쓰게되면, EntityManagerFactory와 EntityManager에 대해서 이해해야 한다.
출처 : 인프런 - 자바 ORM 표준 JPA 프로그래밍(기본편, 김영한)
먼저 웹 어플리케이션을 개발한다고 가정했을 때 고객의 요청이 올 때마다 EntityManagerFactory를 통해서 EntityManager를 생성하게 된다.
EntityManagerFactory를 쉽게 생각하면 EntityManager 를 생성하는 공장 역할이고, DB연결 / 캐시 / 메타데이터 관리 역할을 한다.
- 어플리케이션 로딩시점에 딱 1번만 생성되어서 공유한다.
EntityManager는 내부적으로 데이터베이스커넥션을 사용해서 DB를 사용하게 된다.
- JPA에서 엔티티(객체)와 데이터베이스 사이의 연결 다리 역할을 하는 객체이고 조회, 저장, 수정, 삭제 등 모든 DB 작업을 담당하는 JPA의 실행 담당자이다.
- 스레드간 공유가 절대 안된다. 요청마다 새로 생성해서 사용한다.
EntityManagerFactory와 EntityManager에 대해서 간단하게 알아봤는데, 그렇다면 영속성 컨텍스트는 도데체 뭘까?
영속성컨텍스트란 "엔티티를 영구 저장하는 환경" 이라는 뜻이다.
DB에 저장한다 라는 개념보다는 영속성 컨텍스트를 통해서 "엔티티를 영속화 한다." 라고 생각하면 된다.
- 엔티티를 "영속성컨텍스트" 에 저장한다. -> 영속화
- 이 후 커밋 시점에 flush가 동작하면서 SQL을 발송하고 commit되면서 DB에 반영된다.
영속성 컨텍스트는 논리적인 개념으로, 눈에 보이지 않는다. 보통 EntityManager를 통해서 영속성컨텍스트에 접근한다.
EntityManager.persist(entity);
출처 : 인프런 - 자바 ORM 표준 JPA 프로그래밍(기본편, 김영한)
EntityManager를 생성하게되면 1:1로 영속성컨텍스트가 생성된다.
-> EntityManager 안에 영속성컨텍스트라는 눈에 보이지않는 공간이 생긴다.
출처 : 인프런 - 자바 ORM 표준 JPA 프로그래밍(기본편, 김영한)
- 비영속(new / transient)
- 아직 영속성컨텍스트와 전혀 관련이 없는 새로운 상태Member member = new Member(); // 비영속 상태, 객체만 생성 member.setName("hello"); // EntityManager가 관리하지 않음, DB와도 아무 관련 없음
- 영속(managed)
- 영속화되어 영속성컨텍스트에 관리 되는 상태// JPA가 관리 중, 변경 감지 가능, 쓰기 지연 저장소에 저장됨 // flush() or commit() 시점에 insert 쿼리 실행됨 // 1차 캐시에 저장됨 em.persist(member); // 영속 상태 진입
- 준영속(detached)
- 영속성컨텍스트에 저장되었다가(영속화) 다시 분리 된 상태em.detach(member); // 또는 em.close(); // EntityManager 종료 // 엔티티 객체는 살아있지만, 변경 감지 안 됨 // 다시 영속으로 만들려면 em.merge() 필요
- 삭제(removed)
- 삭제 된 상태// 영속성 컨텍스트에는 있지만, flush or commit 시점에 delete 쿼리나감 // 실제 DB 반영은 commit 이후 em.remove(member);
1. 1차캐시

1차캐시에서 조회

1차캐시에 없으면 DB에서 조회
출처 : 인프런 - 자바 ORM 표준 JPA 프로그래밍(기본편, 김영한)
// 엔티티 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUserName("회원1");
// 엔티티를 영속, 1차캐시에 저장됨
em.persist(member);
// 1차캐시에서 조회
Member findMember = em.find(Member.class, "member1");
// 1차캐시에 없으면 DB조회 -> 1차캐시 저장 -> Entity반환
Member findMember2 = em.find(Member.class, "member2");
1차 캐시는 EntityManager가 생성될 때 함께 만들어지는 영속성 컨텍스트 내부의 메모리 캐시로, 트랜잭션 범위 내에서만 유효하다.
따라서 같은 EntityManager 내에서는 반복 조회 시 DB가 아닌 캐시에서 데이터를 가져와 성능 이점을 줄 수 있지만, 애플리케이션 전체에서 공유되지는 않으므로 한계도 있다.
애플리케이션 레벨에서 공유 가능한 캐시는 JPA나 Hibernate에서 제공하는 2차 캐시(Second-Level Cache)를 사용해야 한다.
2. 영속엔티티의 동일성 보장
Member findMember = em.find(Member.class, 101L);
Member findMember2 = em.find(Member.class, 101L);
// 동일성 비교 true
System.out.println(findMember == findMember2);
JPA에서는 같은 트랜잭션 내에서 1차 캐시에 저장된 엔티티를 조회할 경우, 항상 동일한 객체 인스턴스를 반환하므로 객체의 동일성이 보장된다.
즉, 동일한 식별자(PK)로 여러 번 조회하더라도 같은 엔티티 인스턴스를 공유하게 되어, 객체 간 비교 시에도 == 연산이 성립한다.
3. 트랜잭션을 지원하는 쓰기지연
EntityManager em = emf.createEntityManager();
EntityManager transaction = em.getTransaction();
transaction.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
// 여기까지 Insert쿼리 안나감, 영속성컨텍스트에 쌓이는 중
// commit하는 시점에 flush가 동작 -> 쓰기지연저장소에서 쿼리보냄
// -> 이후 commit되면서 db에 진짜 반영
transaction.commit();

출처 : 인프런 - 자바 ORM 표준 JPA 프로그래밍(기본편, 김영한)
- 영속화 되는 시점에 쿼리가 바로 나가지 않고 쓰기지연저장소와 1차캐시에 저장되었다가 commit되는 시점에 db에 반영되는 방식을 통해서 버퍼링같은 기능을 사용할 수 있다.
- Hibernate에는 Hibernate.jdbc.batch_size 라는 옵션이 있는데 이 옵션을 사용하면 지정한 사이즈만큼 쿼리를 모아서 db로 한번에 보내고 commit시킨다.
4. 변경감지
EntityManager em = emf.createEntityManager();
EntityManager transaction = em.getTransaction();
transaction.begin(); // 트랜잭션 시작
// 영속 엔티티 조회
member memberA = em.find(Member.class, "memberA");
// 영속 엔티티 수정
// JPA에서는 영속 상태의 엔티티에 필드 값 변경이 발생하면,
// 트랜잭션 커밋 시점에 변경된 내용을 자동으로 감지하여 UPDATE 쿼리를 생성하고 실행한다.
memberA.setUserName("hi");
memberA.setAge(10);
// 이런 코드 작성하지 않아도 됨
em.update(member);
transaction.commit();
출처 : 인프런 - 자바 ORM 표준 JPA 프로그래밍(기본편, 김영한)
- JPA는 commit을 하게되면 내부적으로 flush가 호출된다.
- flush가 호출되면 1차캐시에서 Entity와 스냅샷을 비교한다.
- 스냅샷은 값을 읽어온 최초 시점의 상태를 저장 해놓은 공간이다.
- 스냅샷을 비교했을 때 Entity와 스냅샷의 값이 다르다면 UPDATE SQL이 생성된다.
- 이후 commit되면서 DB에 반영된다.
5. 엔티티 삭제
Member memberA = em.find(Member.class, "memberA");
em.remove(memberA); // 엔티티 삭제
JPA에서 SELECT 쿼리는 쓰기 지연 저장소를 거치지 않고, find() 호출 시점에 즉시 데이터베이스로 쿼리가 나간다.
반면, INSERT, DELETE, UPDATE 쿼리는 쓰기 지연 저장소에 우선 저장되었다가 flush 시점에 한 번에 데이터베이스로 전송된다.
단, UPDATE는 예외적으로 flush 시점에 변경 감지(dirty checking) 가 수행되어 이때 변경된 필드가 감지되면 비로소 쓰기 지연 저장소에 UPDATE SQL이 생성되고, 이후 DB로 반영된다는 점에서 차이가 있다.
영속성컨텍스트의 변경내용을 DB에 반영하는 JPA 핵심 메소드