[JPA 기초] 02. 영속성 컨텍스트

Date:     Updated:

카테고리:

태그:

JPA의 영속성 컨텍스트와 엔티티 생명주기

위 링크를 참고하여 포스팅을 작성하였습니다.

JPA에서 가장 중요한 2가지

  1. 영속성 컨텍스트: Persistence Context
  2. 엔티티 매핑: 객체와 관계형 DB를 매핑하는 것, Obejct Relational Mapping (ORM)

영속성 컨텍스트

영속성 컨텍스트는 buffer라고 생각하면 이해하기 쉽다.

Entity가 변경될 때마다 commit을 날려버리면 네트워크를 타고 DB서버를 왕복해야 하는 비용이 commit을 할 때마다 발생한다.

이러한 비용을 최대한 줄이기 위해, 영속성 컨텍스트라는 buffer에 변경 사항들을 모두 저장하여 두고, 마지막에 딱 한 번의 commit으로 네트워크를 타는 작업을 최소화할 수 있다.

EntityManager.persist(entity); 를 통해 entity를 영속성 컨텍스트 안으로 귀속시킬 수 있다.

Persistence Context

영속성 컨텍스트는 1차 캐시SQL 저장소 2가지 영역으로 나뉜다.

  • 1차 캐시: entity가 저장되는 장소
  • SQL 저장소: entity의 변경사항(I, D, U)을 쿼리문으로 쌓아두는 곳

위와 같이 알아두면 영속성 컨텍스트를 이해하기 편하다.

1. Entity 생명주기

Entity Lifecycle

public class JpaMain {
    public static void main(String[] args) {

        /* Entity Manager Factory 생성 */
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        /* Entity Manager 생성 */
        EntityManager em = emf.createEntityManager();

        /* Transaction 실행 */
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

            /* 비영속 상태: 단순히 객체를 생성한 상태 */
            Member member = new Member();
            member.setId("A123")
            member.setUsername("James");

            /* 영속 상태: 객체를 영속성 컨텍스트에 저장한 상태 */
            em.persist(member);

            /* 준영속 상태: member entity를 영속성 컨텍스트에서 분리 */
            em.detach(member);

            /* 삭제 상태: 객체를 삭제 */
            em.remove(member);

            tx.commit();

        } catch (Exception e) {
            tx.rollback();

        } finally {
            /* Entity Manager 종료 */
            em.close();
        }
        emf.close();
    }
}
  1. 비영속
    • 말 그대로, entity가 영속성 컨텍스트와 전혀 관계가 없는 상태이다.
  2. 영속
    • 1차 캐시에 entity가 등록되어 있는 상태
    • 즉, entity가 JPA의 관리를 받고 있는 상태
    • 영속되어 있는 entity를 조회할 때, DB에 select query를 날리지 않고 1차 캐시에 있는 entity를 조회한다.
  3. 준영속
    • 영속되어 있다가 분리된 entity
    • 준영속 상태로 전환하는 방법
      1. em.detach(entity)
        • 특정 entity만 준영속 상태로 전환
      2. em.clear()
        • 영속성 컨텍스트를 완전히 초기화
      3. em.close()
        • 영속성 컨텍스트를 종료

2. 특장점

1) 1차 캐시

1st cache

Member member = new Member();
member.setId("member1");
member.setUsername("James");

// 영속성 컨텍스트의 1차 캐시에 entity 저장
em.persist(member);

// DB가 아닌, 1차 캐시에 저장된 값을 불러 옴
Member findMember = em.find(Member.class, "member1");

// 1차 캐시 조회 후, 1차 캐시에 없는 값은 DB에서 불러 옴
Member findMember2 = em.find(Member.class, "member2");

2) 동일성(identity) 보장

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");

System.out.println(a == b)  // true
  • em.find로 조회하는 순간, ID에 해당하는 entity는 영속성 컨텍스트의 1차 캐시에 저장됨
  • 같은 ID를 조회하면, 1차 캐시를 먼저 조회하여 결과 값을 반환하기 때문에 SELECT Query를 또 뿌리지 않음
  • 1차 캐시를 활용하여, REPEATABLE READ 등급의 트랜잭션 격리 수준을 DB가 아닌 AP 차원에서 제공
  • 성능적으로 크게 향상되는 부분은 없음

3) 트랜잭션을 지원하는 쓰기 지연: Transactional write-behind

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        /* Transaction 실행 */
        tx.begin();

        /* 1차 캐시에 memberA, memberB entity 저장 */
        /* SQL 저장소에 memberA, memberB를 DB에 저장할 INSERT SQL 저장 */
        em.persist(memberA);
        em.persist(memberB);


        /* commit하는 순간 DB에 INSERT SQL 실행 */
        tx.commit();
    }
}

write-behind-1

  • 1차 캐시에는 memberA, memberB entity 저장
  • SQL 저장소에는 memberA, memberB를 DB에 저장할 INSERT SQL 저장

write-behind-2

  • commit을 실행하면 SQL 저장소에 있던 SQL문들이 DB로 flush

4) 변경 감지: Dirty Checking

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        /* SELECT from 영속 엔티티 */
        Member member = em.find(Member.class, 101L);

        /* UPDATE 영속 엔티티 */
        member.setName("ChangedName");
        member.setAge("30");

        // em.update(member);  -> JPA가 1차 캐시에서 알아서 변경사항을 비교해 줌
        // em.persist(member); -> find에서 이미 영속성 컨텍스트 안에 member를 포함시켰으므로 중복 실행이 됨

        tx.commit();
    }
}

dirty-checking

  1. em.find로 entity를 1차 캐시에 저장할 때 @Id, @Entity, 그리고 불러올 당시의 entity에 대한 snapshot을 저장함

    @Id @Entity snapshot
    1 memberA (name → James / age → 20) memberA (name → James / age → 20)
  2. entity를 업데이트하면 @Entity의 내용이 변경됨

    @Id @Entity snapshot
    1 memberA (name → Hans / age → 30) memberA (name → James / age → 20)
  3. commit을 하면 JPA는 @entitysnapshot의 내용을 비교함

    memberA의 이름이 Hans, 나이가 30으로 변경 되었군

  4. 변경된 내용에 대해 UPDATE SQL 생성 후, SQL 저장소에 저장

    UPDATE MEMBER
    SET Username = 'Hans', Age = 30
    WHERE ID = 1;
    
  5. flush를 통해 SQL문 실행으로 entity의 변경 된 내용을 DB에 저장

3. Flush

1) 개념

  • 영속성 컨텍스트의 변경내용을 DB에 동기화
  • 영속성 컨텍스트를 비우지 않음
  • TR 작업 단위가 중요 → commit 직전에만 동기화 하면 됨

2) 방법

  1. 직접 호출

    • em.flush()

      Member member = new Member(200L, "member200");
      em.persist(member);
      
      em.flush();
      em.clear();
      

      테스트를 할 때, em.flush로 DB 동기화 작업을 마치고, em.clear로 영속성 컨텍스트를 완전히 초기화 하는 방법도 종종 사용한다.

  2. 자동 호출

    • tx.commit()
    • JPQL 쿼리 실행

JPA 카테고리 내 다른 글 보러가기

댓글 남기기