오늘 알아볼 것은 JPA의 기초인 영속성을 알아보자. 맨날 매핑, 조인등등 알아봤지만 JPA에서 가장 중요한 영속성을 포스팅은 안한듯하다.

영속성 컨텍스트

영속성 컨텍스트란 엔티티를 영구 저장하는 환경? 이라고 해석 할 수 있다. 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.

entityManager.persist(member)

이 코드는 단순히 회원 엔티티를 저장하는데 정확히 이야기 하면 persist() 메서드는 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장한다.

엔티티의 생명주기

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

한개씩 살펴보자

비영속성

엔티티 객체를 생성하고 순수한 객체 상태이며 저장하지 않았다. 영속성 컨텍스트나 데이터베이스와는 전혀 관련이 없다. 이것을 비영속성 상태라 한다.

Member member = new Member();
member.setId("MemberId1");
member.setName("name1");

영속

엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장했다. 영속성 컨텍스트가 관리하는 엔티티를 영속 상태라 한다. 이제 회원 엔티티는 비영속상태에서 영속상태가 되었다. 결국 영속 상태라는 것은 영속성 컨텍스트에 의해 관리된다는 뜻이다. 그리고 entityManager.find()나 JPQL을 사용해서 조회한 엔티티도 영속성 컨텍스트가 관리하는 영속상태이다.

entityManager.persist(member);

준영속

영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 된다. 특정 엔티티를 준영속 상태로 만들려면 entityManager.detach()를 호출하면 된다. entityManager.close()를 호출해서 영속성 컨텍스트를 닫거나 entityManager.clear()를 호출해서 영속성 컨텍스트를 초기화해도 영속성 컨텍스트가 관리하던 영속 상태의 엔티티는 준영속이 된다.

entityManager.detach(member);

삭제

엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한다.

entityManager.remove(member);

특징

  1. 영속성 컨텍스트는 엔티티를 식별자 값으로 구분한다. 따라서 영속 상태는 식별자 값이 반드시 있어야 한다. 없으면 예외가 발생한다.
  2. 영속성 컨텍스트에 엔티티를 저장하면 이 엔티티는 언제 저장이 될까? JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영하는데 이를 플러시(flush)라 한다.
  3. 영속성 컨텍스트가 엔티티를 관리하면 다음과 같은 장점이 있다.
    • 1차 캐시
    • 동일성 보장
    • 트랜잭션을 지원하는 쓰기 지연
    • 변경 감지
    • 지연 로딩

플러시

플러시(flush())는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다. 플러시를 실행하면 다음과 같은 일이 일어난다.
1. 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해서 수정된 엔티티를 찾는다. 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다.
2. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다. (등록, 수정, 삭제 쿼리)

영속성 컨텍스트를 플러시하는 방법은 3가지가 있다.
1. 직접 호출 하는 방법인데 엔티티 매니저의 flush() 메서드를 직접 호출해서 영속성 컨텍스트를 강제로 플러시 하면 도니다. 테스트나 다른 프레임워크와 JPA를 함께 사용할 때를 제외하고 거의 사용하지 않는다.
2. 트랜잭션 커밋이 자동으로 플러시가 호출된다.
3. JPQL 쿼리 실행 할때에도 플러시가 자동으로 호출된다.

준영속

영속상태에서 준영속 상태 변화를 알아보자
위에서도 말했듯이 영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 것을 준영속 상태라 한다. 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.
영속 상태의 엔티티를 준영속 상태로 만드는 방법은 크게 3가지가 있다.
1. entityManager.detach(member) // 특정 엔티티만 준영속 상태로 전환한다.
2. entityManager.clear() // 영속성 컨텍스트를 완전히 초기화 한다.
3. entityManager.close() // 영속성 컨텍스트를 종료한다.

private static void detached(EntityManager entityManager) {
    Member member3 = new Member("Id3", "회원A", 40);
    entityManager.persist(member3);
    entityManager.detach(member3); // 준영속으로 전환
//    entityManager.clear();   //모든 엔티티를 준영속 전환
//    entityManager.close(); // 영속성 컨텍스트 종료
}

detach 메서드를 호출하는 순간 해당 엔티티를 관리하지 말라는 것이다. 이는 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 사라진다. 실제 위의 코드도 데이터베이스에 저장되지 않는다.
detach 메서드는 엔티티 하나의 대상이지만 clear는 모든 엔티티를 초기화해서 준영속 상태로 만든다. 이때에도 마찬가지로 데이터베이스에 반영되지 않는다.
close 메서드도 clear랑 비슷하지만 약간 다른것 같다 close는 entityManager를 종료 시키기에 다시 entityManager를 만들어야 하는 듯하다.

준영속 상태의 특징

  1. 거의 비영속 상태에 가깝다.
  2. 비영속과는 달리 식별자 값을 가지고 있다.
  3. 지연 로딩을 할 수 없다.

병합

준영속 상태의 엔티티를 다시 영속 상태로 변경하려면 병합을 사용하면 된다. merge() 메서드는 준영속 상태의 엔티티를 받아서 새로운 영속상태의 엔티티를 반환한다.

Member member = createMember("id4", "회원B", 31);
//준영속일때 변경
member.setUsername("병합");
merge(member);
print();

private static Member createMember(String id, String name, Integer age) {
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    EntityTransaction transaction = entityManager.getTransaction();

    Member member = new Member(id, name, age);
    try {
        transaction.begin(); // 트랜잭션 시작
        entityManager.persist(member);
        transaction.commit();
    } catch (Exception e) {
        transaction.rollback();
    } finally {
        entityManager.close();
    }
    return member;
}

private static void merge(Member member) {
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    EntityTransaction transaction = entityManager.getTransaction();
    try {
        transaction.begin(); // 트랜잭션 시작
        Member mergeMember = entityManager.merge(member);
        System.out.println("entityManager.contains(mergeMember) : " +
                entityManager.contains(mergeMember)
        );
        System.out.println("entityManager.contains(member) : " +
                entityManager.contains(member));
        transaction.commit();
    } catch (Exception e) {
        transaction.rollback();
    } finally {
        entityManager.close();
    }
}

만약 위와 같은 코드가 있다면 준영속일때 변경을 했지만 merge를 해서 다시 영속 상태로 변경시켰다. 그럼 실제 데이터베이스에도 준영속 상태에서 변경된 값이 들어간다.

비영속 병합

병합은 비영속 엔티티도 영속 상태로 만들 수 있다.

Member member = new Member();
Member newMember = entityManager.merge(member);
tx.commit();

병합은 파라미터로 넘어온 엔티티의 식별자 값으로 영속성 컨텍스트를 조회하고 찾는 엔티티가 없으면 데이터베이스에서 조회한다. 만약 데이터베이스에서도 찾지 못하면 새로운 엔티티를 생성해서 병합한다. 병합은 준영속, 비영속을 신경쓰지 않는다. 식별자 값으로 엔티티를 조회할 수 있으면 불러서 병합하고 조회할 수 없으면 새로 생성해서 병합한다. 따라서 병합은 save or update 기능을 수행한다.

이것으로 JPA 영속성에 대해서 알아봤다.