Updated:

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

엔티티 매니저 팩토리는 이름 그대로 엔티티 매니저를 만드는 공장인데, 공장을 만드는 비용은 상당히 크다.
따라서 한 개만 만들어서 애플리케이션 전체에서 공유하도록 설계되어 있다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");

반면에 공장에서 엔티티 매니저를 생성하는 비용은 거의 들지 않는다.

EntityManager em = emf.createEntityManager();

그리고 엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드 간에 공유해도 되지만,
엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유하면 안 된다.

2. 영속성 컨텍스트란?

‘엔티티를 영구 저장하는 환경’ 이라는 뜻이다.
아래 코드를 정확히 이야기하면 persist() 메소드는 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장한다는 의미다.

em.persist(member);

3. 엔티티의 생명주기

엔티티에는 4가지 상태가 존재한다.

  • 비영속(new/transient)
    • 영속성 컨텍스트와 전혀 관계가 없는 상태
  • 영속(managed)
    • 영속성 컨텍스트에 저장된 상태
    • 영속성 컨텍스트가 관리하는 엔티티를 영속 상태라고 한다
      em.persist(member); // em.find() 나 JPQL도 해당
      
  • 준영속(detached)
    • 영속성 컨텍스트에 저장되었다가 분리된 상태
    • 영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 된다
      em.detach(member);
      em.close();
      em.clear();
      
  • 삭제(removed)
    • 삭제된 상태
    • 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한다
      em.remove(member);
      

4. 영속성 컨텍스트의 특징

  • 영속성 컨텍스트와 식별자 값
    • 영속성 컨텍스트는 엔티티를 식별자 값 (@Id로 테이블의 기본 키와 매핑한 값) 으로 구분한다
    • 따라서 영속 상태는 식별자 값이 반드시 있어야 한다. 식별자 값이 없으면 예외가 발생한다
  • 영속성 컨텍스트와 데이터베이스 저장
    • 영속성 컨텍스트에 엔티티를 저장하면 트랜잭션을 커밋할 때 데이터베이스에 반영한다
    • 이것을 플러시(flush) 라 한다
  • 영속성 컨텍스트가 엔티티를 관리 시 장점
    • 1차 캐시
      • em.find() 를 호출하면 먼저 1차 캐시에서 엔티티를 찾고, 만약 찾는 엔티티가 없으면 데이터베이스에서 조회한다
    • 동일성 보장
    • 트랜잭션을 지원하는 쓰기 지연
      • 이 기능을 잘 활용하면 모아둔 등록 쿼리를 데이터베이스에 한 번에 전달해서 성능을 최적화할 수 있다
    • 변경 감지
      • JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해두는데 이것을 스냅샷이라 한다
      • 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다
      • 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다
      • 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다
      • 데이터베이스 트랜잭션을 커밋한다
      • 기본 전략은 엔티티의 모든 필드를 업데이트 한다 (변경된 부분만 수정하지 않는다)
      • 이로 인해서 데이터 전송량이 증가하는 단점이 있겠지만, 수정 쿼리가 항상 같기 때문에 한 번 파싱된 쿼리를 재사용할 수 있다
      • 동적으로 쿼리를 생성하는 @DynamicUpdate, @DynamicInsert 도 있다는 것 참고
    • 지연 로딩

5. 플러시

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

  1. 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해서 수정된 엔티티를 찾는다
  2. 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다
  3. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다 (등록, 수정, 삭제 쿼리)

영속성 컨텍스트를 플러시하는 방법 3가지

  • em.flush() 를 직접 호출한다
  • 트랜잭션 커밋 시 플러시가 자동 호출된다
  • JPQL 쿼리 실행 시 플러시가 자동 호출된다
    • 참고) 식별자를 기준으로 조회하는 find() 메소드를 호출할 때는 플러시가 실행되지 않는다

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

  • FlushModeType.AUTO: 커밋이나 쿼리를 실행할 때 플러시 (기본값)
  • FlushModeType.COMMIT: 커밋할 때만 플러시

명심할 부분은
플러시는 영속성 컨텍스트에 보관된 엔티티를 지우는 개념이 아니라
변경 내용을 데이터베이스에 동기화하는 것임을 잊지말자!

6. 준영속

준영속 상태의 특징

  • 거의 비영속 상태에 가깝다
    • 영속성 컨텍스트가 관리하지 않으므로 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 영속성 컨텍스트가 제공하는 어떠한 기능도 동작하지 않는다
  • 식별자 값을 가지고 있다
    • 비영속 상태는 식별자 값이 없을 수도 있지만 준영속 상태는 이미 한 번 영속 상태였으므로 반드시 식별자 값을 가지고 있다
  • 지연 로딩을 할 수 없다
    • 지연 로딩(LAZY LOADING) 은 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법이다
    • 하지만 준영속 상태는 영속성 컨텍스트가 더는 관리하지 않으므로 지연 로딩 시 문제가 발생한다

병합: merge()

  • 준영속 상태의 엔티티를 다시 영속 상태로 변경하는 방법
  • 파라미터로 넘어온 준영속 엔티티를 사용해서 새롭게 병합된 영속 상태의 엔티티를 반환한다
  • 파라미터로 넘어온 엔티티는 병합 후에도 준영속 상태로 남아 있는다
  • 병합은 준영속, 비영속을 신경 쓰지 않는다
  • 식별자 값으로 엔티티를 조회할 수 있으면 불러서 병합하고 조회할 수 없으면 새로 생성해서 병합한다
  • 따라서 병합은 save or update 기능을 수행한다

Leave a comment