개발 기록

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

Spring

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

1z 2024. 1. 5. 14:13

 

 

 

 

 

 

 

 

 

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://docs.spring.io/spring-framework/reference/integration/cache/annotations.html#cache-spel-context

https://jiwondev.tistory.com/282

https://prohannah.tistory.com/88

https://www.ehcache.org/documentation/3.8/xml.html

https://adjh54.tistory.com/166

https://chati.tistory.com/147

https://www.baeldung.com/ehcache