본문 바로가기

Spring/JPA

JPA 심화트랙(2) - Persistence Context(영속성 컨텍스트)

정리: Spring Data Repository 객체의 save(Entity) 메서드를 가급적이면 반환된 Entity 객체를 사용하는 게 좋다.

JPQL 쿼리는 PK를 사용하지 않는 repository JPA 문을 사용 시에 flush()가 사용되므로, 1차 캐시와 db 간에 데이터차이가 발생하는 것에 주의하자. clear()를 사용하여 이 불일치를 해결하면 좋다.

 

영속성 컨텍스트는 JPA에서 제공되는 논리적 구조로, 영속 상태의 엔티티를 관리하는 데 사용된다.

 

JPA는 영속성 콘텍스트를 활용하여 주요 캐시, 식별성 보장, 쓰기 지연, 그리고 변경 감지(더티 체킹)와 같은 다양한 기능을 제공한다. (이 기능은 JPA의 무결성을 보호하는 데 목적이 있지만, 성능 저하의 원인 중 하나가 될 수도 있다.)

 

영속성 컨텍스트에 의해서 관리되는 Entity의 생명주기

  • New/Transient(비영속 상태) : 영속성 컨텍스트와 아예 관련 없는 상태
  • Managed(영속 상태) : 영속성 컨텍스트에 저장되어 관리를 받고 있는 상태
  • Detached(준영속 상태) : Managed 상태였던 Entity 가 영속성 콘텍스트에서 분리된 상태
  • Removed(삭제 상태) : 삭제된 상태
  • 준영속 상태와 flush(), merge()에 대해서는 반드시 내부 동작방식을 이해하고 넘어가야 한다.

영속성 콘텍스트는 기본적으로 @Transactional의 라이프사이클과 함께 동작한다.

트랜잭션이 시작될 때 영속성 콘텍스트가 생겼다가, 트랜잭션이 종료될 때 영속성 콘텍스트가 사라진다고 생각하면 편할 것 같다. (OSIV라는 예외가 존재한다.)

 

EntityManager은 @PersistenceContext를 통해서 의존성 주입이 가능하고, 아래 요청 등을 통해 db에 작업을 수행할 수 있다.

  • find(Enitty 클래스명, PK)
    • 해당하는 Entity 객체를 Id(PK)를 통해 조회한다.
    • 1차 캐시를 이용해 성능향상 및 동일성 보장을 제공한다고 한다.
  • createQuery(JPQL)
    • JPQL을 이용해 SQL 쿼리를 실행(1차 캐시 확인 없이 바로 db에 쿼리를 날린다)
  • persist(Entity)
    • 비영속 상태인 Entity 객체를 영속 상태인 Entity 객체를 영속 상태로 만든다.
    • 쓰기 지연 SQL 저장소에 INSERT 쿼리를 생성한다.
  • merge(Entity)
    • 준영속 상태인 Entity 객체를 영속 상태로 만든다.
    • 1차 캐시에 해당 Entity 객체에 대한 정보가 없으면 데이터베이스에 SELECT 쿼리를 날려 1차 캐시에 데이터를 생성한다.
    • 1차 캐시에 생성된 Entity 객체와 merge(Entity) 요청시 전달한 Entity 객체를 합치고, 합친 새로운 객체를 1차 캐시에 저장한 후 반환하게 된다.
    • 현재 사용 중인 save 함수를 보면 여기에서 persist와 merge의 차이에 대해서 알 수 있다.(해당 entity가 1차 캐시에 존재하면 merge, 아니면 persist를 사용한다.)
@Transactional
@Override
public <S extends T> S save(S entity) {
	Assert.notNull(entity, "Entity must not be null");
	
	if (entityInformation.isNew(entity)) {
		entityManager.persist(entity);
		return entity; // 여기서도 persist(Entity) 가 동일한 Entity 객체를 반환한다는 사실을 확인할 수 있다.
	} else {
		return entityManager.merge(entity);
	}
}
  • remove(Entity)
    • 영속 상태의 Entity를 영속성 콘텍스트에서 제거한다.
    • 쓰기 지연 SQL 저장소에서 DELETE 쿼리를 생성한다.
  • flush()
    • 쓰기 지연 SQL 저장소에 보관되어진 SQL 쿼리문을 실제 db에 날려 실행한다.
    • 쓰기 지연 SQL 저장소를 통해 SQL 쿼리를 최적화할 수 있는 만큼 주의해야 할 부분도 분명히 존재한다.
    • 3가지 방법으로 호출될 수 있다.
      • 직접호출:
        •  EntityManager 객체의 flush() 메서드, Spring Data Repository 객체의 saveAndFlush() 메서드 직접 호출
      • 자동 호출 2가지:
        • Transaction 이 Commit 될 때 자동으로 호출된다
        • JPQL 쿼리를  이용한 요청 시 자동으로 호출된다.
    • 위 3가지 방법 중 3번째 "JPQL 쿼리를 이용한 요청 시 자동으로 호출”에에 대해서 확인하고 넘어가야 할 부분이 존재.
      • 이 부분에서 1차 캐시와 db 간의 불일치가 발생할 수 있으니 주의해야 한다고 한다.(트러블슈팅 부분에서 추후 언급)
      • PK로 조회하지 않는 경우 모두 JPQL 쿼리가 발생한다.
      • JPQL 쿼리를 날리기 전에 flush()를 하는 이유
        • JPQL 쿼리는 1차 캐시를 보지 않고 바로 데이터베이스에 SQL 쿼리를 날린다
        • flush()를 해주지 않으면 1차 캐시와 실제 데이터베이스의 정보가 달라지게 된다.
        • 이 상태에서 1차 캐시의 정보를 보지 않고 db의 정보를 조회하면 데이터 정합성이 깨지게 된다.
        • 1차 캐시를 포함한 영속성 콘텍스트를 비우고 싶다면 clear()를 사용한다.
  • clear()
    • 영속성 컨텍스트를 비운다.
    • 영속성 콘텍스트에 의해서 관리되고 있던 Entity 객체들이 모두 준영속(Detached) 상태가 된다.
    • JPQL로 SQL 쿼리를 요청하게 될 경우 1차캐시와 데이터베이스에 저장된 정보의 불일치가 일어날 수 있다. 이때 1차 캐시를 비움으로써 이 데이터 불일치를 해결하기 위해 사용된다.

준영속 

  • 준영속(Detached) 상태는 3가지 방식으로 생성된다.
    • detach(): 영속 상태인 Entity를 준영속 상태로 만든다.
    • clear(): 모든 영속 상태인 Entity들을 준영속 상태로 만든다.
    • close(): 영속성 콘텍스트가 닫힌다. 즉, 모든 영속 상태인 Entity 가 준영속 상태가 된다.
  • 준영속 상태인 Entity는 영속성 콘텍스트의 관리를 받지 않는 상태기 때문에 Lazy Loading과 같은 작업을 하게 되면 예외(LazyInitalizedException)가 발상한다고 한다.
  • 그렇기에 merge(Entity)를 사용하여 준영속 상태의 Entity를 다시 영속 상태로 변환하여야 한다.
    1. 파라미터로 전달한 Entity 객체의 @Id(Pk)가 1차 캐시에 존재하는 Key 인지 확인한다.
    2. 1차 캐시에 존재하지 않으면 데이터베이스에 SELECT 쿼리를 날려 1차캐시에 추가한다. (이미 1차캐시에 존재한다면 별도의 SELECT 쿼리를 날리지 않는다)
    3. 1차캐시에 저장된 Entity 객체와 파라미터로 전달된 Entity 객체를 합친다.
    4. 합친 결과로 새로운 Entity 객체를 생성하고 1차캐시에 저장한 후 반환한다.
  • merge(Entity)에서 반환된 Entity 객체는 파라미터로 전달한 Entity 객체와 항상 다른 객체다. 
  • 그렇기에 merge(Entity)로 인한 버그가 생성될 수 있으며, 항상 save(Entity)에서 반환된 Entity 객체를 활용하는 것이 안전한 코드를 작성할 수 있는 좋은 습관일 것이라고 한다.