개인 프로젝트에 2차 캐시를 적용하면서 데이터 최적화를 시도한 기록을 기록합니다.
🔍 자바의 표준 캐시 (JCache)
JCache는 자바 애플리케이션을 위한 표준화된 캐싱과 매커니즘을 제공합니다. 캐싱 표준으로 설계되었기 때문에, 벤더 중립적입니다. 즉, JCache API를 사용하여 애플리케이션을 개발하면 다른 캐시 API로의 전환이 용이합니다.
JCache는 다음과 같은 표준 인터페이스를 제공하고 공급자를 제공받음으로써 벤더 중립적인 개발을 가능하게 도와줍니다.
- CacheManager - 캐시 설정, 구성 및 종료를 담당하는 인터페이스
- CachingProvider
- CacheManager의 생명 주기를 생성하고 관리하는 인터페이스
- 대표적인 공급자로 Ehcache, Hazelcast, Caffeine, Redis 등이 있다.
🔍 Ehcache와 JCache
JCache를 청사진이라고 하면 Ehcache는 청사진을 기반으로 한 건축물이라고 할 수 있다. 즉, JCache의 구현체가 Ehcache인 것이다.
Ehcache
- EhCache 3.x부터 JCache를 완벽하게 호환한다.
- hibernate와 CacheProvider(
ehcache
) 사이의 중간 역할을 담당한다. - Hibernate에서 JCache를 사용하고 싶다면 다음 의존성을 추가해주면 된다.
implementation("org.ehcache:ehcache:3.10.0:jakarta")
implementation("org.hibernate.orm:hibernate-jcache:6.4.0.Final")
Ehcache vs Redis
최근에는 Redis를 더 많이 사용하는 것 같다. 필자는 다음 글을 참고하고 Ehcache로 정하였다.
https://stackoverflow.com/questions/33123633/redis-or-ehcache
[Redis or Ehcache?
Which is better suited for the following environment: Persistence not a compulsion. Multiple servers (with Ehcache some cache sync must be required). Infrequent writes and frequent reads. Relatively
stackoverflow.com](https://stackoverflow.com/questions/33123633/redis-or-ehcache)
요약하면 다음과 같다.
- Redis는 공유 데이터 구조이고, Ehcache는 직렬화된 데이터 개체를 저장하는 메모리 블록이다.
- 만약 Java만 사용하는 프로젝트라면 Ehcache가 쉬운 선택이다.
🔍 실습
springboot(3.1.5), kotlin, Jpa 사용
yml 설정
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 폴더 밑에 생성하면 된다.
<config
xmlns='http://www.ehcache.org/v3'
xmlns:jsr107='http://www.ehcache.org/v3/jsr107'>
<service>
<jsr107:defaults default-template="default">
<jsr107:cache name="category" template="categoryCache"/>
</jsr107:defaults>
</service>
<cache-template name="default">
<expiry>
<ttl unit="days">90</ttl>
</expiry>
<heap unit="entries">10</heap>
</cache-template>
<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을 사용하여 캐시할 데이터라고 표시한다.
@org.hibernate.annotations.Cache.Cacheable
@Entity
class ProductCategory(
@Column(unique = true, nullable = false)
val categoryName: String,
) {
...
}
@Entity
class Product(
@ManyToOne
@JoinColumn var category: ProductCategory,
) {
....
}
🔍 결과
[1차 조회]
- Product를 조회하면 지연 로딩을 설정하지 않았기 때문에 ProductCategory 쿼리도 같이 나가게 된다. 빨간색 쿼리를 보면 JDBC 문장(쿼리)이 2개가 실행된 것을 확인할 수 있다.
- 2차 캐시에 정보를 찾을 수 없어(miss), 이를 저장한다.(put)
[2차 조회]
- 2차 캐시에서 필요한 데이터를 찾았다.(cache hit)
- 쿼리문이 하나만 나갔음을 확인할 수 있다.
- 속도가 조금 빨라졌다.
[더 많은 데이터 조회]
2000개의 데이터 조회
3000개의 데이터 조회
이전 Fetch-Join이랑 데이터 조회를 비교했을 때, Fetich-Join방식이 2차 캐시에 비해 결과를 반환하는 시간 변동 폭이 훨씬 컸다.
https://brightmango.tistory.com/entry/프로젝트-기록-fetch-join을-이용해-쿼리-최적화하기?category=1079515
결론
- JCache를 이용하면 벤더 중립적인 코드 작성이 가능하다.
- 자주 사용되지만 잘 바뀌지 않는 데이터에 대해서 캐싱을 함으로써 최적화할 수 있다
[참고]
https://www.baeldung.com/hibernate-second-level-cache
https://thorben-janssen.com/hibernate-ehcache/#using-ehcache-3x](https://thorben-janssen.com/hibernate-ehcache/#using-ehcache-3x)