03. 영속성 관리

zwundzwzig·2023년 2월 5일
0
post-thumbnail

매핑된 엔티티는 EntityManager를 통해 어떻게 사용될까? EntityManager를 자세히 알아보자.

엔티티 매니저 팩토리와 엔티티 매니저

DB를 하나만 사용하는 프로젝트는 일반적으로 팩토리를 하나만 생성한다. 그리고 필요할 때마다 매니저를 생성한다.

팩토리는 여러 스레드가 동시에 접근해도 안전해 서로 다른 스레드 간 공유가 가능하지만, 엔티티 매니저는 동시성 문제로 인해 절대 공유하면 안된다. (살짝 프로세스-스레드 같다.)

그림 속 1번 매니저는 커넥션을 사용하지 않다가 트랜잭션처럼 필요할 때 커넥션을 얻을 것이다.

하이버네이트를 포함한 JPA 구현체들은 팩토리를 생성할 때 커넥션풀도 만드는데, J2SE(스프링 프레임워크 환경 포함) 환경에서 사용된다.

그럼 해당 컨테이너가 제공하는 데이터소스를 사용한다. 이는 11장에서 자세히 다루신단다.

영속성 컨텍스트란?

엔티티를 영구 저장하는 환경이라는 뜻으로, 엔티티 매니저로 저장 및 조회하면 영속성 컨텍스트에 엔티티를 보관하고 관리한다.

영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어진다.

엔티티 매니저를 통해 영속성 컨텍스트에 접근 가능하고 영속성 컨텍스트를 관리할 수 있다.

여러 엔티티 매니저가 같은 영속성 컨텍스트에 접근할 수도 있지만, 이렇게 복잡한 상황 역시 11장에서 자세히 다루신단다. 11장까지 영속성 있게 공부하자.

엔티티의 생명주기

엔티티엔 네 가지 상태가 존재한다.

비영속 new/transient

엔티티 객체를 생성했으나 영속성 컨텍스트나 DB와는 전혀 관련이 없는 상태.

영속 managed

엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장하고 영속성 컨텍스트가 관리하는 엔티티.

em.find()나 JPQL을 사용해 조회한 엔티티도 영속성 컨텍스트 관리 대상이다.

준영속 detached

영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않는 상태. 영속성 컨텍스트에서 분리.

detach() 메소드를 호출하거나 close, clear 등으로 닫거나 초기화해도 준영속 상태가 된다.

삭제 removed

remove 메소드로 삭제.

영속성 컨텍스트의 특징

엔티티 조회

1차 캐시에서 조회

영속성 컨텍스트는 내부에 캐시를 갖는데 이를 1차 캐시라 하고, 영속 상태의 엔티티는 이곳에 저장된다.

member.setId("member1"); member.setName("멤버1"); em.persist(member);
이때 1차 캐시의 키는 식별자 값이고, 값은 엔티티 인스턴스이다.

em.find(Member.class, "member1");
1차 캐시로 조회하려면 엔티티 클래스 타입과 엔티티의 식별자 값을 인자로 넣으면 된다.

DB에서 조회

만약 1차 캐시로 조회할 수 없으면 엔티티 매니저는 DB를 조회해 엔티티를 생성한다. 그리고 1차 캐시에 저장한 후 영속 상태의 엔티티를 반환한다.

둘 중 어떻게 조회를 하든, 조회된 인스턴스는 성능상 이점과 함께 엔티티의 동일성을 보장한다.

엔티티 등록

EntityManager em = emf.createEntityManager();
EntityTransaction et = em.getTransaction();
// 엔티티 매니저는 데이터 변경 시 트랜젝션을 시작해야 한다.
transaction.begin(); 

em.persist(memberA);
em.persist(memberB);
// 여기까진 DB에 쿼리를 보내지 않는다.

transaction.commit(); // 커밋하는 순간 INSERT SQL을 보낸다.

이렇게 엔티티 매니저는 엔티티를 내부 쿼리 저장소에 모아두다가 트랜젝션을 커밋할 때 DB에 보낸다. 이를 쓰기 지연 transactional write-behind이라 한다.

트랜젝션은 커밋하면 엔티티 매니저는 우선 영속성 컨텍스트를 플러시한다. 플러시는 영속성 컨텍스트의 변경 내용을 DB에 동기화하는 작업인데, 이때 내부 저장소에 쿼리를 DB에 보낸다.

이는 성능 최적화와 관련이 있다.

엔티티 수정

transaction.begin(); 

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

// 수정
memberA.setName("gd");
memberA.setAge("36");

// em.update(memberA)와 같은 코드는 필요없다!
transaction.commit(); // 커밋하는 순간 INSERT SQL을 보낸다.

update 같은 메서드 없이 어떻게 수정이 가능할까? 변경 감지 dirty checking 때문에 가능하다.

엔티티를 영속성 컨텍스트에 보관할 때 JPA는 최초 상태를 복사해 저장하고, 이를 스냅샷이라 한다. 이후 플러시 시점에 스냅샷과 엔티티를 비교해 변경된 부분을 찾는다.

변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다. 준영속도 안된다!

JPA는 변경된 필드 뿐만 아니라 변경되지 않은 기존 필드도 함께 업테이트해 전송량이 증가하는 단점이 있지만 엔티티나 DB 입장에서 재사용이 가능하다.

필드나 저장될 내용이 너무 많으면 데이터만 사용해 동적으로 UPDATE SQL을 생성하는 전략을 선택하면 된다. DynamicUpdate 어노테이션을 활용하자.

엔티티 삭제

find로 찾고 remove로 없애자.

이때 삭제된 엔티티는 재사용하지 말고 자연스럽게 가비지 컬렉션의 대상이 되게 냅두자.

flush

플러시를 실행하면 다음과 같은 일이 일어난다.

  • 변경 감지가 동작해 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해 수정된 엔티티를 찾는다.
  • 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다.
  • 쓰기 지연 SQL 저장소으 쿼리를 DB에 전송한다.

플러시를 호출하는 방법은 세 가지이다.

  • 엔티티 매니저의 flush() 메소드를 직접 호출해 영속성 컨텍스트를 강제로 플러시한다. 테스트나 다른 프레임워크와 JPA를 함께 사용할 때만 쓰고 거의 이럴 일 없다.
  • 트랜젝션 커밋 전 JPA가 알아서 자동으로 호출해준다.
  • 객체지향 쿼리(JPQL, Criteria)를 호출해도 JPA가 알아서 플러시를 호출해 준다.

엔티티 매니저에 플러시 모드를 직접 지정하려면 FlushModeType을 사용하면 된다.

지금까지 엔티티의 비영속 ➡️ 영속 ➡️ 삭제 상태 변화를 알아봤다.

준영속

준영속 상태의 엔티티는 영속 컨텍스트가 제공하는 기능을 사용할 수 없다. 영속 상태에서 준영속 상태로 변화하는 과정에 대해 알아보자.

em.detach(entity)

특정 엔티티만 준영속 상태로 만들 때 사용한다.

이 메소드를 호출하는 순간 1차 캐시부터 쓰기 지연 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 영속 컨텍스트에서 제거된다.

이렇게 영속 컨텍스트에서 분리된다고 이해하자.

em.clear()

영속성 컨텍스트를 아예 초기화해서 안에 있던 모든 엔티티를 준영속 상태로 만든다.

영속 컨텍스트를 제거하고 새로 만든 것과 같다,

em.close()

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

준영속 상태의 특징

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

  • 거의 비영속 상태에 가깝다.
    1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩 등 영속 컨텍스트가 제공하는 기능도 동작하지 않기 때문이다.
  • 비영속 상태와의 차이라면, 그래도 영속 상태 출신이기 때문에 식별자 값은 갖고 있다.

영속 상태로 다시 되돌리려면? 병합을 사용하자 : merge()

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

public class ExamMergeMain {
	static EntityManagerFactory emf = 
    	Persistence.createEntityManagerFactory("jpabook");
    
    public static void main(String[] args) {
    	Member member = createMember("memberA", "회원1");
        member.setName("update name"); // 준영속상태에서 변경
        mergeMember(member);
    }
    
    //영속 컨텍스트 1 시작
    static Member createMember(String id, String name) {
    	EntityManager em1 = emf.createEntityManager();
        EntityTransaction tx1 = em1.getTransaction();
        tx1.begin();
        
        Member member = new Member();
        member.setId(id);
        member.setName(name);
        
        em1.persist(member);
        tx1.commit();
        em1.close(); // 종료, member 엔티티는 준영속 상태가 된다.
        
        return member;
    }
    
    static void mergeMember(Member member) {
    	EntityManager em2 = emf.createEntityManager();
        EntityTransaction tx2 = em2.getTransaction();
        
        tx2.begin();
        Member mergeMember = em2.merge(member);
        tx2.commit();
        
        em2.close();
    }
}

위 코드에서 mergeMember 메소드 내부에서 merge가 호출되면 그 밑에 em2.close() 메소드가 호출되기 전까지 mergeMember 엔티티는 영속 상태가 된다. 그렇기 때문에 준영속 상태가 된 기존 member 인스턴스와는 다른 인스턴스가 돼 동일성 보장하지 않는다.

병합은 비영속 엔티티도 영속 상태로 만들 수 있다. 식별자 값으로 캐시와 DB에서 조회하고 없으면 새로운 엔티티를 생성해서 병합한다!

🧷 참조 교재

  • 김영한, 『자바 ORM 표준 JPA 프로그래밍』 에이콘(2015)
profile
개발이란?

0개의 댓글