Post

JPA 2차 캐시를 이용하여 성능 향상시키기 (feat. JCache)

데이터베이스와의 지속적인 상호작용은 애플리케이션의 성능을 저하시킬 수 있습니다. 따라서 자주 사용되지만 잘 변하지 않는 데이터를 캐싱하여 성능을 향상시킬 수 있습니다. 이번에는 자바 표준 캐시 API인 JCache를 사용하여 데이터베이스 접근을 최소화하고 성능을 향상시키는 방법을 알아보겠습니다.

🔍 JCache(JSR-107)

JCache는 자바 애플리케이션을 위한 표준화된 캐싱과 매커니즘을 제공하는 자바 표준 API입니다. 이 API는 캐싱 표준으로 설계되었으며 벤더 중립적입니다. 즉, JCache API를 사용하여 애플리케이션을 개발하면 다른 캐시 API로의 전환이 용이합니다. JCache는 다음과 같은 표준 인터페이스를 제공하고 공급자를 제공받음으로써 벤더 중립적인 개발이 가능하게 합니다.

✏️ CacheManager

  • 캐시 설정, 구성, 종료하는 중요한 인터페이스

✏️ CachingProvider

  • CacheManager의 생명 주기를 생성하고 관리하는 인터페이스
  • 대표적인 공급자로 Ehcache, Hazelcast, Caffeine, Redis 등이 있습니다.

🔍 Ehcache와 JCache

JCache를 청사진이라고 하면 Ehcache는 청사진을 기반으로 한 건축물이라고 할 수 있습니다. 즉, JCache의 구현체가 ehcache입니다. (Hibernate가 JCache를 지원하는데 왜 ehcache가 필요하지라는 생각을 했었는데, JCache와 구현체를 쉽게 사용할 수 있도록 도와줄 뿐 구현체를 제공하지는 않기 때문에 ehcache가 필요합니다.)

✏️ Ehcache

  • EhCache 3.x 부터 JCache를 완벽하게 호환합니다.
  • JCache를 사용한다면 hibernate-jcache 의존성을 추가해주면 됩니다.
1
2
implementation("org.ehcache:ehcache:3.10.0:jakarta") 
implementation("org.hibernate.orm:hibernate-jcache:6.4.0.Final")
  • Ehcache는 hibernate와 CacheProvider(ehcache) 사이의 중간 역할을 담당합니다.

1번.png

✏️ 특징

  1. 속도가 빠르며 경량 Cache이다.
  2. JSR 표준을 지원한다.

Redis와 Ehcache중에 어떤거를 사용할 지 고민했는데 다음 글을 참고하고 Ehcache로 정하였습니다.

Ehcache vs Redis

  • Redis는 공유 데이터 구조이기 때문에 다양한 언어(Python, PHP) 등을 사용하는 분산 환경에서 유리합니다.
  • Ehcache는 Redis보다 간단하기 때문에 단일 언어(Java)에서 사용하기에 유리합니다.

🔍 실습

이제 실제로 JCache를 사용하여 성능을 향상시키는 방법을 알아보겠습니다. 이 실습에서는 Spring Boot(3.1.5), Kotlin 및 JPA를 사용합니다.

✏️ yml설정

1
2
3
4
5
6
7
8
9
10
jpa:  
  properties:  
    hibernate:  
      generate_statistics: true  ## 캐싱 정보를 확인할 수 있다.
      javax.cache:  
        provider: org.ehcache.jsr107.EhcacheCachingProvider   ## --- 2
        uri: ehcache.xml  ## --- 3
      cache:  
        use_second_level_cache: true   ## 2차 캐시 활성화
        region.factory_class: jcache   ## --- 1
  1. Hibernate는 org.hibernate.cache.spi.RegionFactory 인터페이스를 통해 높은 추상화를 제공하므로 해당 구현체만 제공하면 됩니다.
  2. 공급자로 JSR107(JCache) 표준을 구현한 EhcacheCachingProvider를 선택합니다.
  3. Hibernate는 각 엔티티 클래스를 2차 캐시의 개별 영역에 저장하기 때문에 캐시할 데이터에 대한 설정을 해주어야 합니다.

✏️ ehcache.xml 설정

  • 이 파일은 resources 폴더 밑에 생성하면 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<config  
    xmlns='http://www.ehcache.org/v3'  
    xmlns:jsr107='http://www.ehcache.org/v3/jsr107'>  
  
    <service>        
        <!--  JSR-107 캐시의 기본 설정을 지정한다.  -->
      
        <!-- 기본 템플릿을 설정한다. 기본적인 TTL, 최대 엔트리 수 등을 설정할 수 있다.  -->
        <jsr107:defaults default-template="default">
          
        <!--  특정 캐시에 대한 설정을 지정한다.  -->  
        <jsr107:cache name="category" template="categoryCache"/>  
        </jsr107:defaults>  
    </service>  
    <cache-template name="default">  
      <expiry>            
        <!-- TTL 시간을 지정한다 -->
        <ttl unit="days">90</ttl>  
      </expiry>        
      <!--  캐시의 최대 엔트리 수를 지정한다.  -->
      <heap unit="entries">10</heap>  
    </cache-template>  
    <!-- key-type과 value-type을 지정한다 -->
    <cache-template name="categoryCache">  
        <key-type>java.lang.Long</key-type>  
        <value-type>com.example.commerce.entity.ProductCategory</value-type>  
    </cache-template>  
</config>

✏️ 캐시할 데이터 정하기

Product와 ProductCategory 구조의 엔티티에서 ProductCategory는 삽입, 수정, 삭제 등이 이루어지지 않기 때문에 캐싱하기로 결정했습니다. 데이터를 5개 조회하였으며 @Cacheable 어노테이션을 사용하여 캐시할 데이터로 표시해주었습니다.

1
2
3
4
5
6
7
8
@org.hibernate.annotations.Cache.Cacheable  
@Entity  
class ProductCategory(  
    @Column(unique = true, nullable = false)  
    val categoryName: String,  
) {  
  ...
}
1
2
3
4
5
6
7
8
@Entity  
class Product(  
    @ManyToOne  
    @JoinColumn    
    var category: ProductCategory,    
    ) {  
    ....
}

✅ 1차 조회

  • Product를 조회하면 지연 로딩을 설정하지 않았기 때문에 ProductCategory 쿼리도 같이 나갑니다.
  • 2차 캐시에 정보를 찾을 수 없어 (miss) 이를 저장합니다 (put).

2차_캐시_첫_조회.png

✅ 2차 조회

  • 2차 캐시에서 필요한 데이터를 찾았습니다 (cache hit).
  • 쿼리문이 하나만 나갔음을 확인할 수 있으며 속도가 조금 빨라졌습니다.

2차_캐시_첫_조회.png

✅ 더 많은 데이터 조회

  • 2000개 조회

2차_캐시_첫_조회.png

  • 3000개 조회

2차_캐시_첫_조회.png

생각보다 성능이 많이 좋아지지는 않았습니다. 영속성 컨텍스트의 1차 캐시가 동작하기 때문에 2차 캐시의 효과를 크게 못 본 것 같습니다. 2차 캐시에 적합한 데이터 구조를 만나면 성능 측정을 다시 해보겠습니다.

📋 결론

  • 자바 표준 캐시인 JCache를 사용하면 벤더 중립적인 개발이 가능합니다.
  • 캐싱 설정을 효과적으로 구성하면 데이터 접근 횟수와 시간을 크게 줄일 수 있으며, 애플리케이션의 전체적인 성능 향상을 기대할 수 있습니다.

참고

baeldung
hibernate-ehcache
springboot-cache

This post is licensed under CC BY 4.0 by the author.

© bear-su. Some rights reserved.