[Spring Boot] Simple Cache, EhCache(v3.10.8) 간단 테스트 해보기
캐시란?
- 데이터나 값을 미리 복사해놓는 임시 저장소
- 시스템 성능을 향상시키기 위한 메커니즘
- 캐시에 데이터를 저장하고 엑세스하는 프로세스이다
캐시를 사용해야 하는 이유
① 데이터 접근이 빠르고 비용이 저렴
② 애플리케이션 성능이 향상됨
③ 응답이 빠름
④ 메모리에 데이터 접근하는게 DB에서 가져오는 것보다 항상 빠름
⑤ 비용이 많은 백엔드 요청이 줄어듦
캐시에 데이터를 미리 복사해 놓음으로써 처리/접근 시간(비용) 없이 빠른 속도로 데이터 접근할 수 있다
언제 사용
- 자주 변경되지 않는 데이터
- 원본 데이터에 접근/처리 시간이 오래 걸리는 경우
캐싱 종류
① 인메모리 캐싱 (ex. Redis)
② 데이터베이스 캐싱 (ex. hibernate 1차 캐시)
③ 웹 서버 캐싱
- HTTP Cache : 브라우저/프록시/웹 서버 캐시
- Application Cache : 인 메모리/글로벌 캐시
④ CDN 캐싱
Spring Caching
스프링에서 사용가능한 캐시는 아래와 같다
- JCache (JSR-107) : standard caching API for Java
- EhCache : 오픈 소스 Java 기반 캐시, EhCache3 부터 JCache 표준 정식 지원
- Hazelcast
- Infinispan
- Couchbase
- Redis : JCache 표준 지원x, Spring Data Redis 통해 쉽게 통합 가능
- Caffeine : 고성능 Java 기반 캐시, JCache 표준 지원
- Simple
- Simple 캐시의 경우 SimpleCacheManager 뜻함, Spring에서 기본 제공하고, 특별한 캐시 구현체 필요 없이 간단한 애플리케이션이나 테스트 용도로 사용됨
- Spring Boot에서는 특별한 캐시 구현체 없는 경우 ConcurrentMapCacheManager 빈을 기본 생성
- EhCache 추가할 경우 JCacheCacheManager 빈을 기본 생성
참고. 슈퍼마켓에서 우유를 사면서 캐싱을 알아보자
https://rinae.dev/posts/web-caching-explained-by-buying-milk-kr/
[번역] 슈퍼마켓에서 우유를 사면서 웹 캐싱(Web Caching)을 알아봅시다
웹 개발을 할 때 기본 중의 기본이지만 대충 알고 넘어갔던 캐싱을 아주 깔끔하게 설명한 글
rinae.dev
프로젝트 버전
- spring boot 3.2.6
- jdk 17
- EhCache 3.10.8 (2024.07.22 기준 최신)
Simple Cache
특별한 캐시 구현체를 사용하지 않을 경우 ConcurrentMapCacheManager 자동 생성하여 사용할 수 있다
참고. 전체 코드
helloCaching/src/main/java/hello/caching/HelloCachingApplication.java at simple-cache · ljw1126/helloCaching
spring boot cache 학습. Contribute to ljw1126/helloCaching development by creating an account on GitHub.
github.com
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.6'
id 'io.spring.dependency-management' version '1.1.5'
}
group = 'caching'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-cache'
}
tasks.named('test') {
useJUnitPlatform()
}
@EnableCaching 애노테이션 추가시 기본적으로 설정된다
@EnableCaching
@SpringBootApplication
public class HelloCachingApplication {
public static void main(String[] args) {
SpringApplication.run(HelloCachingApplication.class, args);
}
}
애플리케이션 실행시 SimpleCacheConfiguration이 동작함을 확인할 수 있었다
해당 Bean이 주입되는지 확인해본다
@Slf4j
@Component
public class CacheManagerCheck implements CommandLineRunner {
private final CacheManager cacheManager;
public CacheManagerCheck(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public void run(String... args) throws Exception {
log.info("================================\n Using cache manager :" + this.cacheManager.getClass().getName());
}
}
콘솔 출력
==================================
Using cache manager :org.springframework.cache.concurrent.ConcurrentMapCacheManager
SpringBoot + Ehcache 기본 예제 및 소개 - 기억보단 기록을 기술 블로그에 있는 동일한 예제 코드를 활용해서 캐시 테스트를 진행해본다 (전체 코드는 깃 허브 참고)
간단하게 RestController를 생성하고 MemberRepository를 호출하도록 기능 구현
① 캐시를 사용하지 않는 경우 : getNoCacheMember(..)
② 캐시를 사용하는 경우 : getCacheMember(..)
③ 캐시 만료 요청 : refrech(..)
@Slf4j
@RestController
public class MemberController {
private final MemberRepository memberRepository;
public MemberController(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@GetMapping("/member/nocache/{name}")
public ResponseEntity<Member> getNoCacheMember(@PathVariable("name") String name) {
long start = System.currentTimeMillis();
Member member = memberRepository.findByNameNoCache(name);
long end = System.currentTimeMillis();
log.info("{}의 NoCache 수행시간 : {} ", name, (end - start));
return ResponseEntity.ok(member);
}
@GetMapping("/member/cache/{name}")
public ResponseEntity<Member> getCacheMember(@PathVariable("name") String name) {
long start = System.currentTimeMillis();
Member member = memberRepository.findByNameCache(name);
long end = System.currentTimeMillis();
log.info("{}의 Cache 수행시간 : {} ", name, (end - start));
return ResponseEntity.ok(member);
}
@GetMapping("/member/refresh/{name}")
public ResponseEntity<String> refresh(@PathVariable("name") String name) {
memberRepository.refresh(name);
return ResponseEntity.ok("Cache Clear");
}
}
public interface MemberRepository {
Member findByNameNoCache(String name);
Member findByNameCache(String name);
void refresh(String name);
}
Thread.sleep(..)을 사용하여 2초 지연 시간을 부여한다
@Slf4j
@Repository
public class MemberRepositoryImpl implements MemberRepository {
@Override
public Member findByNameNoCache(String name) {
slowQuery(2000);
return new Member(1L, name + "@gmail.com", name);
}
@Override
@Cacheable(value = "findMemberCache", key = "#name", unless = "#result == null")
public Member findByNameCache(String name) {
slowQuery(2000);
return new Member(1L, name + "@gmail.com", name);
}
@Override
@CacheEvict(value = "findMemberCache", key = "#name")
public void refresh(String name) {
log.info("{}의 Cache Clear!", name);
}
private void slowQuery(long seconds) {
try {
Thread.sleep(seconds);
} catch (InterruptedException e) {
throw new IllegalArgumentException(e);
}
}
}
캐시를 사용하지 않는 API의 경우 매 호출시 2초가 걸린다
캐시를 사용하는 API의 경우 첫 호출시 2초 시간이 걸리고 이후에는 캐싱 데이터 반환하여 빠르게 응답함을 확인할 수 있었다
EhCache
- Java 기반 오픈 소스
- EhCache 3버전 부터 JSR-107 자바 표준 인터페이스를 지원한다
- 캐시에 대한 자바 표준 인터페이스를 JCache 라고 한다
- EhCache 설정은 Programmatic 방식과 xml 두 가지 방식으로 설정할 수 있다 (공식문서 참고)
참고. 전체 코드
helloCaching/src/main/java/hello/caching/HelloCachingApplication.java at ehcache-test · ljw1126/helloCaching
spring boot cache 학습. Contribute to ljw1126/helloCaching development by creating an account on GitHub.
github.com
1. 의존성 문제 해결
처음 EhCache v3.8.1 로 했는데 아래 의존성 문제가 발생했다
- java.lang.NoClassDefFoundError: javax/xml/bind/JAXBElement
- Caused by: java.lang.ClassNotFoundException: com.sun.xml.internal.bind.v2.ContextFactory
- Caused by: java.lang.ClassNotFoundException: javax.xml.bind.ValidationEventHandler
Mkyong.com 에서 com.sun.xml.internal.bind.v2.ContextFactory가 Java 9에서 Deprecated 되었고,
Java11부터 완전히 삭제되었다고 jakarata.* dependencies 수정할 것을 가이드하였지만,
패키지가 맞지 않아 원하는 해답이 되진 않았다
깃허브 이슈를 찾아보니 같은 이슈를 발견 (해결방안은 없다)
https://github.com/ehcache/ehcache3/issues/2797
ehcache 3.8.1 - jaxb dependency issue · Issue #2797 · ehcache/ehcache3
Hi, Previously I asked the same same question in other platforms but since I could not receive an answer: https://stackoverflow.com/questions/62774018/ehcache-3-8-1-jaxb-dependency-issue https://gr...
github.com
해결
- JAXB 관련 의존성을 2.x버전으로 맞추니 정상 동작한다
참고. https://groups.google.com/g/ehcache-users/c/U1bO6QArswQ
ehcache 3.10.0 dependency download issue on Maven
a minor potential inconvenience when upgrading from 3.9.9 to 3.10.0: In our build, dependencies are downloaded with dependency:go-offline by the CircleCI Maven ORB. "mvn dependency:go-offline" works fine for 3.9.9, but on 3.10.0, it's producing [ERROR] Fa
groups.google.com
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'javax.cache:cache-api:1.1.1'
implementation 'org.ehcache:ehcache:3.10.8'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
implementation 'org.glassfish.jaxb:jaxb-runtime:2.3.3'
애플리케이션 구동시 기본 CacheManager 빈으로 JCacheManager 주입 확인
Using cache manager :org.springframework.cache.jcache.JCacheCacheManager
2. 추가 설정 (xml, properties, ..)
/resources/ehcache.xml 추가
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.ehcache.org/v3"
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd">
<cache alias="findMemberCache">
<key-type>java.lang.String</key-type>
<value-type>hello.caching.domain.Member</value-type>
<expiry>
<!--class(직접 정책 구현), none, ttl(timeToLive), tti(timeToIdle) 정책 지정-->
<ttl unit="seconds">20</ttl>
</expiry>
<listeners>
<listener>
<class>hello.caching.CacheLogger</class>
<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
<event-ordering-mode>UNORDERED</event-ordering-mode>
<events-to-fire-on>CREATED</events-to-fire-on>
<events-to-fire-on>EXPIRED</events-to-fire-on>
<events-to-fire-on>EVICTED</events-to-fire-on>
</listener>
</listeners>
<resources>
<heap unit="entries">2</heap> <!-- v3.10.8 deprecated -->
<offheap unit="MB">10</offheap>
</resources>
</cache>
</config>
참고. https://www.ehcache.org/documentation/2.8/configuration/data-life.html
Ehcache
Pinning, Expiration, and Eviction Introduction The architecture of an Ehcache node can include a number of tiers that store data. One of the most important aspects of managing cached data involves managing the life of those data in those tiers. Use the fig
www.ehcache.org
이벤트 발생(CREATED, EXPIRED, ..)시 로그로 정보를 간단히 출력한다
@Slf4j
public class CacheLogger implements CacheEventListener<Object, Object> {
@Override
public void onEvent(CacheEvent<?, ?> event) {
log.info("key : {}, eventType : {}, old value : {}, new value : {}",
event.getKey(),
event.getType(),
event.getOldValue(),
event.getNewValue());
}
}
- CREATED의 경우 신규 생성시 CacheEventListener 출력 확인
- EXPIREd의 경우 20초 후 만료 된 후 캐시 API 호출시 EXPIRED와 CREATED 출력됨을 확인
application.properties 설정 추가
spring.cache.jcache.config=classpath:ehcache.xml
에러
Caused by: org.ehcache.spi.serialization.SerializerException: java.io.NotSerializableException: hello.caching.domain.Member
- EhCache에 사용되는 객체는 Serializable 구현해야 한다
Member 도메인 수정
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
//..
}
3. 호출 테스트
- 20초 이상 지났을 때 캐시가 만료되는지 확인
- httpie 사용하여 호출
http -v ":8080/member/cache/tester"
38 분 27초에 최초 호출 후 39분 00초에 호출시 캐시가 만료된 것을 확인가능했다
참고.
SpringBoot + Ehcache 기본 예제 및 소개 - 기억보단 기록을
니들이 Caffeine 맛을 알아! - 네이버 결제 백엔드 개발자