본문 바로가기
JPA

영속성 컨텍스트

by 긍정의고등어 2021. 6. 7.

JPA를 통해 entity를 만들고 DB와 연동하기 전, JPA의 동작 원리가 어떻게 되는지에 대해 알고갈 필요가 있다. JPA에 대한 개념 중 가장 중요한 두 가지를 들자면, 1. 객체와 관계형 DB 매핑 2. 영속성 컨텍스트 라고 할 수 있다. 이번 정리에서는 객체와 관계형 DB를 매핑하는 방법을 배우기 전에 영속성 컨텍스트의 개념에 대해서 정리한다.

영속성 컨텍스트

해당 포스트에는 나와있지 않지만 이 전 까지의 강의에서 JPA를 사용하기 전, EntityManagerFactory를 Singleton으로 생성하고 그것으로 부터 EntityManager를 생성하여 DB의 connection pool과 연동하여 JPA를 사용한다고 배웠다. 해당 설명을 그림으로 나타내면 아래와 같다.

영속성 컨텍스트는 이 중에서 EntityManager와 연관이 있으며 그에 대한 정의는 다음과 같다.

  • JPA의 핵심이며 entity를 영구 저장하는 환경이라는 뜻을 가지고 있다.
  • 물리적으로 존재하는 것이 아닌 논리적 개념이며 EntityManager를 통해 접근한다.

Entity의 생명주기

앞서 영속성 컨텍스트는 entity를 영구 저장하는 환경이라고 했는데 entity는 영속성 컨텍스트와의 관계에 따라 4가지의 생명주기를 가질 수 있으며 각 생명주기의 천이도는 아래와 같다.

  • 비영속(new / transient)

    • 영속성 컨텍스트와 관계가 없는 전혀 새로운 상태

    • entity를 생성만 했을 때는 이 상태이다.

Member member = new Member();
member.setId("member1");
member.setName("회원1");
  • 영속(managed)

    • entity가 영속성 컨텍스트에 저장된 상태 - 아직 DB에 저장되지는 않았다!

    • entity를 생성 후 persist()를 통해 영속성 컨텍스트에 저장했을 때 영속 상태이다.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist(memger);
  • 준영속(detached)

    • entity를 영속성 컨텍스트에서 분리한 상태이다.
em.detach(member);
  • 삭제(removed)

    • entity를 아예 삭제한 상태이다.
em.remove(member);

영속성 컨텍스트의 장점

Entity를 바로 DB에 저장하지 않고 영속성 컨텍스트를 거치면 얻을 수 있는 이점은 아래와 같다.

  • 1차 캐시

    • 영속성 컨텍스트 내부에는 1차 캐시라는 저장소가 있으며 여기에 persist 등의 명령어로 저장 명령을 내린 entity를 저장한다. 이때, 각 entity는 Table에서 @Id 로 설정한 값에 의해 구분된다.

    • 1차 캐시는 하나의 트랜젝션(em) 안에서만 동작하며, 따라서 비즈니스 로직이 끝나면 사라지기 때문에 크게 성능적 이점을 볼 수는 없다. 전체적으로 공유하는 캐시는 2차 캐시라고 한다.

    • JPA를 통해 entity를 검색하게 되면 바로 DB에 조회쿼리를 날리지 않고 캐시에서 조회를 한 뒤에 캐시에 entity가 있다면 그 값을 반환하는 방식으로 동작하게 된다.

Member member = new Member(1L, "회원1");
em.persist(member);
Member findMember = em.find(Member.class, 1L); // Db가 아닌 1차 캐시에서 조회

  • 만약 이 때 1차 캐시에 해당 엔티티가 없다면 DB에서 조회를 하고 1차캐시에 저장을 한 뒤 entity 값을 넘겨준다.
  • 동일성(identity) 보장

    • JPA의 기본 개념은 자바의 콜렉션과 동일하게 동작하는 것이기 때문에 다음과 같이 조회한 두 member를 조회하면 동일한 객체임을 보장할 수 있다.
Member a = em.find(Member.class, 1L);
Member b = em.find(Member.class, 1L);
System.out.println(a == b); // true
  • 트랜잭션을 지원하는 쓰기 지연

    • JPA를 이용해서 객체를 저장(persist)하면 그 즉시 DB에 반영되지 않고 해당 트랜잭션이 끝나기 전(트랜잭션 커밋)까지는 영속성 컨텍스트에만 해당 엔티티가 저장된다.
    • 만약 아래 코드와 같이 두 멤버를 persist하고 그 아래에 프린트를 할 경우, 쿼리는 프린트 문 아래에 찍히는 것을 확인할 수 있다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // em은 데이터 변경시 트랜잭션을 시작해야 한다!

em.persist(memberA);
em.persist(memberB);
System.out.println("query test");

transaction.commit();

// query test
// insert ... member A
// insert ... member B
  • 이를 그림으로 나타내면 아래와 같다. persist 명령어를 실행하면 em의 1차 캐시에 해당 엔티티를 저장 후 쓰기 지연 SQL 저장소에 update 쿼리를 쌓아둔다.

  • 마지막에 transcation.commit() 명령어가 들어오면 그 때 쓰기 지연 저장소의 sql을 모두 Db로 flush하고 DB에서 commit을 하여 데이터를 저장한다.

  • 변경 감지(Dirty Checking)

    • 처음 JPA를 배우면서 헷갈리고 신기했던 기능인데, em을 통해 entity(DB의 값)를 변경할 때에는 값을 변경한 뒤에 또 다시 persist 명령어를 실행할 필요가 없다.
    • JPA가 자바의 콜렉션과 동일하게 동작하는 것을 지향하기 때문인데, 실제 자바 콜렉션과 같이 해당 엔티티의 속성을 수정하면 update 쿼리가 쌓이고 이후에 commit하는 시점에 DB에 반영이 된다!
    • 이 때 속성이 변경된 것을 스냅샷과 비교를 해서 변경되었다면 쓰기 지연 sql 저장소에 update쿼리를 적재하게 되는데, 스냅샷은 해당 entity를 처음 불러왔을 때의 상태를 저장해 놓은 것이다.
Member member = em.find(Member.class, 1L);
member.setId(2L);

// em.persist(member); // 이 부분 필요 없음!
transcation.commit();

  • 지연 로딩(Lazy Loading)

플러시

이제까지는 DB에 직접 데이터를 넣기 전, 영속성 컨텍스트와의 상호작용을 알아보았다. 하지만 결국에는 영속성 컨텍스트의 캐시에 있는 정보를 DB에 반영을 해야 하는데, 영속성 컨텍스트의 변경 사항을 DB에 반영하는 것을 플러시라고 한다. 플러시가 발생하면 em은 몇 가지 작업을 수행하게 되는데 이는 아래와 같다.

플러시 발생 시 em이 실행하는 작업

  • 변경 감지
  • 수정된 엔티티에 대한 sql을 쓰기 지연 저장소에 등록
  • 쓰기 지연 저장소의 sql을 DB에 전송

이러한 작업이 진행되는 플러시를 발생시키기 위해서는 여러 방법이 있으며 그 방벙들은 아래와 같다.

플러시가 발생하는 경우

  • em.flush() - 직접 플러시를 발생시키는 방법이며 잘 쓰지 않는다.
  • 트랜잭션 커밋 시 - 플러시 자동 호출
  • JPQL 쿼리 호출 시 - 플러시 자동 호출

플러시 모드 옵션

플러시에는 default인 AUTO 옵션과 JPQL 쿼리가 실행되더라도 플러시 하지 않는 COMMIT 옵션이 있는데 기본적으로 AUTO 옵션을 권장한다.

  • FlushModeType.AUTO: 커밋이나 쿼리를 실행할 때 플러시 발생
  • FlushModeType.COMMIT: 커밋을 할 때에만 플러시 발생
em.setFlushMode(FlushModeType.COMMIT);

위에서 살펴본 em.flush() 등을 사용하는 경우, 플러시가 발생하면 기존에 존재하던 영속성 컨텍스트의 모든 내용이 지워진다고 생각할 수 있다. 하지만 플러시를 하더라도 영속성 컨텍스트의 내용은 유지되며 단지 영속성 컨텍스트의 변경 내용을 DB에 반영할 뿐이다.

준영속 상태

준영속 상태는 앞의 entity 생명주기에서 알아보았듯이 영속 → 준영속 상태로 변경될 수 있으며 더 이상 em에서 entity를 관리하지 않는다. entity를 준영속 상태로 만드는 방법은 다음과 같이 3가지 방법이 있다.

준영속 상태로 변경하는 법

  • em.detach(member): entity 하나만 준영속 상태로 변경
  • em.clear(): em의 1차 캐시를 모두 비움. 테스트 시에 사용할 수 있음
  • em.close(): em 자체를 닫아버림

다음과 같이 entity를 영속 상태에서 준영속 상태로 변경하게 된다면 1차 캐시에 존재하지 않아 기존에는 1차 캐시에서 조회하던 것을 DB에서 직접 조회하게 된다.

Memeber member1 = em.find(Member.class, 1L);

em.detach(member1)

Member member2 = em.find(Member.class, 1L);

// select * from member where id = 1;
// select * from member where id = 1; -> detach를 함으로써 쿼리가 나감

'JPA' 카테고리의 다른 글

DB 스키마 자동 생성  (0) 2021.06.10

댓글