[JPA]영속성 관리(2)

Inung_92·2023년 10월 24일
1

JPA

목록 보기
4/7
post-thumbnail

해당 포스팅의 내용은 김영한 강사님의 자바 ORM 표준 JPA 프로그래밍 책의 내용을 정리하여 작성하였습니다.


영속성 컨텍스트

플러시

📖영속성 컨텍스트의 변경 내용을 데이터베이스와 동기화하는 작업을 의미하며 변경된 엔티티의 상태를 영속성 컨텍스트에서 데이터베이스로 푸시하고, SQL 명령을 실행하는 역할을 한다.

플러시는 변경 감지가 동작하면 모든 엔티티를 스냅샷과 비교해서 수정된 엔티티를 찾고 변경 사항에 대한 쿼리를 작성하여 쓰기 지연 SQL 저장소에 등록한 뒤 데이터베이스에 전송한다.

앞서 설명한 것처럼 persist() 등의 메소드를 호출하는 시점에는 영속성 컨텍스트에만 영향을 미친다. 이런 것들이 쓰기 지연 SQL 저장소에 쌓이고, 플러시가 실행되는 시점에 데이터베이스에 반영되어 동기화되는 것이다.

방법

  • flush() 직접 호출
  • 트랜잭션 커밋 시 자동 호출
  • JPQL 실행 시 자동 호출

직접 호출의 경우는 거의 사용하지 않으므로 넘어가겠다. JPA는 트랜잭션 범위 내에서 작업한 내용이 정상적으로 데이터베이스에 반영되게 하기 위하여 트랜잭션 커밋 시 자동으로 플러시를 호출한다.

또한, JPQL 실행 시 자동으로 플러시를 호출하는데 이유는 다음 코드를 보며 설명하겠다.

...생략
entityManager.persist(a);
entityManager.persist(b);
entityManager.persist(c);

// JPQL 실행
query = entityManager.createQuery("select ...");
List<Entity> entities = query.getResultList(); 

위 코드에서 a, b, c 객체를 persist() 로 처리했다. 이 시점에서는 플러시가 실행되지 않기 때문에 영속성 컨텍스트에만 해당 내용이 반영되어있다. 하지만 JPQL을 이어서 실행하면 데이터베이스에 반영되지 않은 데이터를 가져와야 하는 상황이 발생한다. 이러한 이유로 JPA는 JPQL을 실행하기 직전에 플러시를 자동으로 호출해서 이전 결과를 데이터베이스에 반영하고 쿼리를 수행하여 결과를 도출하는 것이다.

모드

플러시는 두가지 모드가 있다.

  • FlushModeType.AUTO : 커밋이나 JPQL 실행 시 플러시
  • FlushModeType.COMMIT : 커밋할 때만 플러시

플러시 모드를 별도 설정하지 않을 경우 기본 설정은 AUTO이다.

다시 정리하면 플러시는 영속성 컨텍스트의 변경 사항을 데이터베이스에 동기화하는 것을 의미한다. 그리고 이러한 동기화를 늦출 수 있는 이유는 트랜잭션이라는 작업 단위가 있기 때문이다.

준영속 상태의 특징

앞서 엔티티의 생명주기를 배우면서 준영속 상태에 대해서 배웠다. 그렇다면 준영속 상태의 특징에 대해서 조금 더 자세히 알아보고, 준영속 상태의 엔티티를 다시 영속 상태로 변경 할 수 있는 방법에 대해서도 알아보자.

준영속 상태는 다음과 같은 특징을 갖는다.

  • 1차 캐시 사용 불가능
  • 쓰기 지연 불가능
  • 변경 감지 대상에서 제외
  • 지연 로딩 불가능
  • 식별자 값 보유

위 특징들은 거의 비영속 상태와 가깝지만 식별자 값을 보유하고 있다는 점이 다르다. 식별자 값을 가지고 있는 이유는 최소 한번은 영속 상태로 영속성 컨텍스트의 관리를 받았었기 때문이다.

이러한 이유로 식별자 값을 가지고 있다고해서 영속 상태로 관리하려고 했다가는 오류가 발생한다. 그렇다면 준영속 상태의 엔티티를 다시 영속 상태로 변경하는 방법은 어떤 것이 있을까?

병합 : merge()

merge() 메소드는 준영속 상태의 엔티티의 정보를 통해서 새로운 영속 상태의 엔티티를 반환한다. 코드를 통해 알아보자.

// merge
public <T> T merge(T entity);

// 사용
Member mergeMember = entityManager.merge(member);

위 코드는 merge() 의 정의와 사용하는 예시를 나타낸 것이다. 위 코드를 토대로 예시를 작성하고 결과를 보자.

public class ExamMergeMain {
    static EntityManagerFactory factory = Persistence.createEntityManagerFactory("jpabook");

    public static void main(String[] args) {
        // 준영속 상태 엔티티 반납
        TestMember member = createMember("test1", "testMember");
        // 객체 필드 변경
        // 지연쓰기에 적재된 상태
        member.setUserName("mergeMember");
        
        // merge로 준영속 -> 영속
        mergeMember(member);
    }

    public static TestMember createMember(String id, String userName) {
        EntityManager entityManager1 = factory.createEntityManager();
        EntityTransaction transaction1 = entityManager1.getTransaction();

        transaction1.begin();
        
        // 비영속 상태
        TestMember member = new TestMember();
        member.setId(id);
        member.setUserName(userName);
        
        // 영속 상태
        entityManager1.persist(member);
        transaction1.commit();
        
        // 영속성 컨텍스트 종료 = 모든 엔티티 준영속 상태
        entityManager1.close();

        return member;
    }

    public static void mergeMember(TestMember member) {
        EntityManager entityManager2 = factory.createEntityManager();
        EntityTransaction transaction2 = entityManager2.getTransaction();

        transaction2.begin();
        // 준영속 -> 영속(새로운 영속 상태의 엔티티를 반환)
        TestMember mergeMember = entityManager2.merge(member);

        // 지연쓰기에 적재된 쿼리 수행
        transaction2.commit();

        System.out.println("member :: " + member.getUserName());
        System.out.println("mergeMember :: " + mergeMember.getUserName());

        System.out.println("member contains? " + entityManager2.contains(member));
        System.out.println("mergeMember contains? " + entityManager2.contains(mergeMember));

        entityManager2.close();
    }
}

// 결과
member :: mergeMember
mergeMember :: mergeMember
member contains? false
mergeMember contains? true

예시에서 엔티티를 저장하고 영속성 컨텍스트를 close() 메소드로 종료했다. 모든 엔티티가 준영속 상태가 되었고, 여기서 엔티티의 상태를 변경했다. 당연히 객체의 필드는 변경이되지만 해당 사항이 데이터베이스에 반영되지는 않는다. 다시 merge() 를 수행해 준영속 상태의 엔티티를 영속 상태로 변경했다.

여기서 조금 더 안전하게 merge()를 사용하는 방법을 알아보자.

transaction2.begin();
// 준영속 -> 영속(새로운 영속 상태의 엔티티를 반환)
TestMember mergeMember = entityManager2.merge(member);

// 지연쓰기에 적재된 쿼리 수행
transaction2.commit();

이 부분에서 새로운 엔티티인 mergeMember 에 준영속 엔티티를 대입하면 준영속 상태와 영속 상태 엔티티를 구분하는 것이 힘들고, 이로 인한 오류가 발생 할 가능성이 증가한다. 따라서 준영속 상태를 영속 상태로 변경 할 때에는 동일한 엔티티에 대입하여 주면 된다.

transaction2.begin();
// member를 merge의 인자로 전달하면서 동시에 member에 반환 값 대입
member = entityManager2.merge(member);

// 지연쓰기에 적재된 쿼리 수행
transaction2.commit();

이렇게 코드를 변경하면 준영속 상태 엔티티인 member 의 정보로 새로 영속 상태가 된 엔티티 정보를 반환하여 대입하게 되기 때문에 불필요한 객체가 하나 더 생성되는 것을 방지 할 수 있다.

참고로 병합은 준영속과 비영속을 구분하지 않으니 참고하자. 조회 할 수 있는 엔티티라면 불러서 병합을 시키고, 조회 할 수 없다면 새로 생성해서 병합을 하는 것이 차이이다.


마무리

JPA의 핵심 개념인 영속성 관리에 대해서 알아보았다. 너무 핵심적인 개념이기도하고, 잘 이해하려고 작성하다보니 길어졌던 것 같다. 모든 걸 이해한 것은 아니지만 영속성에 대해 인지를 하고 JPA를 공부하는 것과 모르고 하는 것에는 아주 큰 차이가 있다는 것을 느끼게 되었다.
다음 포스팅부터는 본격적으로 JPA를 사용하는 방법에 대해서 알아보도록 하자.

그럼 이만.👊🏽

profile
서핑하는 개발자🏄🏽

0개의 댓글