관리 메뉴

너와 나의 스토리

[JPA] Hibernate Cache란? / Hibernate cache로 인한 문제 해결 방법 본문

개발

[JPA] Hibernate Cache란? / Hibernate cache로 인한 문제 해결 방법

노는게제일좋아! 2023. 6. 13. 00:31
반응형

Hibernates를 ORM 프레임워크로 사용하는 애플리케이션의 성능을 향상하기 위해 자주 접근하는 데이터를 메모리에 저장한다. Hibernates는 두 가지 수준의 캐시를 지원한다. 

* ORM(Object Relational Mapping): 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑해주는 것

 

출처: Hibernate 5.5 Documentation:  Caching

 

1차 캐시(First-Level Cahce) = 세션 캐시(Session Cache)

  • Hibernate는 세션 수준의 캐시를 사용한다. 특정 세션에서 현재 사용 중인 데이터를 저장하고 있다.
  • 즉, persistent 상태인 모든 객체를 캐싱함.
  • 영속성 컨텍스트(Persistence Context)에 데이터 저장. 
    • 영속성 컨텍스트: 엔티티 객체의 상태를 추적하고, 디비와의 상호작용을 관리하는 역할.
  • 세션에서 어떤 엔티티를 첫 번째로 불러오거나 업데이트하면 세션 수준 캐시(1차 캐시)에 저장된다. 
  • 같은 세션에서 같은 엔티티에 가져오는 후속 요청이 들어오면 캐시에서 데이터를 반환해 줍니다. 
  • 세션 수준 캐시는 기본적으로 활성화되어 있으며, 비활성화시킬 수 없다.

 

2차 캐시(Second-Level Cache) = 쿼리 캐시(Query Cache)

  • Hibernate는 여러 세션 간에 공유되는 캐시를 가진다.
  • 동일한 쿼리를 실행할 때 캐싱한 값을 반환

 

 

Hibernate cache로 인한 문제 발생

ZuulFilter를 쓰다가 Spring Cloud Gateway로 넘어오면서 ProxyExchange를 사용해 기존 Zuul filter를 대체하였다. 

public ResponseEntity<byte[]> proxy(ProxyExchange<byte[]> exchange, JsonNode body) {
    saveDataA();
    return exchange.uri(routingUrl).post(responseEntity -> {
        findDataA();
        return responseEntity;
    });
}
  • 위 예시 코드A라는 데이터를 저장(1)하고 routing한 후, 라우팅한 서버로부터 응답이 오면 A라는 데이터를 다시 디비에서 조회해서(2) 처리하는 로직이다.
  • 이때, (1)번 작업 후 (2)번 작업이 수행되기 전에 다른 api 콜이 들어와 updateDataA()를 수행한 경우 문제가 발생하였다.
  • 중간에 updateDataA()를 호출되었으면 (3)에서 A를 조회할 때, 업데이트된 값이 반환되어야 하는데 처음 저장한 A 값 그대로 반환되었다. 
  • 원인: 위 코드는 모두 한 세션에서 작업이 이루어지기 때문에 1차 캐시에 저장된 값이 반환되는 것이었다.
    • 조회 쿼리는 처음 한번만 db에 접근하며, 그다음부터는 캐시 값만 사용함.
  • 해결 방법 1: 데이터를 조회할 때, cache 업데이트
    • entityManager.clear()을 하면 영속성 컨텍스트의 모든 엔티티를 제거할 수 있다. 
      • 이로 인해 영속성 컨텍스트가 관리하던 모든 엔티티의 상태가 비영속 상태로 변경되어 현재 변경 사항이 모두 롤백된다.
    • 아래 예제처럼 entityManager.refresh(entity)를 호출하면 특정 엔티티를 데이터베이스로부터 다시 로드하여 영속성 컨텍스트의 상태를 업데이트할 수 있다.
    • entityManager.detach(entity)를 호출하면 entity를 영속성 컨텍스트에서 분리시킬 수 있다.
      • 분리된 엔티티는 영속성 컨텍스트의 관리를 받지 않게 된다. 1차 캐시에서도 제거된다. 
      • 하지만, 이후에 해당 엔티티를 수정하거나 업데이트하려면 entityManager.merge(entity)를 사용해야 한다.
    • entityManager.merge(entity)를 호출하면 해당 엔티티가 다시 영속성 컨텍스트에 병합된다.
@PersistenceContext
private EntityManager entityManager;

@Transactional
public DataA find(DataAId dataAId) {
    val entity = repository.findById(dataAId.getValue())
            .orElseThrow(() -> new DataANotFoundException(dataAId));
    entityManager.refresh(entity);
    return entity.asModel();
}
  • 해결 방법 2: 영속성 컨텍스트 생존 범위 지정
    • Spring은 요청이 시작될 때 새로운 Hibernate 세션을 연다. 세션이 반드시 데이터베이스에 연결되어 있지는 않는다.
    • open-in-view 옵션이 true인 경우, 영속성 컨텍스트가 요청의시작부터 응답의 종료까지 유지된다. 즉, 영속성 컨텍스트가 틔랜잭션 범위를 벗어나도 유지된다. (기본적으로 이렇게 설정되어 있음)
    • 이 옵션을 false로 설정하면, 영속성 컨텍스트는 데이터베이스 트랜잭션 범위 내에서만 유지되게 된다.
    • 설정 방법:
<application.yml>
spring:
   jpa:
      open-in-view: false  // true가 디폴트

 

위 로직을 Zuul Filter로 구현했을 때에는 문제가 발생하지 않았던 이유

  • 스프링 필터의 경우 각각의 필터가 하나의 독립적인 요청으로 인식되어 세션이 각각 생성된다.
  • 즉, 각각 다른 영속성 컨텍스트를 가지므로 이러한 문제가 발생하지 않았던 것.

 

 

출처

- GeeksForGeeks

- Hibernate 5.5 Documentation: Caching 

- https://www.baeldung.com/spring-open-session-in-view

반응형
Comments