개발 기록
>[cache] EHcache 3.x 개념을 알아보고 Spring Boot 에서 사용해보자! 본문

1. 캐시(Cache) : 자주 사용하는 데이터나 값을 미리 복사해 놓는 임시 장소를 가리킨다.
2. 캐싱(Caching) : 이전에 처리(검색/계산) 되었던 데이터를 효과적으로 재사용하는 기법
3. CacheManager : Ehcache/Redis/Memcached
[1] Ehcache 개념
㉮ Spring 에서 사용할 수 있는 Java 기반 오픈소스 로컬 캐시라이브러리
㉯ javax.cache API (JSR-107 : 자바의 표준 캐시 스펙이자 java 객체의 메모리 캐싱에서 사용할 API에 대한 기준)
㉰ key-value 형태로 데이터를 저장한다.
㉱ EhCache VS Redis
: Redis 와 달리 Spring 내부적으로 동작하여 네트워크 지연 혹은 단절같은 이슈에서 자유롭다.
㉲ EhCache VS memcached
memcached 가 로컬 환경일지라도 별도로 구동하는 것과 다르게 ehcache는 서버 어플리케이션과 라이프사이클을 같이 하므로 사용하기 더욱 간편하다.
(1) 저장 공간 : heap, off-heap, disk
▶ heap : JVM 힙 메모리
▶off-heap : Garbage Collecotr에 의해 관리되지 않는 영역으로 GC 오버헤드가 일어나지 않아 성능이 좋다. 하지만 조작이 까다롭기 때문에 EhCache와 같은 캐시 라이브러리를 사용하는 걸 권장한다. (버전 3 이상)
▶ disk
[2]. Ehcache 사용법
1) 종속성 추가

★ Java 17 의 경우 java.lang.ClassNotFoundException: com.sun.xml.internal.bind.v2.ContextFactory 예외가 발생하므로
Javax 라이브러리 종속성을 추가해준다.

2) 주석 기반 캐시 관리 활성화
Spring Boot 의 경우, @EnableCaching 으로 ConcurrentMapCacheManager가 등록된다.
따라서 별도의 CacheManager Bean 선언이 필요하지 않다.

3) Ehcache 설정: XML 구성
EnableCacheing 어노테이션을 추가한다고 해서 캐시가 자동으로 생성되지 않는다. Spring 에서 관리하는 cache managemenet를 사용할 수 있게 활성화만 했을 뿐이므로, 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.xsd">
<persistence directory="cache/data"/>
<!-- CacheManager 에 의해 작성되고 관리될 Cache 인스턴스(Cache<k,v>) -->
<cache alias = "userCache">
<!-- <cache-template name="default">-->
<key-type>java.lang.String</key-type>
<value-type>java.lang.String</value-type>
<!-- 캐시 만료 시간 = timeToLiveSeconds
현재는 tti과 ttl을 함께 사용할 수는 없고, 원한다면 클래스를 새로 추가해서 두 속성 모두 사용되게 하면 된다.-->
<expiry>
<ttl unit="seconds">30</ttl>
<!-- <tti unit="seconds">30</tti>-->
</expiry>
<!-- listeners는 Cache의 리스너를 등록하는 요소이다. -->
<listeners>
<listener>
<!-- 리스너 클래스 위치 -->
<class>server.spring.guide.cache.CacheEventLogger</class>
<!-- 비동기 방식 사용, 캐시 동작을 블로킹하지 않고 이벤트를 처리, SYNCHRONOUS와 반대 -->
<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
<!-- 이벤트 처리 순서 설정 X, ORDERED와 반대 -->
<event-ordering-mode>UNORDERED</event-ordering-mode>
<!-- 리스너가 감지할 이벤트 설정(EVICTED, EXPIRED, REMOVED, CREATED, UPDATED) -->
<events-to-fire-on>CREATED</events-to-fire-on>
<events-to-fire-on>EXPIRED</events-to-fire-on>
</listener>
</listeners>
<!-- 캐시 데이터의 저장 공간과 용량을 지정한다.-->
<resources>
<!-- JVM 힙 메모리에 생성 될 최대 캐시 항목? LRU strategy -->
<heap unit="entries">1000</heap>
<!-- JVM 힙 메모리 외부 메모리 (GC 비관리 영역)-->
<offheap unit="MB">10</offheap>
<!-- Disk 메모리, LFU strategy
persistent="false" : 종료 시 디스크 데이터를 삭제
persistent="true" : 종료 시 디스크 데이터를 보존 및 JVM을 재시작 시 재로드 -->
<disk unit="MB" persistent="false">20</disk>
</resources>
</cache>
<!-- <cache alias="findMemberCache" uses-template="default"></cache>-->
</config>
(3-1) excache.xml 파일을 Spring 이 알도록 하기 위해 프로젝트 설정 파일에 추가한다.

★ (3-1-1) Ehcache 설정이 잘 적용 됬는지 확인!


(3-2) 참고. 캐시 교체 정책
💡 Ehcache 2.x : 힙 메모리에 대한 캐시 교체 정책(LRU, LFU, FIFO)을 지원
💡 EhCache 3.x : Eviction 정책이 캐시 저장 공간(heap, off-heap, disk)에 맞게 고정
+ no expiry : 캐시 매핑이 만료되지 않음
+ time-to-live : 캐시 매핑이 생성된 후 정해진 기간 후에 만료됨
+ time-to-idle : 캐시 매핑이 마지막으로 액세스한 시간 이후 정해진 기간 후에 만료됨
* Eviction 정책 종류
+ LRU (Least Recently Used)
+ LFU (Least Frequently Used)
+ FIFO (First-In, First-Out)
+ Custom (사용자 정의 정책)
(3-3) EhCache 3.x Custom expiry
Ehcache3 은 Custom expiry 정책을 정의할 수 있다. 아래 인터페이스를 구현하는 클래스를 생성하여 xml 설정에 경로를 넣어주면 된다.
public interface ExpiryPolicy<K, V> {
Duration INFINITE = Duration.ofNanos(Long.MAX_VALUE);
ExpiryPolicy<Object, Object> NO_EXPIRY = new ExpiryPolicy<Object, Object>() {
public String toString() {
return "No Expiry";
}
public Duration getExpiryForCreation(Object key, Object value) {
return INFINITE;
}
public Duration getExpiryForAccess(Object key, Supplier<?> value) {
return null;
}
public Duration getExpiryForUpdate(Object key, Supplier<?> oldValue, Object newValue) {
return null;
}
};
Duration getExpiryForCreation(K var1, V var2);
Duration getExpiryForAccess(K var1, Supplier<? extends V> var2);
Duration getExpiryForUpdate(K var1, Supplier<? extends V> var2, V var3);
}
4) 캐싱 처리 : Cache Annotation
* default 로 메서드의 파라미터를 캐시값으로 구성한다. = 같은 메서드가 불리더라도 파라미터가 다르면 다른 캐시를 생성한다.
* 주의할 점은 설정 파일(ehcache.xml)에서의 캐시명(alias 태그)와 키(key-type 태그), 값(value-type 태그)가 일치해야한다.
* 정상적으로 종료 되어야만 캐시가 저장되기 때문에 Exception 발생 시 저장되지 않는다.
▶ @Cacheable : 캐시 조회, 저장
▶ @CacheEvict : 캐시 삭제
▶ @CachePut : 캐시의 존재 여부를 떠나서 저장 혹은 갱신
▶ @Caching : 여러 개의 ‘캐시 어노테이션’을 ‘함께 사용’할 때 사용
▶ @CacheConfig : 캐시정보를 클래스 단위로 사용하고 관리
▶ ▶ ▶ 공통 속성 ◁ ◁ ◁
| value | 캐시할 데이터의 이름으로 ehcache.xml 의 cache 요소의 alias 값과 동일해야 한다. |
| key | 캐시를 구분하기 위한 key (ex. #root.methodName 키워드는 method 이름을 key로 잡아준다.) |
| condition | 캐시를 저장 할 조건 |
| unless | 캐시를 저장하지 않을 조건 (ex.null 로 반환되는 데이터) |
위의 속성외에도 cacheName, keyGenerator, cacheManager, cacheResolver 등 을 설정할 수 있다.
[3] 적용
(3-1) @Cacheable
- 캐시 존재 시 메서드 호출 전 결과값을 캐시에서 꺼내 리턴
- 캐시가 존재하지 않을 경우 캐시 저장 후 리턴
- codition: Parameter value == '홍길동' 경우에만 캐시처리를 하겠다. 라는 설정

결과 확인
: 최초 호출 시에는 DB 에서 데이터를 조회 해오고, 그 이후부터는 저장된 캐시로부터 결과를 받아 시간이 단축 된 것을 볼 수 있다.

(3-2) @CacheEvict
- 메서드가 실행될 때 캐시의 내용이 삭제 된다. 기본적으로 메소드의 키에 해당하는 캐시만 제거한다.
- allEntries : 전체 캐시 삭제 여부
- beforeInvocation : 메소드 실행 전 캐시 삭제 여부

- DB에 저장된 데이터에 update가 일어나면 캐시 데이터도 다시 저장되어야 할 것이다.
이럴 경우 기존에 저장된 캐시 데이터를 제거하면, 이후 호출에 대해 다시 갱시된 데이터를 캐시하게 될 것이다.
2024-01-06T17:22:19.235+09:00 INFO 10100 --- [e [_default_]-0] s.spring.guide.cache.CacheEventLogger : cache event logger message:::::: getKey: 홍길동 / getOldValue: null / getNewValue:홍길동
2024-01-06T17:22:19.237+09:00 INFO 10100 --- [nio-8080-exec-3] s.s.guide.cache.CacheCallController : 홍길동의 Cache 수행시간 : 2264
2024-01-06T17:22:24.855+09:00 INFO 10100 --- [nio-8080-exec-6] s.s.guide.cache.CacheCallController : 홍길동의 Cache 수행시간 : 5
2024-01-06T17:22:33.317+09:00 INFO 10100 --- [nio-8080-exec-9] server.spring.guide.cache.CacheService : 홍길동의 Cache Clear!
2024-01-06T17:22:44.318+09:00 INFO 10100 --- [nio-8080-exec-2] s.s.guide.cache.CacheCallController : 임꺽정의 Cache 수행시간 : 2036
2024-01-06T17:22:44.318+09:00 INFO 10100 --- [e [_default_]-0] s.spring.guide.cache.CacheEventLogger : cache event logger message:::::: getKey: 임꺽정 / getOldValue: null / getNewValue:임꺽정
2024-01-06T17:22:48.425+09:00 INFO 10100 --- [nio-8080-exec-3] s.s.guide.cache.CacheCallController : 임꺽정의 Cache 수행시간 : 1
(3-3) @CachePut
- keyGenerator : 캐시에 사용할 키 생성기 지정
- cacheManager : 캐시 매니저를 지정

6) 캐시 될 데이터 직렬화(Serializable)
ehcache3 는 캐싱할 데이터를 외부 메모리(offheap 혹은 disk)에 저장하기 위해서는
저장할 데이터(객체 혹은 인스턴스)가 Serializable이 구현 되어 있어야 한다.

[4] Cache Event Listener 추가 : CREATED 및 EXPIRED 캐시 이벤트를 기록

[5] 이슈
문제: 같은 클래스 내부에서 호출하게 되면 캐싱 되지 않는다.
원인:
Spring AOP를 활용해서 제공되는 기능이라 AOP 특성이나 제약을 따른다.
Spring AOP는 proxy를 통해 이루어지는데, self-invocation 상황에서는 Proxy interceptor를 타지 않고 this를 이용해 바로 메소드를 호출하기 때문이다.
해결 : AspectJ라는 AOP를 지원해주는 라이브러리 사용, Class 분리
* 참고 EHCache 2.0 설정

참고
https://jiwondev.tistory.com/282
https://prohannah.tistory.com/88
https://www.ehcache.org/documentation/3.8/xml.html