영속성 컨텍스트(Persistence Context)와 영속성 관리

 

들어가며

 

영속성 컨텍스트란? 엔티티(DB테이블의 관계를 자바로 표현한 클래스)를 영구 저장하는 환경을 뜻한다.
즉, 비즈니스로직에서 어떠한 엔티티 객체의 상태를 DB 테이블에 바로 반영하지 않고 영속성 컨텍스트에 우선적으로 엔티티 상태가 저장되며 1차 캐시, 지연로딩 등의 기능 수행하여 성능을 높여주는 녀석이라고 생각하면 된다.

 

영속성 컨텍스트가 관리할 수 있는 엔티티에는 4가지 상태가 존재한다.

 

  • 비영속(new/transient) : 엔티티가 영속성 컨텍스트와 전혀 관계가 없는 상태
  • 영속(managed) : 엔티티가 영속성 컨텍스트에 저장된 상태
  • 준영속(detached) : 엔티티가 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(removed) : 엔티티가 영속성 컨텍스트에서 삭제된 상태

 

 

 

비영속

 

Member 엔티티 객체를 생성하더라도 단순히 순수한 객체 상태이며

영속성 컨텍스트나 데이터베이스와는 전혀 관련 없는 상태를 뜻한다.

 

 

 

 

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

 

 

영속

 

영속성 컨텍스트가 관리하는 엔티티를 영속 상태라 한다.

 

 

 

 

em.persist(member);

 

 

준영속

 

영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리 하지 않으면 준영속 상태라 한다.

 

em.detach(member);

 

 

삭제

 

엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한 상태를 뜻한다.

 

em.remove(member);

 

영속성 컨텍스트 어떤 구조이길래 엔티티의 상태를 결정하고 관리를 할까?

영속성 컨텍스트의 구조, 특징과 장점들을 살펴보겠다.

 

 

 

영속성 컨텍스트와 식별자 값

 

영속성 컨텍스트에 저장되는 엔티티들은 식별자 값으로 구분된다. 즉, 영속 상태의 엔티티는 식별자 값이 반드시 존재해야 한다.

(존재하지 않으면 예외가 발생)

 

 

 

 

 

 

1차 캐시에서 조회

 

영속성 컨텍스트 내부에 캐시를 가지고 있는데 이를 1차 캐시라고 한다.

 

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

em.persist(member);

 

위의 코드를 수행하면

아래와 같이 1차캐시에 member 엔티티를 저장한다.(아직 데이터베이스에는 저장되지 않았다.)

 

 

 

 

이후 아래와 같은 코드를 수행하면

 

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

 

1차 캐시에서 식별자에 해당하는 엔티티를 찾고 만약 찾으려는 엔티티가 1차 캐시에 존재하지 않는다면 데이터베이스에서 조회한다.

데이터베이스에서 조회 후 1차 캐시에 저장한 후에 영속 상태의 member 엔티티를 반환한다.

 

 

 

동일성 보장

 

영속성 컨텍스트에 저장된 엔티티 인스턴스를 조회시 항상 같은 인스턴스를 반환한다.

추가로 반환된 인스턴스의 데이터 값을 수정하면 영속성 컨텍스트에 저장된 엔티티의 데이터 값도 수정된다.

이유는 같은 메모리주소를 가리키는 인스턴스이기 때문이다.

 

 

 

 

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

 

 

쓰기지연

 

영속성 컨텍스트에는 1차 캐시 뿐만 아니라 쓰기 지연 SQL 저장소가 존재한다. 엔티티 매니저는 트랜잭션을 커밋하기 전까지는 엔티티를 데이터베이스에 동기화하지 않고 엔티티를 1차캐시에 저장하고 엔티티를 데이터베이스에 동기화할 수 있는 SQL를 쓰기 지연 SQL 저장소에는 저장한다.

 

쓰기 지연을 잘 활용하면 쓰기 지연 SQL 저장소에 모아둔 쿼리를 데이터베이스에 한 번에 전달해서 성능을 최적화할 수 있는 장점이 있다.

 

 

 

변경감지

 

엔티티를 수정하고 데이터베이스에 반영할 때는 JPA에서는 엔티티를 조회해서 데이터만 변경해주고 트랜잭션을 커밋 해주면 된다.

이를 변경 감지라고 한다.

 

 

 

 

JPA는 엔티티를 영속성 컨텍스트내부의 1차 캐시에 저장할 때, 최초 상태를 복사해서 저장하는데 이를 스냅샷이라고 한다.

엔티티 객체의 데이터값 수정 후 트랜잭션 커밋시 엔티티 매니저 내부에서 플러시가 호출되며 1차캐시에 존재하는 스냅샷과 엔티티를 비교해서 스냅샷기준으로 변경된 엔티티가 있는 찾는다.

 

변경된 엔티티가 있다면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸 후 다시 쓰기 지연 저장소의 저장된 SQL을 데이터베이스로 보낸다. 이후 데이터베이스 트랜잭션이 커밋되면 엔티티의 변경내용이 데이터베이스에 반영된다.

 

단, 변경 감지는 영속 상태의 엔티티에만 적용된다.

 

추가로 영속성 컨텍스트에 엔티티를 제거하려면 어떻게해야 할까?

 

Member memberA = em.find(Member.class, "memberA");
em.remove(memberA);

 

remove 메서드를 호출하면 영속성 컨텍스트에 엔티티가 제거되며, 쓰기 지연 SQL 저장소에 삭제 쿼리가 저장된다. 그러나 데이터 베이스에도 반영하기 위해서는 트랜잭션 커밋을 해서 플러시가 호출되게해야한다. 

(※ 삭제된 엔티티는 재사용하지 말자.)

 

 

 

플러시

 

앞에서 등장했던 플러시(flush)는 영속성 컨텍스트의 변경내용을 데이터베이스에 반영한다고 설명했다.

(영속성 컨텍스트의 엔티티를 데이터베이스에 동기화)

플러시가 호출되는 시점은 다음과 같다.

 

1. em.flush() 직접 호출.

 

2. 트랜잭션 커밋 전에 플러시가 호출됨.

 

트랜잭션 커밋시 플러시가 호출되지 않는다면 영속성 컨텍스트의 엔티티들의 변경내용 즉, 변경된 내용(SQL)이 데이터베이스에 반영되어있지 않을 수도 있기 때문에 기대와는 다른 결과를 응답 받을 수도 있다. JPA는 이를 예방하기 위해 트랜잭션 커밋 전에 플러시를 먼저 호출 하도록 한다.

 

3. JPQL 쿼리 실행 전 플러시가 호출됨.

 

JPQL쿼리를 실행하면 SQL로 변환되어 데이터베이스에 전달되어 쿼리가 수행된다. 그러나 영속성 컨텍스트의 엔티티들의 변경 내용이 데이터베이스에 반영되어 있지 않을 수도 있기 때문에 마찬가지로 JPA는 이를 예방하기 위해 JPQL 쿼리 실행 전 플러시를 먼저 호출하도록 한다.

 

※ 플러시 모드를 직접 지정할 수 도있다.

javax.persistence.FlushModeType.AUTO 커밋이나 쿼리를 실행할 때 플러시(default)

javax.persistence.FlushModeType.COMMIT 커밋할 때만 플러시

EX) em.setFlushMode(FlushModeType.COMMIT)

 

이제 영속, 비영속, 삭제 상태는 어느 정도 알겠다. 그러나 초반에 소개되었던 영속성 컨텍스트가 관리하지 않는 상태인 준영속 상태는 정확히 무엇인지 아직 감이 오지 않는다. 준영속 상태에 대해 자세히 알아보자.

 

 

 

준영속

 

엔티티가 영속성 컨텍스트에서 분리된것을 준영속 상태라 한다.

영속 상태에서 준영속 상태로 전환되는 방법은 크게 3가지다.

 

1. em.detach(entity) : 특정 엔티티만 준영속 상태로 전환한다.

 

//step 1
Member member = new Member();
member.setId("member1");
em.persist(member);

//step 2
em.detach(member);

 

위의 코드가 Step 1 까지 수행되면 다음과 같은 영속성 컨텍스트 내용을 가질 것이다.

 

 

 

 

Step 2 까지 수행되어 준영속 상태로 전환되면 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거된다. 다음과 같이 말이다.

 

 

 

 

※ 쓰기 지연 SQL 저장소에도 member1 엔티티의 INSERT 정보가 제거 됐으므로 트랜잭션을 커밋하더라도 데이터베이스에 저장되지 않는다.

 

2. em.clear() : 영속성 컨텍스트를 완전히 초기화 한다.

 

em.detach(entity) 가 특정 엔티티 하나를 준영속 상태로 전환한다면 em.clear()는 해당 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만든다.

 

 

3. em.close() : 영속성 컨텍스트를 종료한다.

 

영속성 컨텍스트를 종료시키므로 em.clear()와 마찬가지로 해당 영속성 컨텍스트의 모든 엔티티는 관리되지 않으므로 준영속 상태로 전환된다.

 

※ 준영속 상태의 엔티티는 영속성 컨텍스트가 더 이상 관리하지 않으므로 1차 캐시, 쓰기 지연같은 기능도 동작하지 않으므로 거의 비영속 상태에 가깝지만 한 번 영속했던 상태였기 때문에 식별자 값을 가지고있다. 그리고 지연로딩(실체 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법)을 할 수 없다.

 


병합: merge()

 

준영속 상태의 엔티티를 영속 상태로 전환하고 싶다면 병합을 사용하면 된다. 단, 준영속에만 국한된것이 아니다.

이유는 다음과 같다.

 

em.merge(entity) 메서드는 엔티티를 받아서 엔티티의 식별자 값으로 영속성 컨텍스트를 조회한다. 찾았다면 파라미터로 넘어온 엔티티의 와 병합하고 이를 반환한다. 만약 조회하고자 한 엔티티가 없으면 데이터베이스에서 조회한다. 데이터베이스에도 찾지 못하면 해당 엔티티를 생성해서 병합시킨 후 이를 반환한다.

 

즉, 병합은 save or update 기능을 수행하며 준영속, 비영속을 신경 쓰지 않는다고 볼 수 있다.

 

 

참고자료


자바 ORM 표준 JPA (김영한)