3. 영속성 관리


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

  • 1
  • 엔티티 매니저 팩토리
    • persistence.xml로부터 데이터베이스 정보를 읽어와 엔티티 매니저 팩토리가 생성됨
    • 비용이 많이 들지만 여러 스레드가 동시에 접근해도 안전하기 때문에 한번만 생성하고 공유해서 사용해야 함
    • 하이버네이트를 포함한 대부분의 구현체가 엔티티 매니저 팩토리가 생성될 때 DB의 커넥션 풀도 같이 생성해줌
  • 엔티티 매니저
    • 테이블 접근이 필요할 때마다 엔티티 매니저를 생성해서 테이블에 접근, 비용이 거의 들지 않음
    • 동시성 문제가 있어 스레드 간에 공유하면 안됨
    • 데이터베이스 연결이 필요한 시점에 커넥션을 획득함(EntityManager2)
    • 연결이 필요한 시점? -> 트랜잭션을 시작할 때

2. 영속성 컨텍스트란?

  • 영속성 컨텍스트(persistence context)는 엔티티를 영구적으로 저장하기 위한 논리적인 환경
  • em.persiste(object) 메소드를 호출하면 엔티티 매니저를 사용해서 엔티티를 영속성 컨텍스트에 저장함
  • 엔티티 매니저를 생성할 때 하나의 영속성 컨텍스트가 만들어짐
  • 엔티티 매니저를 통해 영속성 컨텍스트에 접근, 관리가 가능함
  • 여러 엔티티 매니저가 같은 영속성 컨텍스트에 접근할 수 있음

3. 엔티티 생명주기

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

  • 2

  • 비영속(new/transient)
    • 엔티티 객체를 생성하고 아직 저장하지 않은 상태
    • // 객체를 생성한 상태 (비영속)
      Member member = new Member();
      member.setId(1);
      member.setName("name");
      
  • 영속(managed)
    • 엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장한 상태
    • 영속성 컨텍스트가 관리하는 엔티티를 영속 상태라고 함
    • // 객체를 저장한 상태(영속)
      em.persist(member);
      // 조회한 엔티티도 영속 상태임
      Member member = em.find(Member.class, id);
      
  • 준영속(detached)
    • 영속 상태의 엔티티를 영속성 컨텍스트가 더이상 관리하지 않을 때의 상태
    • 다음 세 방법으로 준영속 상태로 만들 수 있음
    • em.detach(member); // 엔티티를 영속성 컨텍스트로에서 분리
          
      em.close(); // 영속성 컨텍스트를 닫음
      
      em.clear(); // 영속성 컨텍스트를 초기화 함
      
  • 삭제(removed)
    • 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한 상태
    • em.remove(member);
      

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

  • 영속성 컨텍스트와 식별자 값
    • 영속성 컨텍스트는 엔티티를 식별자 값(@Id 어노테이션으로 테이블의 기본 키와 매핑한 값)으로 구분함
    • 따라서 영속 상태는 식별자 값이 반드시 존재해야 함
  • 영속성 컨텍스트와 데이터베이스 저장
    • 트랜잭션을 커밋하는 순간 영속성 컨텍스트의 내용이 데이터베이스에 반영됨 flush
  • 영속성 컨텍스트가 엔티티를 관리해서 얻는 장점
    • 1차 캐시
    • 동일성 보장
    • 트랜잭션을 지원하는 쓰기 지연
    • 변경 감지
    • 지연 로딩
  • 1차 캐시
    • 영속성 컨텍스트가 내부적으로 가지고 있는 캐시
    • 영속 상태의 엔티티는 모두 이 1차 캐시에 저장됨
    • Map 자료구조 형태로 @Id를 키, 엔티티 인스턴스를 값으로 가지도록 구현되어 있음
    • 아래 사진과 같이 조회 요청에 대해서 1차 캐시에 엔티티가 없을 경우, 데이터베이스에서 엔티티를 1차 캐시로 가져온 다음에 영속 상태의 엔티티를 반환함
    • 3
  • 영속 엔티티의 동일성 보장
    • Member a = em.find(Member.class, 1);
      Member b = em.find(Member.class, 2);
      
      System.out.println(a == b); // 동일성 비교
      
    • 같은 엔티티를 반복해서 호출하므로 1차 캐시에서 동일한 엔티티를 가져오게 됨
    • 동일성 vs 동등성
      • 동일성(identity) : 실제 인스턴스가 같음 (==)
      • 동등성(equality) : 인스턴스가 가지고 있는 값이 같음 (equals())
  • 쓰기 지연
    • em.persist(memberA);
      em.persist(memberB); // 여기까지는 SQL을 데이터베이스에 쿼리하지 않음
      
      tx.commit();         // 트랜잭션을 커밋하는 순간 INSERT SQL 쿼리
      
    • 4
    • 위 사진과 같이 트랜잭션을 커밋하기 전까지 내부 쿼리 저장소에 SQL문을 저장해둠
    • 이것을 쓰기 지연 transactional write-behind라고 함
    • commit() 요청이 올 때 DB에 쿼리를 하고 이를 flush라고 함
    • flush : 영속성 컨텍스트의 변경 내용을 데이터베이스와 동기화하는 작업
  • 변경 감지
    • JPA에서 엔티티를 수정할 때는 단순히 엔티티를 조회해서 데이터만 변경하면 됨
    • em.update() 같은 메소드 없음, 엔티티의 변경사항을 알아서 반영해줌
    • 이를 변경 감지(dirty checking)라고 함
    • JPA는 엔티티를 영속성 컨텍스트에 보관할 때 최초 상태를 스냅샷 해둠
    • 이후 플러시 시점에 스냅샷과 비교하여 변경된 엔티티를 찾아 수정 쿼리를 생성함
    • 5
    • 변경 감지는 영속성 컨텍스트가 관리하는(영속 상태) 엔티티에만 적용됨
    • 업데이트 쿼리를 생성할 때는 엔티티의 모든 필드를 수정에 반영함
      • 데이터의 전송량이 증가하는 단점
      • 업데이트 쿼리가 항상 같아 재사용이 가능하다는 장점
      • 데이터베이스에 동일한 쿼리를 보낼 경우 한 번 파싱된 쿼리를 재사용 할 수 있음
      • 필드가 많거나 저장되는 내용이 많을 경우엔 하이버네이트 확장 기능을 통해 수정된 데이터만 사용해서 동적으로 UPDATE SQL을 생성할 수 있음 DynamicUpdate
        • @Entity
          @org.hibernate.annotations.DynamicUpdate // 수정 필드만 업데이트 쿼리
          @Table(name = "Member")
          public class Member {...}
          
        • 일반적으로 필드가 30개가 넘어갈 경우 동적 업데이트를 사용하는 것이 성능이 좋음
        • 정적 쿼리 방식으로 하다가 최적화가 필요하다 싶으면 동적을 시도해보는게 바람직

5. 플러시

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

  1. 변경 감지가 동작해서 영속성 컨텍스트의 모든 엔티티를 스냅샷과 비교, 수정된 엔티티를 찾음
  2. 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록
  3. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송
  • 플러시를 하는 방법
    • em.flush()를 직접 호출
      • 테스트나 다른 프레임워크와 JPA를 함께 사용할 때를 제외하고 거의 사용되지 않음
    • 트랜잭션 커밋 시 자동 호출
      • 트랜잭션을 커밋할 때 flush가 자동으로 호출됨
    • JPQL 쿼리 실행
      • JPQL 쿼리를 실행하면 flush가 자동으로 호출됨
      • 이유는?
        • JPQL은 데이터베이스에 직접 쿼리를 하기 때문에 쿼리 전에 영속성 컨텍스트의 내용을 데이터베이스에 반영해줘야 함
  • 플러시 모드 옵션
    • FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 플러시 (기본값)
    • FlushModeType.COMMIT : 커밋할 때만 플러시
    • em.setFlushMode(FlushModeType.COMMIT); // 플러시 모드 직접 설정
      
    • 플러시는 영속성 컨텍스트에 보관된 엔티티를 지우는것이 절대 아님.. 데이터베이스와 동기화 하는 것..

6. 준영속

영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)된 것을 준영속 이라고 함 따라서 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없음

준영속 상태로 만드는 방법

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

  • 엔티티를 준영속 상태로 전환 : detach()
    • 해당 메소드를 호출하는 순간 영속성 컨텍스트의 1차 캐시부터 쓰기 지연 SQL 저장소 까지 모두 제거됨
    • 6
  • 영속성 컨텍스트를 초기화 : clear()
    • 해당 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만듬
    • 영속성 컨텍스트를 제거하고 새로 만드는 것과 동일한 상태
  • 영속성 컨텍스트 종료 : close()
    • 해당 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만듬

주로 준영속 상태가 되는 것은 영속성 컨텍스트가 종료되면서 준영속이 됨. 개발자가 직접 준영속 상태로 만드는 것은 드문 일

  • 준영속 상태의 특징
    • 거의 비영속 상태에 가까움
      • 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 영속성 컨텍스트가 제공하는 기능들 동작 안함
    • 식별자 값을 가짐
      • 비영속 상태와의 차이점, 비영속 상태는 식별자 값이 없을 수 있음
      • 준영속 상태는 영속 상태에 있었던 것이기 때문에 식별자 값이 반드시 있음
    • 지연 로딩을 할 수 없음
      • 지연 로딩(Lazy Loading)은 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 것
      • 준영속 상태 영속성 먼텍스트가 관리하지 않으므로 지연 로딩 시 문제가 발생
  • 병합 : merge
    • 준영속 상태의 엔티티를 다시 영속 상태로 변경하는 것
    • merge()
      • 준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 반환함
      • 병합은 준영속, 비영속 상태일 때 모두 가능함
        • 식별자 값으로 엔티티가 조회가 되면 불러와서 병합
        • 식별자 값으로 조회가 불가능 하면 새로운 엔티티이므로 새로 생성해서 병합함
    • 7
        1. merge() 실행
        1. 파라미터로 넘어온 준영속 엔티티(member)의 식별자 값으로 1차 캐시에서 엔티티 조회
          • 2-1. 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 조회 후 1차 캐시에 저장
        1. 조회한 영속 엔티티(mergeMember)에 member 엔티티의 값을 채워 넣음
        1. mergeMember 반환





© 2020.02. by blupine