[JPA] 내부 동작 방식 (feat. 영속성 컨텍스트란?)

JPA를 사용하다보면 "영속성 컨텍스트"라는 단어를 많이 보게 된다.

하지만 대충 어떤방식인줄은 알지만, 정확히는 이해하지 못했다.

이번에 JPA에서 가장 중요하고 핵심 개념인 "영속성 컨텍스트"에 대해 다뤄보려한다.

 

 

영속성 컨텍스트

영속성 컨텍스트를 한국어로 번역하면 "엔티티를 영구 저장하는 환경"이라는 뜻이다.

EntityManager.persist(entity)

우리가 Spring Data JPA를 사용할 때는 우리가 직접 EntityManager을 생성해서 객체를 저장하지는 않는다.

하지만 영속성 컨텍스트를 이해하기 위해서는 EntityManager에 대한 이해가 필요하다.

 

EntityManager의 persist() 메서드는 객체를 저장하는 역할을 한다.

하지만 저장하는 곳이 DB가 아닌 바로, 영속성 컨텍스트에 저장을 한다.

 

이 영속성 컨텍스트는 우리가 직접 확인할 수 없다. 

영속성 컨텍스트는 EntityManager를 생성하면 1:1로 매칭되어 영속성 컨텍스트가 생성된다.

즉,EntityManager안에 영속성 컨텍스트라는 공간이 있고 EntityManager를 통해 영속성 컨텍스트에 접근할수 있다.

(이 다음 부터는 EntityManager를 줄여서 em이라고 하겠다.)

 

엔티티의 생명주기
  • 비영속 (new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태 ( JPA와 관계가 없는 상태 )
  • 영속 (managed) : 영속성 컨텍스트에 관리되는 상태 ( em.persist(entity) 로 객체를 저장한 상태 )
  • 준영속 (detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태 ( em.detach(entity) )
  • 삭제 (removed) : 삭제된 상태 ( em.remove(entity) )

( 참고 : detach()로 JPA가 관리하지 않는 준영속 상태로 만들게 되면 영속성 컨텍스트가 제공하는 기능을 사용하지 못한다. )

public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager(); // 엔티티 매니저 생성

        EntityTransaction tx = em.getTransaction(); // 트랜잭션
        tx.begin();

        try {
		// 비영속 상태 ( 객체만 생성한 상태, JPA와 관계 X )
            Member member = new Member();
            member.setId(1L);
            member.setName("HelloA");

		// 영속 상태 (영속성 컨텍스트가 member 객체를 관리)
            em.persist(member);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

언제 객체가 DB에 저장되는가 ?

위에서 persist()를 실행하면 DB에 객체를 저장하지 않고 영속성 컨텍스트에 저장한다고 하였다.

그럼 언제 DB에 객체를 저장하는 걸까?

 

위의 코드에서 다음과 같이 로그를 찍어보았다.

터미널에 찍힌 로그를 보면 트랜잭션매니저를 통해 commit()한 이후로 SQL 쿼리가 전달되어 DB에 객체가 저장된 것을 볼 수 있다. (쿼리 로그 옵션을 추가해 아래처럼 보임)

이렇게 어느 시점에서 객체가 DB에 저장하는지 확인해봤다.

그런데 왜 이렇게 복잡한 방식으로 영속성 컨텍스트라는 개념을 두고 사용하는 걸까?


영속성 컨텍스트의 이점

1차 캐시

영속성 컨텍스트 내부에는 캐시가 있는데 이를 1차 캐시라고 한다. (우리가 알고 있는 그 "캐시"이다.)

 

em.persist(member)로 Member 엔티티를 영속성 컨텍스트 내부의 1차 캐시에 저장을 한다.

그런데 Member 엔티티를 persist를 통해 저장하지 않고 DB에서 직접 쿼리를 날려 member와 다른 속성을 갖는 member2를 생성했다고 해보자.

 

그럼 현재 member2는 영속성 컨텍스트에는 없고, DB에는 저장되어 있는 상태이다.

이때 em.find()를 통해 member2의 값을 조회하면 어떤일이 일어날까? 

// em.find(엔티티 클래스 타입, 식별자 값);
Member member = em.find(Member.class, "member2");
  1. 영속성 컨텍스트의 1차 캐시에서 member2가 있는지 찾는다.
  2. 없으면 DB를 조회해서 member2를 찾고 영속성 컨텍스트의 1차 캐시에 저장한다. (물론 DB에 member2가 있어야 한다.)
  3. member2를 반환한다.

이후에 다시 member2를 조회하게 되면, DB를 거치지 않고 1차 캐시에서 member2를 조회해 반환한다. 

 

영속 엔티티의 동일성 보장
Member findMember1 = em.find(Member.class, 1L);
Member findMember2 = em.find(Member.class, 1L);

System.out.printf("결과 = " + (findMember1 == findMember2)); // true

위 코드를 보면 똑같은 객체를 꺼내 두 객체를 비교하였다.

실제 인스턴스가 같은지 == 비교를 통한 결과가 true이므로 엔티티의 동일성을 보장한다고 할 수 있다.

 

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

영속성 컨텍스트안에는 1차 캐시 뿐 아니라 "쓰기 지연 SQL 저장소"라는 것이 존재한다.

 

 

em.persist(member1)를 실행하면 member1이 1차 캐시에 저장되고, member1를 분석해 INSERT 쿼리를 생성하고 쓰기 지연 SQL 저장소에 저장한다.

그 다음 em.persist(member2)를 실행하면 똑같이 1차 캐시에 저장되고, member2를 분석해 INSERT 쿼리를 생성하고 쓰기 지연 SQL 저장소에 저장한다.

지금 까지 member1과 member2는 아직 DB에 저장되지 않은 상태이다.

 

이후 commit()이 호출되면 쓰기 지연 SQL 저장소에 모아져있던 쿼리가 DB에 전달되고 실제 DB가 커밋된다.

즉, 트랜잭션을 commit하기 전까지 영속성 컨텍스트 쓰기 지연 SQL 저장소에 SQL 쿼리를 모아 두었다가 커밋이 실행되면 모아둔 SQL 쿼리를 DB에 전달한다.

 

더티 체킹 ( 변경 감지 )

Spring Data JPA를 사용하면 save() 메서드는 있지만 update() 메서드는 없다.

또한 데이터를 변경할 때 save 하지 않아도 값이 저장되는 것을 볼 수 있다.

이게 가능한 이유는 바로 JPA의 더티 체킹 때문이다.

( 위 코드를 보면 em.persist()를 하지 않은 것을 볼 수 있다. )

JPA는 트랜잭션이 커밋되는 시점에 flush가 호출되면서 엔티티와 이 스냅샷을 비교한다.

비교했을 때 변경을 감지하면 쓰기 지연 SQL 저장소UPDATA 쿼리를 저장해놓고 DB에 전달하여 반영한다.

 

참고로, 더티 체킹의 대상은 영속성 컨텍스트가 관리하는 엔티티에만 적용된다.

준영속, 비영속상태의 엔티티는 더티체킹의 대상이 아니다.

 

 

엔티티 삭제

삭제는 엔티티를 찾아와 그 엔티티를 remove하여 삭제할 수 있다.

이 방식도 위 처럼 쓰기 지연 SQL 저장소에 DELETE 쿼리가 저장되었다가 commit 시점에 쿼리가 DB에 전달된다.

//삭제 대상 엔티티 조회 
Member memberA = em.find(Member.class, 1L);
em.remove(memberA); //엔티티 삭제

플러시

플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.

플러시를 한다고 해서 영속성 컨텍스트의 내용을 지우고 DB에 보내는 것이 아니다.

즉, 영속성 컨텍스트의 변경 내용을 데이터베이스 동기화하는 것이라 할 수 있다.

 

플러시 방법
  • em.flush() 직접 호출 - 테스트 용도 외에 직접 적으로 사용하는 일은 거의 없다.
  • 트랜잭션 커밋 시 플러시 자동 호출
  • JPQL 쿼리 실행 시 플러시 자동 호출

 

플러시 발생 시
  1. 더티 체킹 (변경 감지)
  2. 수정된 엔티티를 쓰기 지연 SQL 저장소에 등록
  3. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송