1. 영속성 컨텍스트


영속성 컨텍스트는 객체와 관계형 데이터베이스 사이에 위치하는 공간, 즉 중간 계층입니다.
영속성 컨텍스트라는 중간 계층을 둠으로써 얻는 이점은 다음과 같습니다.

  • 1차 캐시
  • 조회한 entity 객체의 동일성(identity) 보장
  • 트랜잭션을 지원하는 쓰기 지연
  • 변경 감지(Dirty Checking)
  • 지연 로딩(Lazy Loading)

 

위 5가지 이점을 차례로 살펴보겠습니다. 우선 1차 캐시부터 살펴보겠습니다.

1.1 1차 캐시


다음과 같이 entity 객체를 생성하고 .persist() 메서드를 이용해 entity 객체를 영속시킨다고 해보겠습니다.

Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

entityManager.persist(member); // entity 객체를 영속시킵니다.

.persist() 메서드로 entity 객체를 영속시키면 아래와 같이 Id와 entity 객체가 key-value 형태로 저장됩니다.

[그림 1] 영속성 컨텍스트의 1차 캐시

이 상태에서 EntityManager의 .find() 메서드를 이용해 조회하면 우선적으로 1차 캐시를 살펴보기에 DB까지 가지 않아도 됩니다. 👏

// "member1" 을 Id로 가지는 값이 영속성 컨텍스트의 1차 캐시에 존재하기 때문에
// 따로 DB에 쿼리를 날리지 않고 바로 조회해옵니다.
Member findMember = entityManager.find(Member.class, "member1");

만약 DB에는 저장되어 있지만, 1차 캐시에는 없다면 DB에서 조회해와서 1차 캐시에 저장합니다.
1차 캐시는 한 트랜잭션 내에서만 공유되었다 사라지는 영역이기 때문에 실질적으로 성능에 큰 기여를 하지는 않습니다.
애플리케이션 전체 영역에서 공유하는 캐시 공간은 2차 캐시입니다.

1.2 entity 객체의 동일성(identity) 보장


두 번째는 조회한 entity 객체의 동일성(identity) 보장입니다.
위에서 살펴본 1차 캐시로 인해 다음의 코드가 성립합니다.

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

System.out.println(a == b); // 동일성 비교로 true가 출력됩니다.

당연하다고 생각될 수 있지만, JPA가 아닌 네이티브 JDBC나 MyBatis를 쓰면 DB에서 쿼리를 날려 매번 새로 조회해오기 때문에 동일성 비교를 하면 false가 나옵니다. (당연한걸 당연하게 만들어주는 JPA...🙂 )

 

그리고 이 특징으로 인해 트랜잭션 Isolation Level이 READ COMMITED여도 REPEATABLE READ를 보장받을 수 있습니다.
즉, 애플리케이션 차원에서 REPEATABLE READ를 제공하는 것입니다.

1.3 트랜잭션을 지원하는 쓰기 지연


세 번째는 트랜잭션을 지원하는 쓰기 지연입니다.

EntityManager의 .persist() 메서드를 이용해 entity 객체를 영속성 컨텍스트의 1차 캐시에 영속시키고, .commit() 메서드가 실행될때 DB에 쿼리를 날립니다. 

Member memberA = new Member("memA", "홍길동");
Member memberB = new Member("memB", "김길동");

entityManager(memberA); // 영속성 컨텍스트 내에만 저장됩니다.
entityManager(memberB); // 영속성 컨텍스트 내에만 저장됩니다.

entityTransaction.commit(); // 커밋하는 순간 모아뒀던 SQL을 한번에 DB로 전송합니다.

[그림 2] 영속성 컨텍스트 안에는 SQL 쿼리를 모아두는 공간이 있다. .commit() 시 모아뒀던 SQL을 한번에 전송한다.

.persist() 메서드로 영속성 컨텍스트 내에 데이터를 저장해뒀다가, .commit() 메서드가 실행되면 모아뒀던 SQL을 한번에 전송합니다.

1.4 변경 감지(Dirty Checking)


변경 감지는 JPA에서 entity 객체의 업데이트를 처리하는 방법에 관한 얘기입니다.
아래는 데이터를 수정하는 코드입니다. 그런데, 1차 캐시에 담긴 객체 중 변경된 데이터를 어떻게 알고 변경된 객체에 한해서만 UPDATE 쿼리를 DB로 날리는걸까요?

EntityTransaction tx = entityManager.getTransaction();
tx.begin()

// id=3 인 객체를 조회
Member member = entityManager.find(Member.class, 2L); // {2L, "홍길동"}
member.setName("홍길동 -> 김길동");

tx.commit();

[그림 3] 내부에서 entity 객체의 업데이트를 처리하는 방법

위 그림의 과정은 다음과 같습니다.

  1. 최초 entityManager.find() 메서드로 객체를 조회하면 DB에서 SELECT문으로 가져와 1차 캐시에 저장한다.
    이때, 스냅샷까지 두 개를 저장해둔다.
  2. 내부적으로 .flush() 가 호출되면 1차 캐시에 담긴 모든 객체들을 for 문으로 스냅샷과 Entity 객체의 값이 같은지 일일이 비교한다.
    값이 다른게 있다면 UPDATE문을 생성해서 실행한다.
  3. tx.commit() 이 실행되면 DB에서도 commit을 실행한다. (RDB에서 commit을 해야 비로소 연산이 적용됩니다.)

 

flush

위 그림을 보면 .flush() 라는 메서드가 나오는데, flush가 무엇일까요?
flush는 영속성 컨텍스트의 변경 내용에 대한 쿼리를 DB로 날리는 작업을 말합니다.
JPA는 flush가 실행될 경우 내부적으로 다음의 작업을 수행합니다. (위 그림과 비교하여 살펴보세요.)

  1. 1차 캐시에서 현재 Entity 객체의 값과 이전의 스냅샷을 비교하여 변경된 객체가 있는지 찾습니다.
  2. 변경된 객체가 있다면 UPDATE SQL을 만들어 쓰기 지연 SQL 저장소에 저장합니다.
  3. 쓰기 지연 SQL 저장소에 있는 모든 쿼리를 DB에 전송합니다. 

 

영속성 컨텍스트를 flush하는 방법은 3가지입니다.

  • entityManager.flush() 호출 (직접 호출하는 방법)
  • entityTransaction.commit() 호출 (자동으로 flush가 호출)
  • JPQL 쿼리 실행 (자동으로 flush가 호출)
Member member = entityManager.find(Member.class, 3L); // {3L, "홍길동"}
member.setName("홍길동 -> 김길동"); // 영속성 컨텍스트 내에만 변경이 반영된 상태

entityManager.flush(); // 변경 감지 및 쓰기 지연 SQL 저장소의 쿼리를 DB로 전송합니다.

member.setName("이길동");
entityTransaction.commit(); // flush가 자동호출되고, DB에 커밋까지 전송합니다.

Member mem1 = new Member(4L, "유재석");
entityManager.persist(mem1);

// 위 mem1은 영속성 컨텍스트에만 저장해놓고, 아직 DB에 저장하지는 않았습니다.
// 그런데, 이 시점에서 아래와 같이 JPQL을 실행하면 DB에서 위 데이터도 같이 조회가 되어야합니다.
// 이런 이유로 JPQL을 실행하면 flush를 자동으로 실행합니다.
Member findMember = entityManager.query("SELECT m FROM Member as m");