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
) 사이의 중간 역할을 담당합니다.
✏️ 특징
- 속도가 빠르며 경량 Cache이다.
- JSR 표준을 지원한다.
Redis와 Ehcache중에 어떤거를 사용할 지 고민했는데 다음 글을 참고하고 Ehcache로 정하였습니다.
- 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
- Hibernate는 org.hibernate.cache.spi.RegionFactory 인터페이스를 통해 높은 추상화를 제공하므로 해당 구현체만 제공하면 됩니다.
- 공급자로 JSR107(JCache) 표준을 구현한 EhcacheCachingProvider를 선택합니다.
- 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차 조회
- 2차 캐시에서 필요한 데이터를 찾았습니다 (cache hit).
- 쿼리문이 하나만 나갔음을 확인할 수 있으며 속도가 조금 빨라졌습니다.
✅ 더 많은 데이터 조회
- 2000개 조회
- 3000개 조회
생각보다 성능이 많이 좋아지지는 않았습니다. 영속성 컨텍스트의 1차 캐시가 동작하기 때문에 2차 캐시의 효과를 크게 못 본 것 같습니다. 2차 캐시에 적합한 데이터 구조를 만나면 성능 측정을 다시 해보겠습니다.
📋 결론
- 자바 표준 캐시인 JCache를 사용하면 벤더 중립적인 개발이 가능합니다.
- 캐싱 설정을 효과적으로 구성하면 데이터 접근 횟수와 시간을 크게 줄일 수 있으며, 애플리케이션의 전체적인 성능 향상을 기대할 수 있습니다.