[JPA] 준영속

이번 포스팅에서는 영속 → 준영속의 상태 변화를 알아보자.

📂 준영속

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

따라서, 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다

 

영속 상태의 엔티티를 준영속 상태로 만드는 방법엔 크게 3가지가 있다.

  1. em.detach(entity) : 특정 엔티티만 준영속 상태로 전환
  2. em.clear() : 영속성 컨텍스트를 완전히 초기화
  3. em.close() : 영속성 컨텍스트를 종료

순서대로 한 번 알아보자. 

1️⃣ 엔티티를 준영속 상태로 전환 : detach()

em.detach() 메소드는 특정 엔티티를 준영속 상태로 만든다. 

- detach() 테스트 코드

public void testDetached() {
    ...
    // 회원 엔티티 생성, 비영속 상태
    Member member = new Member();
    member.setId("memberA");
    member.setUsername("회원A");
    
    // 회원 엔티티 영속 상태
    em.persist(member);
    
    // 회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
    em.detach(member);
    
    transaction.commit(); // 트랜잭션 커밋
}

먼저 회원 엔티티를 생성하고 영속성화한 다음 detach()를 호출했다. 이 메소드를 호출하는 순간 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 재거된다. 그림으로 살펴보면 다음과 같다.

detach 실행 전
detach 실행 후

위의 두 번째 그림에서 보는 것처럼 영속성 컨텍스트에서 memberA에 대한 모든 정보를 삭제했다.

이렇게 영속 상태였다가 더는 영속성 컨텍스트가 관리하지 않는 상태준영속 상태라 한다. 이미 준영속 상태이므로 영속성 컨텍스트가 지원하는 어떤 기능도 동작하지 않는다. 심지어 쓰기 지연 SQL 저장소의 INSERT SQL도 제거되어 데이터베이스에 저장되지도 않는다. 

정리하면, 준영속 상태는 영속성 컨텍스트로부터 분리된 상태이다. 

2️⃣ 영속성 컨텍스트 초기화 : clear()

em.clear() 메소드는 영속성 컨텍스트를 초기화해서 해당 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만든다. 

- 영속성 컨텍스트 초기화

// 엔티티 조회, 영속 상태
Member member = em.find(Member.class, "memberA");

// 영속성 컨텍스트 초기화
em.clear();

// 준영속 상태
member.setUsername("changedName");

초기화 전
초기화 후

위의 두 그림을 비교해보면, 영속성 컨텍스트에 있는 모든 것이 초기화되어 버렸다. 영속성 컨텍스트를 제거하고 새로 만든 것과 같다. 이제 memberA, memberB는 영속성 컨텍스트가 관리하지 않으므로 준영속 상태이다. 

member.setUsername("changedName");

그리고 준영속 상태에서는 영속성 컨텍스트가 지원하는 변경 감지도 동작하지 않는다. 따라서 위의 코드로 회원의 이름을 변경해도 데이터베이스에 반영되지 않는다. 

3️⃣ 영속성 컨텍스트 종료 : close()

영속성 컨텍스트를 종료하면 해당 영속성 컨텍스트가 관리하던 영속 상태의 엔티티가 모두 준영속 상태가 된다. 

- 영속성 컨텍스트 닫기

public void closeEntityManager() {
    EntityManagerFactory emf =
        Persistence.createEntityManagerFactory("jpa_test");
    
    EntityManager em = emf.createEntityManager();
    EntityTransaction transaction = em.getTransaction();
    
    transaction.begin(); // [트랜잭션] - 시작
    
    Member memberA = em.find(Member.class, "memberA");
    Member memberB = em.find(Member.class, "memberB");
    
    transaction.commit(); // [트랜잭션] - 커밋
    
    em.close(); // 영속성 컨텍스트 닫기(종료)
}

영속성 컨텍스트 제거 전
영속성 컨텍스트 제거 후

영속성 컨텍스트가 종료되어 더는 memberA, memberB가 관리되지 않는다. 

영속 상태의 엔티티는 주로 영속성 켄텍스트가 종료되면서 준영속 상태가 된다. 개발자가 직접 준영속 상태로 만드는 일은 드물다.

 

📂 준영속 상태의 특징

그럼 준영속 상태인 회원 엔티티는 어떻게 될까?

거의 비영속 상태에 가깝다

영속성 컨텍스트가 관리하지 않아 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩 등을 포함한 지원하는 어떤 기능도 동작하지 않는다. 

식별자 값을 가지고 있다.

비영속 상태는 식별자 값이 없을 수도 있지만 준영속 상태는 이미 한 번 영속 상태였기 때문에 반드시 식별자 값을 가지고 있다.

지연 로딩을 할 수 없다.

지연 로딩은, 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법이다. 하지만 준영속 상태는 영속성 컨텍스트가 관리하지 않기 때문에 지연 로딩 시 문제가 발생한다. 이에 대한 자세한 내용은 뒤에서 알아보자. 


📂 병합 : merge()

준영속 상태의 엔티티를 다시 영속 상태로 변경하기 위해서는 병합을 사용하면 된다. 

merge() 메소드는 준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 반환한다. 

- merge() 사용 예시

Member mergeMember = em.merge(member);

준영속 병합

준영속 상태의 엔티티를 영속 상태로 변경해보자.

- 준영속 병합 예제

public class ExamMergeMain {
    
    static EntityManagerFactory emf =
        Persistence.createEntityManagerFactory("jpa_test");
    
    public static void main(String args[]) {
        Member member = createMember("memberA", "회원1"); - (1)
        
        member.setUsername("회원명 변경"); - (2)
        
        mergeMember(member); - (3)
    }
    
    static Member createMember(String id, String username) {
        //==영속성 컨텍스트1 시작==//
        EntityManager em1 = emf.createEntityManager();
        EntityTransaction tx1 = em1.getTransaction();
        tx1.begin();
        
        Member member = new Member();
        member.setId(id);
        member.setUsername(username);
        
        em1.persist(member);
        tx1.commit();
        
        em1.close(); // 영속성 컨텍스트 종료,
                     // member 엔티티는 준영속 상태가 된다. 
        //==영속성 컨텍스트1 종료==//
        
        return member;
    }
    
    static void mergeMember(Member member) {
        //==영속성 컨텍스트2 시작==//
        EntityManager em2 = emf.createEntityManager();
        EntityTransaction tx2 = em2.getTransaction();
        
        tx2.begin();
        Member mergeMember = em2.merge(member);
        tx2.commit();
        
        // 준영속 상태
        System.out.println("member = " + member.getUsername());
        
        // 영속 상태
        System.out.println("mergeMember = " + mergeMember.getUsername());
        
        System.out.println("em2 contains member = " +
            em2.contains(member));
        System.out.println("em2 contains mergeMember = " +
            em2.contains(mergeMember));
        
        em2.close();
        //==영속성 컨텍스트2 종료==//
    }
}

위 코드의 출력 결과는 다음과 같다.

member = 회원명변경
mergeMember = 회원명변경
em2 contains member = false
em2 contains mergeMember = true

위 코드의 main 함수 부분을 한 번 살펴보자.

1

member 엔티티는 createMember() 메소드의 영속성 컨텍스트1에 영속 상태였다가 종료되면서 준영속 상태가 되었다. 따라서 createMember() 메소드는 준영속 상태의 member 엔티티를 반환한다. 

2

member.setUsername() 메소드로 회원 이름을 변경했지만, 준영속 상태인 member 엔티티를 관리하는 영속성 컨텍스트가 존재하지 않기 때문에 수정 사항을 데이터베이스에 반영할 수 없다

3

준영속 상태의 엔티티를 수정하기 위해서는 다시 영속 상태로 변경해야 한다. 이때 merge()(병합)을 사용한다. 코드에서 준영속 상태의 member 엔티티를 영속성 컨텍스트2가 관리하는 영속 상태로 변경했다. 영속 상태이므로 트랜잭션을 커밋할 때 수정했던 회원명이 데이터베이스에 반영된다(정확하게는 mergeMember라는 새로운 영속 상태의 엔티티가 반환).


참고

자바 ORM 표준 JPA 프로그래밍(김영한 저)