Spring/JPA

성능 향상을 위한 1차 캐시와 2차 캐시

blockbuddy93 2023. 12. 31. 18:51

네트워크를 통해 데이터베이스에 접근하는 시간비용이 어플리케이션 서버에 내부 메모리 접근하는 시간보다 훨씬 큼.

조회한 데이터를 메모리에 캐시해서 데이터베이스 접근횟수 줄일 수 있다면, 애플리케이션 성능을 획기적으로 개선 가능.

 

1차 캐시

  • 영속성 컨텍스트 내부에는 엔티티를 보관하는 장소가 있는데, 이것을 1차 캐시라함.
  • 트랜잭션을 커밋하거나 플러시를 호출하면 1차 캐시에 있는 엔티티의 변경 내역을 데이터베이스에 동기화

 

1. 1차 캐시의 동작 과정분석

  1. 최초 조회할 때는 1차 캐시에 엔티티가 없으므로
  2. 데이터베이스에서 엔티티를 조회해서 1차 캐시에 보관하고
  3. 1차 캐시에 보관한 결과를 반환.
  4. 이후 같은 엔티티를 조회하면 1차 캐시에 같은 엔티티가 있으므로 데이터베이스를 조회하지 않고 1차 캐시 엔티티 반환

 

2. 1차 캐시의 특징

  • 1차 캐시는 엔티티가 있으면 해당 엔티티를 그대로 반환한다. 따라서 1차 캐시는 객체의 동일성을 보장.
  • 1차 캐시는 기본적으로 영속성 컨텍스트 범위의 캐시(컨테이너환경에서는 트랜잭션 범위의 캐시, OSIV를 적용하면 요청범위의 캐시)

 

이것으로 얻을 수 있는 이점이 많지만, 일반적인 웹 애플리케이션 환경은 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효함.

따라서 애플리케이션 전체로 보면, 데이터베이스 접근횟수를 획기적으로 줄이지 못함.

 

하이버네이트를 포함한 JPA 구현체들은 애플리케이션 범위의 캐시를 지원하는데 이것을 공유캐시 또는 2차 캐시라 함. 이런 2차캐시를 활용하면 애플리케이션 조회 성능을 향상시킬 수 있음.

 

2차 캐시

  • 애플리케이션에서 공유하는 캐시를 JPA는 공유캐시(shared Cache)라 하는데
  • 일반적으로 2차캐시(second level cache, L2 cahce)라라 부름.
  • 2차 캐시는 애플리케이션 범위의 캐시이다. 따라서 애플리케이션을 종료할 때까지 유지됨.
  • 분산캐시나 클러스터링 환경의 캐시는 애플리케이션보다 더 오래 유지될 수도 있음.
  • 2차 캐시를 적용하면 엔티티 매니저를 통해 데이터를 조회할 때 우선 2차캐시에서 찾고 없으면 데이터베이스에서 찾음.
  • 2차 캐시를 적절히 활용하면 데이터베이스 조회횟수를 획기적으로 줄일 수 있음

 

1. 2차 캐시 동작 방식

  1. 영속성 컨텍스트는 엔티티가 필요하면 2차 캐시를 조회한다.
  2. 2차 캐시에 엔티티가 없으면 데이터베이스를 조회해서 
  3. 결과값을 2차 캐시에 보관한다.
  4. 2차 캐시는 자신이 보관하고 있는 엔티티를 복사해서 반환한다.
  5. 2차 캐시에 저장되어있는 엔티티를 조회하면 복사본 데이터를 만들어 반환한다.

2. 2차 캐시의 특징

2차 캐시는 동시성을 극대화하려고 캐시한 객체를 직접 반환하지 않고, 복사본을 만들어서 반환.

만약 캐시한 객체를 그대로 반환하면 여러곳에서 같은 객체를 동시 수정하는 문제가 발생할 수 있다.

이문제를 해결하려면 객체에 락을 걸어야 하는데, 이렇게하면 동시성이 떨어질 수 있다.

락에 비하면 객첼를 복사하는 비용이 저렴하기 때문에 2차 캐시는 원본 대신에 복사본을 반환한다.

 

2차 캐시의 특징은 다음과 같다.

  1. 2차 캐시는 영속성 유닛 범위의 캐시
  2. 2차 캐시는 조회한 객체를 그대로 반환하는 것이 아니라 복사본을 만들어서 반환
  3. 2차 캐시는 데이터베이스 기본 키를 기준으로 캐시하지만 영속성 컨텍스트가 다르면 객체 동일성을 보장하지 않음.

 

3. JPA 2차 캐시 사용

1. 엔티티에 @Cacheable 어노테이션 추가

   import javax.persistence.Cacheable;
   import javax.persistence.Entity;
   import javax.persistence.Id;

   @Entity
   @Cacheable(true) // 2차 캐시 사용
   public class Product {
       @Id
       private Long id;
       private String name;
       private double price;
       
       // getters and setters
   }


2. SharedCacheMode 설정

SharedCacheMode 캐시 모드

  • All : 모든 엔티티를 캐시한다.
  • none : 캐시를 사용하지 않는다.
  • Enable_selective : Cacheable(true)로 설정된 엔티티만 캐시를 적용한다
  • disable_seelcted : 모든 엔티티를 캐시하는데 Cacheable(false)로 명시된 엔티티는 캐시하지 않는다.
  • unsepctified_jpa : 구현체가 정의한 설정을 따른다.
   import javax.persistence.EntityManagerFactory;
   import javax.persistence.Persistence;

   public class JPAUtil {
       private static final EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-persistence-unit");

       public static EntityManagerFactory getEntityManagerFactory() {
           return emf;
       }
   }

 

   <!-- persistence.xml -->
   <persistence-unit name="my-persistence-unit">
       <properties>
           <!-- SharedCacheMode 설정 -->
           <property name="javax.persistence.sharedCache.mode" value="ALL"/>
       </properties>
   </persistence-unit>



3. 캐시 조회, 저장 방식 설정

   import javax.persistence.CacheRetrieveMode;
   import javax.persistence.CacheStoreMode;
   import javax.persistence.EntityManager;
   import javax.persistence.Query;

   public class ProductService {
       public Product getProductById(Long id) {
           EntityManager em = JPAUtil.getEntityManagerFactory().createEntityManager();
           em.getTransaction().begin();

           // 캐시 조회, 저장 방식 설정
           Query query = em.createQuery("SELECT p FROM Product p WHERE p.id = :id")
                           .setParameter("id", id)
                           .setHint("javax.persistence.cache.retrieveMode", CacheRetrieveMode.USE)
                           .setHint("javax.persistence.cache.storeMode", CacheStoreMode.BYPASS);

           Product product = (Product) query.getSingleResult();

           em.getTransaction().commit();
           em.close();

           return product;
       }
   }



4. 하이버네이트와 EHCACHE(분산 캐시)적용

<!-- pom.xml -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-ehcache</artifactId>
    <version>5.5.7.Final</version>
</dependency>
<!-- persistence.xml -->
<persistence-unit name="my-persistence-unit">
    <properties>
        <!-- 하이버네이트 캐시 설정 -->
        <property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>
        <property name="hibernate.cache.use_second_level_cache" value="true"/>
        <property name="hibernate.cache.use_query_cache" value="true"/>
    </properties>
</persistence-unit>

 

하이버네이트가 지원하는 캐시는 크게 3가지가 있음.

  1. 엔티티 캐시 : 엔티티 단위로 캐시한다. 식별자 단윈로 엔티티를 조회하거나 컬렉션이 아닌 연관된엔티티를 로딩할떄 사용.
  2. 컬렉션 캐시 : 엔티티와 관련된 컬렉션을 캐시한다. 컬렉션이 엔티티를 다고있으면 식별자 값만 캐시.
  3. 쿼리 캐시 : 쿼리와 파라메터 정보를 키로 사용해서 캐시한다. 결과가 엔티티면 식별자 값만 캐시.

 

JPA 표준에는 엔티티 캐시만 적용되어있다.