DEV ℧ Developer Diary

[JPA] 영속성 컨텍스트의 이점

해당 포스트는 인프런 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 듣고 정리한 글입니다.

영속성 컨텍스트

영속성 컨텍스트의 이점

  1. 1차 캐시
  2. 동일성(identity) 보장
  3. 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
  4. 변경 감지 (Dirty Checking)
  5. 지연 로딩(Lazy Loading)

엔티티 조회, 1차 캐시

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");

영속성 이점2

위의 이미지와 같이, 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(); /* 트랜잭션 커밋 */

영속성 이점3 영속성 이점4

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객체와 같이 생각해주면 좋다.

영속성 이점5

flush()가 된 데이터를 1차캐시의 스냅샷 공간에 저장하고 해당 트랜잭션이 커밋 되는 시점에 1차 캐시 내부에 있는 스냅샷 데이터와, 변경된 데이터를 비교하여 값이 변경 될 경우 UPDATE SQL을 만들어 DB에 날린다.

entity를 수정 할 경우 entity 내부에서 변경감지가 일어나 알아서 UPDATE SQL을 작성하므로, entityManager.persist()를 통해 값을 넣어주면 안된다.