본문 바로가기

JPA

[JPA] 영속성 컨텍스트 (Persistance Context)

JPA를 사용함에 있어 가장 중요한 개념은 아마 영속성 컨텍스트(Persistance Context)일 것이다.

em.persist(member);

JPA는 다음과 같인 엔티티 매니저(Entity Manager)의 메서드인 persist() 메서드를 통해 객체를 영속성 컨텍스트에 저장한다. 영속성 컨텍스트에 저장된 데이터는 이름 그대로 영구히 관리되게 된다. 또한 영속성 컨텍스트는 엔티티 매니저를 통해 접근할 수 있고 관리할 수 있다.

 

이번 포스팅에서는 영속성 컨텍스트의 개념 및 특징 그리고 사용 전략에 대해 차례대로 알아보기로 하자.

 

참고도서

 

자바 ORM 표준 JPA 프로그래밍 - 교보문고

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임 | ★ 이 책에서 다루는 내용 ★■ JPA 기초 이론과 핵심 원리■ JPA로 도메인 모델을 설계하는 과정을 예제 중심으로

www.kyobobook.co.kr

 

 

영속성 컨텍스트

 


엔티티의 생명주기

엔티티는 크게 4단계의 생명주기를 가진다.

엔티티 생명주기

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

 

영속성 컨텍스트의 특징

영속성 컨텍스트를 사용하면서 주의해야할 특징은 다음과 같다.

 

  • 영속 상태는 식별자 값이 반드시 있어야 한다. 식별자 값이 지정하지 않고 영속성 컨텍스트에 저장할 경우, 예외가 발생한다.
  • 영속 상태로 관리되는 엔티티에 데이터를 변경하면 플러시(flush)를 통해 값이 DB에 반영되는데 플러시는 보통 트랜잭션이 커밋되면 자동으로 실행되어 DB 값을 변경시킨다.

 

이외에도 영속성 컨텍스트는 여러 장점이 있는데 다음과 같다.

 

  • 1차 캐시
  • 동일성 보장
  • 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
  • 변경 감지 (dirty chekcing)
  • 지연 로딩 (lazy loading)

 


 

엔티티 조회와 1차캐시

Member member = new Member();
member.setId(1L);
member.setUsername("orly");

// member 엔티티를 영속 상태로 만듦
em.persist(member);

member 엔티티를 생성하고 영속성 컨텍스트에 저장해주었다. 그렇다면 이 데이터는 바로 DB에 반영이 될까? 답은 아니다. 영속상태로 관리되는 데이터는 일단 1차 캐시 라는 공간에 저장이 되고, 이후 트랜잭션을 커밋을 해야 DB에 반영이 된다.

영속성 컨텍스트의 1차 캐시에 member 엔티티가 저장되었다

1차 캐시는 key-value 형태로 id와 entity를 저장한다. 그리고 키 값인 식별자를 통해 엔티티를 조회할 수 있다.

em.find(Member.class, 1L);

EntityManager의 find() 메서드를 값을 저장한 클래스와 식별자값을 매개변수로 호출하면 1차캐시에서 식별자를 조회해 해당 키를 가진 엔티티를 반환한다.

그렇다면 조회할 엔티티가 1차 캐시에는 없고, DB에만 있는 경우는 어떻게 조회할까? 일단 엔티티매니저가 1차 캐시 조회 후, 데이터가 1차 캐시에 없다는 것을 확인하면 그후, DB를 조회하여 조회할 엔티티를 일단 1차캐시에 저장한다. 그후, 다시 1차 캐시를 조회하여 엔티티를 반환한다.

 

 

<엔티티 조회 전략>

  1. 1차 캐시를 조회한다.
  2. 1차 캐시에서 조회한 엔티티를 반환한다. (finish)
  3. 조회할 엔티티가 1차 캐시에 없는 경우, DB를 조회한다.
  4. DB에서 조회된 데이터를 1차 캐시에 올린다.
  5. 1차 캐시를 조회하여 엔티티를 반환한다. (finish)

 

엔티티 등록과 쓰기 지연(write-behind)

위에서도 언급했듯이 엔티티를 등록(em.persist...)하면 그것이 1차 캐시에는 등록되지만 바로 DB에 반영되는 것은 아니다. 트랜잭션을 커밋해야지만 쿼리가 DB로 전달돼 변경 사항이 DB에 반영되는데 이러한 방식을 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)이라고 한다.

 

em.persist() 메서드를 통해 엔티티를 등록하면 크게 2가지일이 발생한다. 첫째는 엔티티 객체를 1차 캐시에 저장하는 것이고, 두번째는 INSERT SQL을 생성하는 것이다. 이때 INSERT SQL은 트랜잭션이 커밋되는 시점 까지 영속성 컨텍스트에서 관리 해야하는데 이를 위해 영속성 컨텍스트는 쓰기 지연 SQL 저장소라는 공간을 따로 두고 있다.

 

트랜잭션을 커밋하면 엔티티 매니저는 먼저 영속성 컨텍스트를 플러시한다. 플러시란 영속성 컨텍스트의 변경 사항을 동기화하는 과정을 말한다. 그 후, 실제 DB의 트랜잭션을 커밋하면 변경사항이 DB에 반영되게 된다.

 

<엔티티 등록 전략>

  1. 저장한 엔티티를 1차 캐시에 저장
  2. INSERT SQL 쿼리를 쓰기 지연 SQL 저장소에 저장
  3. 트랜잭션 커밋시 플러시(flush)를 통해 변경 사항 DB에 반영
  4. 실제 DB의 트랜잭션을 커밋

 

등록 쿼리를 그때 그때 보내지 않고, 커밋 시점에 몰아서 보내는 것이 문제가 되지 않을까 생각할 수 있다. 하지만 어차피 쿼리의 변경 사항이 DB 데이터의 반영되는 것은 커밋 시점이다. 즉, 트랜잭션을 커밋하기 전에만 보내면 어느 시점에 보내든 아무 상관이 없다. 그렇기 때문에 등록 쿼리를 메모리에 모아놨다가 트랜잭션을 커밋하기 직전에 DB로 보내도 아무런 문제가 발생하지 않는다.

 


엔티티 수정과 변경 감지(dirty checking)

SQL을 직접 작성하여 코딩을 하면 가장 골치가 아픈것이 바로 수정 쿼리이다.

UPDATE MEMBER
SET
    NAME=?,
    AGE=?
WHERE
	ID=?

보통 다음과 같이 수정 쿼리를 작성하는데 문제는 수정 쿼리는 수정 되는 데이터의 종류에 따라 다 다르게 작성된다는 것이다. 예를 들어, Member의 등급(GRADE)만을 수정하는 요구사항과 Member의 모든 필드를 수정하는 요구사항이 추가되었다고 생각해보자. 그렇다면 상황에 맞게 또 다른 쿼리를 각각 만들어 줘야 한다.

 

이렇듯 SQL로 수정 쿼리를 직접 작성하면 수정 쿼리가 많아지는 데다가, 비즈니스 로직을 분석하기 위해 SQL 레벨까지 내려와야 한다는 단점이 있다.

 

JPA는 이러한 문제를 어떻게 해결할까?

Member member = em.find(Member.class, 1L);

// 엔티티 수정
member.setName("name_new");
member.setAge(10);

tx.commit();

위 코드는 JPA에서 엔티티를 수정하는 방법이다. 이상하다. 왠지 em.update(?) 같은 메서드가 있어야 할 것 같지만 그냥 엔티티를 조회해 엔티티의 데이터를 수정만 할 뿐이다. JPA는 영속성 엔티티의 데이터만을 수정해도 해당 변경 사항을 감지해 커밋 시점에 DB에 반영하는데 이를 변경 감지(Dirtch Checking)이라고 한다.

 

변경 감지의 매커니즘을 알기 위해서는 플러시(flush()) 메서드의 작동 방식에 대해 먼저 알아야 한다. 엔티티가 최초로 영속성 컨텍스트에 등록되면 엔티티 매니저는 엔티티의 초기상태를 복사해 저장해두는데 이를 스냅샷(snapshot)이라고 한다. 이후 flush() 메서드를 호출하면 엔티티와 스냅샷을 비교해 변경사항 찾아 변경된 부분이 있으면 수정 쿼리를 생성해 쓰기 지연 SQL 저장소에 저장한다. 이후 트랜잭션을 커밋하면 이 쿼리가 DB에 반영되는 것이다.

 

<변경 감지(dirty checking) 작동 과정>

  1. 트랜잭션을 커밋하면 flush() 메서드를 호출
  2. 엔티티와 스냅샷을 비교하여 변경 사항이 있으면 수정 쿼리를 생성해 쓰기 지연 SQL에 저장
  3. 쓰기 지연 SQL 저장소에 있는 쿼리를 DB로 저장
  4. 실제 DB의 트랜잭션을 커밋

위에서도 언급했어지만 수정 쿼리는 수정 사항에 따라 쿼리의 형식이 달라지게 된다. 그렇다면 JPA는 어떻게 수정 쿼리를 작성할까? JPA의 기본 전략을 모든 필드를 업데이트 하는 것이다. 모든 필드를 업데이트 하는 것은 다음과 같은 장점을 가진다.

 

  1. 모든 필드를 사용하면 수정 쿼리가 항상 같다. 그리므로 쿼리를 미리 생성해두고 반복해서 사용할 수 있다.
  2. DB에서 동일한 쿼리를 보낼시 DB는 이전에 사용한 쿼리를 재사용할 수 있다.

 


엔티티 삭제

Member member = em.find(Member.class, 1L);
em.remove(member);

엔티티를 삭제하려면 일단 엔티티를 조회 후, remove() 메서드를 통해 삭제하면 된다. 등록/수정과 마찬가지로 일단 삭제 쿼리를 쓰기 지연 SQL 저장소에 저장 후, 트랜잭션 커밋 시점에 삭제 쿼리가 DB에 반영된다.

 


준영속과 병합

엔티티 매니저(entity manager)를 통해 엔티티를 조회/등록/수정/삭제를 하는 것은 영속 상태의 엔티티만이 가능하다. 지금부터는 영속 상태였다가 영속성 컨텍스트에서 분리된 엔티티인 준영속 엔티티에 대해 알아보자.


준영속

 준영속 상태로 만드는 방법은 3가지가 있다.

 

  • em.detach(entity) : 특정 엔티티를 준영속 상태로 만듦
  • em.clear() : 영속성 컨텍스트를 완전 초기화 (영속성 컨텍스트내 모든 엔티티를 준영속 상태로 만듦)
  • em.close() : 영속성 컨텍스트를 종료 (영속성 컨텍스트내 모든 엔티티를 준영속 상태로 만듦 + 영속성 컨텍스트 없어짐)

엔티티가 준영속 상태가 되면 해당 엔티티의 정보가 담긴 1차캐시와 쓰기지연 SQL 저장소에 데이터는 모두 지워진다.

1차 캐시와 쓰기 지연 SQL 저장소 내의 준영속 상태의 엔티티 정보가 모두 지워진다.

당연한 거지만 DB의 데이터를 그대로 남아있다.

 

<준영속 상태의 특징>

  • 거의 비영속 상태에 가깝다. 영속성 컨텍스트가 더 이상 관리하지 않으므로 영속성 컨텍스트의 어떠한 기능도 사용할 수 없다.
  • 식별자 값을 가지고 있다.
  • 지연 로딩을 할 수 없다. (추후 다룰 예정)

 


병합 merge()

병합 메서드(merge())는 준영속 상태의 엔티티를 다시 영속 상태로 만들어 준다. merge() 메서드는 준영속 상태의 엔티티를 매개변수로 받아 영속 상태의 엔티티를 반환한다. 

Member member = em.find("Member.class", 1L);
em.detach(member);	// member는 준영속 상태

Member mergeMember = em.merge(member);
// mergetMember : 영속, member : 준영속

여기서 주의해야 하는 것이 반환되는 엔티티가 영속 상태라는 것이다. 즉, 매개변수로 쓰인 member는 준영속 상태이고, 반환 값으로 받은 mergeMember만이 영속 상태의 엔티티이다.

만약 member 자체를 영속 상태로 바꾸고 싶다면 아래와 같이 작성하면 된다.

member = em.merge(member);

마무리

이번 포스팅에서는 JPA의 핵심 개념인 영속성에 대해 알아보았다. JPA는 영속성이라는 논리적 개념을 통해 개발자가 SQL을 직접 작성하지 않고, 객체 단위에서 코딩을 할 수 있도록 하였다. 이는 데이터 계층과 비즈니스 로직을 완벽히 분리하여 유지보수성을 향상시키고, 애플리케이션의 확장성을 높이게 만든다.

 

앞으로는 본격적으로 JPA의 중요파트인 엔티티와 테이블을 매핑하는 설계파트에 대해 알아보기로 한다.