영속성 관리

3. 영속성 관리

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

  • 엔티티 매니저 팩토리
    • 엔티티 매니저를 만드는 오브젝트이다.
    • META-INF/persistence.xml의 정보를 바탕으로 EntityManagerFactory 오브젝트를 생성한다.
    • 스레드 간에 공유해도 된다.
  • 엔티티 매니저
    • 여러 스레드가 동시에 접근하면 동시성 문제가 발생한다.
    • 스레드 간에 공유하면 안된다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
<persistence-unit name="jpabook">
    <properties>
        ...
    </properties>
</persistence-unit>

3.2 영속성 컨텍스트란?

  • 영속성 컨텍스트는 ‘엔티티를 영구 저장하는 환경’ 으로 해석된다.
  • 엔티티 매니저를 통해서 영속성 컨텍스트에 접근, 관리 할 수 있다.
  • 엔티티 매니저의 persist() 메소드는 엔티티를 영속성 컨텍스트에 저장한다.

3.3 엔티티의 생명주기

  • 비영속
    • 엔티티 객체가 생성된 상태이다.
    • 영속성 컨텍스트와 관련이 없는 상태이다.
    • 일반적으로 식별자 값이 할당되지 않은 상태이다.
Member member = new Member();
member.setUsername("회원1");
  • 영속
    • 엔티티가 영속성 컨텍스트에 의해 관리되는 상태이다.
em.persist(member);
Member member = em.find(Member.class, memberId);
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
  • 준영속
    • 영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않는 상태이다.
    • 엔티티 객체에 식별자 값이 있는 상태이다.
em.detach(member);
em.close();
em.clear();
  • 삭제
    • 엔티티가 영속성 컨텍스트에서 삭제된 상태이다.
    • 엔티티가 데이터베이스에서 제거된다.
em.remove(member);

3.4 영속성 컨텍스트의 특징

  • 영속성 컨텍스트와 식별자 값
    • 영속성 컨텍스트는 엔티티를 식별자 값으로 구분한다. (식별자 값이란 @Id로 기본 키와 매핑한 값을 말한다.)
    • 따라서 영속 상태는 식별자 값이 반드시 있어야 한다.
  • 영속성 컨텍스트와 데이터베이스 저장
    • 영속성 컨텍스트에 엔티티를 저장하는 시점과 데이터베이스에 저장하는 시점은 다르다.
    • 플러시를 하면 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영한다.
  • 영속성 컨텍스트가 엔티티를 관리했을 때 얻을 수 있는 장점은 다음과 같다.
    • 1차 캐시
    • 동일성 보장
    • 트랜잭션을 지원하는 쓰기 지연
    • 변경 감지
    • 지연 로딩

3.4.1 엔티티 조회

  • 1차 캐시
    • 영속성 컨텍스트 내부에 캐시가 있는데, 영속상태의 엔티티가 저장된다.
    • Map에 키는 식별자이고 값은 엔티티 인스턴스인 형태로 저장한다.
  • em.find() 호출시
    • 1차 캐시에서 식별자 값으로 엔티티를 찾는다. 만약 엔티티가 있으면 데이터베이스를 조회하지 않고 1차 캐시에서 조회한다.
    • 1차 캐시에 엔티티가 없으면 데이터베이스를 조회해서 엔티티를 생성하여 1차 캐시에 저장한다. 그리고 엔티티를 반환한다.
  • 식별자가 같은 엔티티를 반복해서 호출했을 때 1차 캐시에 있는 같은 엔티티가 반환되므로 동일성이 보장된다.

3.4.2 엔티티 등록

  • em.persist() 호출시
    • 1차 캐시에 식별자와 엔티티를 키와 값으로 저장한다.
    • 등록 쿼리를 생성하여 쓰기 지연 SQL 저장소에 저장한다.
    • 트랜잭션 커밋시 영속성 컨텍스트를 플러시 하면서 데이터베이스에 엔티티가 저장된다.(이 때 쿼리가 전송된다.)
      • 단, 식별자 자동 생성 사용시 IDENTITY 전략을 사용하면 persist 메소드 호출 뒤에 쿼리가 전송된다.
    • 트랜잭션 커밋 직전에만 데이터베이스에 SQL을 전송하면 되므로, 트랜잭션을 지원하는 쓰기 지연이 가능하다.

3.4.3 엔티티 수정

  • 엔티티를 조회해서 데이터를 변경하면 된다.
  • 변경 감지(dirty checking) 기능에 의해 엔티티의 변경사항이 자동으로 데이터베이스에 반영된다.
  • 변경 감지
    • JPA가 엔티티를 영속성 컨텍스트에 저장할 때 최초 상태를 복사해서 저장해둔다. (이를 스냅샷이라고 부른다.)
    • 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다.
    • 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 저장한다.
    • 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.
  • 변경 감지로 인해 실행된 Update SQL은 모든 필드를 업데이트한다.

3.4.4 엔티티 삭제

  • em.find()로 삭제 대상 엔티티를 먼저 조회한다.
  • em.remove()로 삭제 대상 엔티티를 넘겨준다.
    • 영속성 컨텍스트에서 제거된다.
    • 삭제 쿼리가 쓰기 지연 SQL 저장소에 저장된다.

3.5 플러시

  • 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.
    • 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해서 수정된 엔티티를 찾는다.
    • 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다.
    • 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.(등록, 수정, 삭제 쿼리)
  • 플러시는 3가지 방법으로 할 수 있다.
    • em.flush()를 호출한다.
    • 트랜잭션 커밋 시 플러시가 자동 호출된다.
    • JPQL 쿼리 실행 시 플러시가 자동 호출된다.
      • JPQL 쿼리 실행 전 영속상태의 엔티티들을 데이터베이스에 반영해야 JPQL 쿼리가 제대로 동작하기 때문이다.
try {
    // 비영속
    Member member = new Member();
    member.setUsername("member1");
    member.setAge(10);

    em.persist(member);

    // 플러시 자동 호출됨
    List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
    for (Member m : members) {
        System.out.println("m = " + m);
    }
    
    tx.commit();
}
catch (Exception e) {
    tx.rollback();
    e.printStackTrace();
}
finally {
    em.close();
}

emf.close();

3.5.1 플러시 모드 옵션

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

3.6 준영속

  • 영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 것을 준영속 상태라고 한다.
  • 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.

3.6.1 detach()

  • detach() 메소드를 호출하는 순간 1차 캐시에서 엔티티가 제거된다. 쓰기 지연 SQL 저장소의 해당 엔티티의 쿼리도 제거된다.
  • 따라서 아래 코드에서 member는 데이터베이스에 저장되지 않는다.
tx.begin();

try {
    // 비영속
    Member member = new Member();
    member.setUsername("member1");
    member.setAge(10);

    // 영속
    em.persist(member);

    // 준영속
    em.detach(member);

    tx.commit();
}
catch (Exception e) {
    tx.rollback();
    e.printStackTrace();
}
finally {
    em.close();
}

emf.close();

3.6.2 clear()

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

3.6.3 close()

  • close() 메소드는 영속성 컨텍스트를 종료한다.

3.6.4 준영속 상태의 특징

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

3.6.5 merge()

  • 준영속 상태의 엔티티, 비영속 상태의 엔티티를 영속 상태로 변경한다.
  • 준영속 병합
    • 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다.
    • 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고 1차 캐시에 저장한다.
    • 조회한 엔티티에 member 엔티티의 값을 채워넣는다.
    • 영속 상태의 member를 반환한다. (파라미터로 넘어온 준영속 엔티티는 준영속으로 남아있다.)
  • 비영속 병합
    • 파라미터로 넘어온 엔티티의 식별자 값으로 영속성 컨텍스트를 조회하고 없으면 데이터베이스에서 조회한다.
    • 데이터베이스에서도 찾지 못하면 새로운 엔티티를 생성해서 병합한다.
try {
    // 비영속
    Member member = new Member();
    member.setUsername("member1");
    member.setAge(10);

    // 영속
    em.persist(member);

    // 준영속
    em.flush();
    em.clear();

    System.out.println(em.contains(member)); // false
    member = em.merge(member);
    System.out.println(em.contains(member)); // true

    tx.commit();
}
catch (Exception e) {
    tx.rollback();
    e.printStackTrace();
}
finally {
    em.close();
}

emf.close();

Tags:

Categories:

Updated:

Leave a comment