본문 바로가기

Java/Cache

[Spring] ehCache2와 달라진 ehCache3 사용

반응형

ehCache 3 을 Spring Boot 에 적용해보겠다.

ehCache 3 버전부터는 JSR-107 cache manager를 구현했다고 한다.

다만, ehCache 2.x 버전과 3.x 버전의 환경설정 포맷 및 사용방법이 조금 달라졌기에

tti, ttl, expiry 등 캐시 유지 기간에 대한 속성을 적용하려면 버전과 환경설정이 일치해야 한다.

 


⌗ ehCache 3 적용 고려사항

  • Database Selection Query 대상으로 비지니스 특성상 Hit 확율이 높은지 확인이 필요하다.
    cache는 기본적으로 Hit 확율이 높을 경우 쓰는 의미가 있다. 만약 Hit 확율이 현저히 떨어지는 경우 적용 하지 않는 편이 성능적으로 더 유리하다.
    Hit 확율이 높은지, 낮은지에 대해서는 해당 cache의 key 값이 되는 속성이 얼마나 자주 호출되는지로 확인하여야 한다. 
  • Cache TTL time 및 삭제 알고리즘에 대해 검토가 필요하다.
    • 특정 element가 cahce 되었을 경우, 해당 캐시가 얼마나 오랫동안 cache 메모리에 유지시킬건지 TTL time에 대한 정책을 고려해야한다.
    • cache에 데이터가 메모리에 가득차게 되면, 정의한 알고리즘(LRU or LFU or FIFO) 기반으로 데이터가 삭제된다. 
      삭제 알고리즘 동작(eviction)과 GC 가 동작 할 경우에도 정상 동작을 보장 할 수 있어야 하며, OOM과 같은 상황이 발생되는지도 확인이 필요하다.

⌗ ehCache 3 적용 형태

  • ehCache에서 제공 하는 cache 방식에는 크게 3가지(heap memory, big memory, local disk) 가 존재 하지만,
    heap memory 형태만 사용 하도록 한다.
  • Cache TTL time 활용한 cache eviction 기능을 활용 하는것을 추천한다.
    명시적인 eviction 기능을 적용 있지만, 로직 버그가 있을 경우 해당 cache는 갱신 되지 않을 위험이 존재하므로 적어도 TTL은 항상 적용 하는 형태로 사용 한다. (이 내용은 최하단에 예시를 들어 설명하겠다.)
  • Cache Element 개수는 수치상으로 메모리가 충분하다는 가정하에 max 5000개 이하로 설정하는 것이 효율적이다.
    단, One Thread / One instnace 환경 테스트에서는 entry 개수가 1000개 이하는 CPU usage가 큰 의미가 없다.

⌗ ehCache3 적용 및 사용

 

1 ) gradle dependency 추가 

/build.gradle 파일

dependencies {

  // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-cache
  compile group: 'org.springframework.boot', name: 'spring-boot-starter-cache', version: '2.3.3.RELEASE'

  // https://mvnrepository.com/artifact/org.ehcache/ehcache
  compile group: 'org.ehcache', name: 'ehcache', version: '3.8.1'

  // https://mvnrepository.com/artifact/javax.cache/cache-api
  compile group: 'javax.cache', name: 'cache-api', version: '1.1.1'
  
}

 

2 ) ehcache.xml 파일 생성 및 캐시 설정

resources/ehcache.xml 파일 

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns='http://www.ehcache.org/v3'>
	<!-- 캐시 파일이 생성되는 경로 -->
  <persistence directory="cache/data"/>
  
<!--  <cache-template name="template">-->
<!--    캐시가 생성되고 삭제되고 하는 이벤트를 모니터링 하고 싶으면 org.ehcache.event.CacheEventListener 를 구현하는 클래스를 만들어서 설정 (태그 순서가 중요)-->
<!--    <listeners>-->
<!--        <listener>-->
<!--            <class>sample.CacheEventLogger</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>EVICTED</events-to-fire-on>-->
<!--            <events-to-fire-on>REMOVED</events-to-fire-on>-->
<!--            <events-to-fire-on>UPDATED</events-to-fire-on>-->
<!--            <events-to-fire-on>EXPIRED</events-to-fire-on>-->
<!--        </listener>-->
<!--    </listeners>-->
<!--  </cache-template>-->

  <cache alias="cacheName">
    <key-type>java.lang.String</key-type>
    <value-type>java.lang.Boolean</value-type>
    <expiry>
      <!-- 캐시 만료 시간 = timeToLiveSeconds -->
      <ttl unit="seconds">30</ttl>
    </expiry>
    <resources>
      <!-- JVM heap 메모리, LRU strategy-->
      <heap unit="entries">10</heap>
      <!-- JVM heap 메모리 외부의 메모리 -->
<!--      <offheap unit="MB">10</offheap>-->
      <!-- Disk 메모리, LFU strategy-->
<!--      persistent="false" Ehcache will wipe the disk data on shutdown.-->
<!--      persistent="true" Ehcache will preserve the disk data on shutdown and try to load it back on restart of the JVM.-->
      <disk unit="MB" persistent="false">5</disk>
    </resources>
  </cache>
</config>

 

3 ) ehcache.xml 파일을 Spring이 알도록 하기 위해 프로젝트 설정 파일에 추가

resources/application.yml 파일

spring.cache.jcache.config=classpath:ehcache.xml

 

4 ) Main 클래스에 ehCache를 사용하겠다는 어노테이션(@EnableCaching) 추가

 

@EnableCaching어노테이션을 추가한다고 해서 캐시가 자동으로 적용되지 않는다. 

단순히 spring에서 관리하는 cache management를 사용할 수 있게 활성화만 했을 뿐이다.

Spring이나 Ehcache는 어플리케이션 내에 ehcache.xml 파일이 존재하는지 찾기에 ehcache.xml 을 생성하고, 

Spring에 ehcache.xml 파일의 위치를 알려주자.

java/Application.java 파일

@EnableCaching
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

 

5) Ehcache 3 환경 설정

★ 단, 주의할 점은 설정 파일(ehcache.xml)에서의 캐시명(alias 태그)와 키(key-type 태그), 값(value-type 태그)가 일치해야한다.

     alias 태그 = @Cacheable 의 value

     key-type 태그 = @Cacheable 의 key, 적용하는 메소드의 매개변수(parameter)형의 class 위치

     value-type 태그 = 적용하는 메소드의 반환(return)형의 class 위치

 

(ehcahce.xml은 application.yml과 동일한 위치에 파일을 생성한다.)

ehcache.xml 파일

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns='http://www.ehcache.org/v3'>
  <persistence directory="{캐시를 저장할 폴더 위치/cahce-data}"/>
<!--  <cache-template name="template">-->
<!--    캐시가 생성되고 삭제되고 하는 이벤트를 모니터링 하고 싶으면 org.ehcache.event.CacheEventListener 를 구현하는 클래스를 만들어서 설정 (태그 순서가 중요)-->
<!--    <listeners>-->
<!--        <listener>-->
<!--            <class>sample.CacheEventLogger</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>EVICTED</events-to-fire-on>-->
<!--            <events-to-fire-on>REMOVED</events-to-fire-on>-->
<!--            <events-to-fire-on>UPDATED</events-to-fire-on>-->
<!--            <events-to-fire-on>EXPIRED</events-to-fire-on>-->
<!--        </listener>-->
<!--    </listeners>-->
<!--  </cache-template>-->

  <cache alias="cacheName">
    <key-type>java.lang.String</key-type>
    <value-type>java.lang.Boolean</value-type>
    <expiry>
      <!-- 캐시 만료 시간 = timeToLiveSeconds -->
      <ttl unit="seconds">60</ttl>
    </expiry>
    <resources>
      <!-- JVM heap 메모리, LRU strategy-->
      <heap unit="entries">20000</heap>
      <!-- JVM heap 메모리 외부의 메모리 -->
      <!--      <offheap unit="MB">10</offheap>-->
      <!-- Disk 메모리, LFU strategy-->
      <!--      persistent="false" Ehcache will wipe the disk data on shutdown.-->
      <!--      persistent="true" Ehcache will preserve the disk data on shutdown and try to load it back on restart of the JVM.-->
      <disk unit="MB" persistent="false">50</disk>
    </resources>
  </cache>
</config>

 

application.yml

spring:
	cache:
		jcache:
			config: classpath:ehcache.xml

 

 

6) 캐시를 사용할 Service의 메소드에 어노테이션(@Cacheable) 추가

java/Service.java 파일

@Service
public class CacheService {

    @Cacheable(value = "cacheName", key = "#cacheKey")
    public Boolean square(String cacheKey) {
       	...
        return true;
    }

}

 


⌗ ehCache2와 다른 ehCache3 적용 방법

 

ehCache2와 ehCache3의 가장 큰 차이점은 캐시 설정 방법이 다르다.

필자는 캐시 설정파일(ehcache.xml)에서 캐시 설정을 하는데, 이부분이 가장 다르고 그 외에 사용하는 건 똑같았다.

 

아무래도 ehCache2에서 자잘하게 설정하던걸 JVM에 넘기면서 이를 ehCahce3에서는 TTL을 주로 설정하는 것 같다..

(뇌피셜이므로 자세한 내용을 아시는 분은 댓글로 공유해주세요ㅎㅎ)

 

AS-IS (ehcache2 설정)

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
  updateCheck="false">
  <diskStore path="java.io.tmpdir" />

  <cache name="cacheName"
    maxEntriesLocalHeap="20000"
    maxEntriesLocalDisk="1000"
    eternal="false"
    diskSpoolBufferSizeMB="20"
    timeToIdleSeconds="30"
    timeToLiveSeconds="60"
    memoryStoreEvictionPolicy="LFU"
    transactionalMode="off">
    <persistence strategy="localTempSwap" />
  </cache>

</ehcache>

 

TO-BE (ehcahce3 설정)

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns='http://www.ehcache.org/v3'>
  <persistence directory="java.io.tmpdir"/>

  <cache alias="cacheName">
    <key-type>java.lang.String</key-type>
    <value-type>java.lang.Boolean</value-type>
    expiry>
      <ttl unit="seconds">30</ttl>
    </expiry>
    <resources>
      <heap unit="entries">1000</heap>
      <disk unit="MB" persistent="false">20</disk>
    </resources>
  </cache>
</config>

 

게다가 ehCache3는 JSR 107을 지원하고 있어, ehCache2 경우 이 구현을 사용할 있다.

다만, ehCache2에 JSR 107을 구현하려면 JCacheCacheManager, CacheManager 을 구현해 로직적으로 풀어야할 것 같다..

관련 내용은 아래에 적용 방법을 참고하여 구현하면 될 것 같다.

 

ehCache2 의 JSR 107 적용 방법 (1) : github.com/ehcache/ehcache-jcache

 

ehcache/ehcache-jcache

The Ehcache 2.x implementation of JSR107 (JCACHE). Contribute to ehcache/ehcache-jcache development by creating an account on GitHub.

github.com

ehCache2 의 JSR 107 적용 방법 (2) : github.com/ehcache/ehcache-jcache/tree/master/ehcache-jcache

 

ehcache/ehcache-jcache

The Ehcache 2.x implementation of JSR107 (JCACHE). Contribute to ehcache/ehcache-jcache development by creating an account on GitHub.

github.com

 


⌗ ehCache3 주의 사항

1. 정상적으로 종료되는 로직에 cache를 저장하자.

다양한 테스트를 해보다가 알게된 내용으로,

ehCache를 적용한 메소드는 예외가 발생하면 안되므로, 애매한 경우에는 try-cache로 묶어서 예외가 발생되지 않도록 하는게 좋다.

 

즉, @cacheable 을 이용해 설정한 메소드는 정상적으로 종료되어야 캐쉬가 저장된다.

해당 메소드 안에서 throw new Exception()  발생하면 캐쉬는 저장되지 않는다.

 

정상적으로 저장되는 CASE.

@Cacheable(value = "Name", key = "#root.methodName")
public cacheDto cacheUse() {
    cacheDto result = new cacheDto();
    result.setResultCode(SUCCESS_CODE);
    result.setResultMessage(SUCCESS_MESSAGE);

    return result;
}

 

절대 저장되지 않는 CASE.

@Cacheable(value = "cacheName", key = "#root.methodName")
public cacheDto cacheUse() {
    cacheDto result = new cacheDto();
    result.setResultCode(SUCCESS_CODE);
    result.setResultMessage(SUCCESS_MESSAGE);

    if (SUCCESS_CODE.equal(result.getResultCode)) {
       throw new Exception();    
    }

    return result;
}

 

만약, null로 반환되는 데이터를 cache하고 싶은 CASE는 unless 키워드를 사용하자.

@Cacheable(value = "cacheName", key = "#root.methodName", unless = "#result == null")
public cacheDto cacheUse() {
    cacheDto data = new cacheDto();
    data.setResultCode(SUCCESS_CODE);
    data.setResultMessage(SUCCESS_MESSAGE);

    if (SUCCESS_CODE.equal(data.getResultCode)) {
       return null;    
    }

    return data;
}

 

❉ 참고

  #root.methodName 키워드는 자동으로 key를 method 이름으로 잡아서 지칭해준다.

  #result 키워드는 method의 반환 데이터를 지칭해준다.

 

2. cache의 특성상 자기 참조가 안되니 내부 메소드로 설정하지 말자.

 

하나의 Service 안에 메소드를 내부에서 call(동일 클래스 내 호출)하게 되면(self-invocation) proxy interceptor를 타지 않고,
바로 메소드를 호출하기 때문에 결국 캐싱이 되지 않는 다는 것이다.

 

cahce와 transaction 처리는 Spring AOP를 활용해서 제공되는 기능이다.
그래서 Spring AOP 특성이나 제약을 그대로 이어받는다.

Spring AOP는 proxy를 통해 이루어지는데, self-invocation 상황에서는 Proxy 객체가 아닌 this를 이용해 메소드를 호출한다.

In proxy mode (the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation (in effect, a method within the target object that calls another method of the target object) does not lead to actual caching at runtime even if the invoked method is marked with @Cacheable. Consider using the aspectj mode in this case. Also, the proxy must be fully initialized to provide the expected behavior, so you should not rely on this feature in your initialization code (that is, @PostConstruct).

(참고 : https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache)

 

따라서, cache가 적용되지 않고 계속 호출되는 현상을 볼 수 있을 것이다.

만약 cache를 적용하고 싶다면 AspectJ라는 AOP를 지원해주는 라이브러리를 사용해야하는데, 만약 proxy 단위로 작업을 진행하면 spring framework를 잘 모르는 상태에서 다른 영향이 있을 수 있어 작업이 까다로워지므로 권장하지 않는다.

 

다른 방향으로 고민해보는게 좋을 듯 싶다..

예를 들어, class를 분리해 호출하는 것만으로도 간단하게 해결될 문제이다.ㅎㅎ

 

3. cache될 데이터가 직렬화(Serializable)가 가능해야 한다.

직렬화(serializalbe)이 되지 않는다면, 캐시가 되지 않는다.

혹시 캐시가 되지 않는다면 데이터의 implement Serializalbe를 이용하자.

 

@Setter
@Getter
@ToString
public class data implements Serializable {
    ...
}

 


[참고]

ehcache3 (3.7v) 공식 사이트 : www.ehcache.org/documentation/3.7/examples.html

ehcache3 적용 방법 (1) : https://dimitr.im/spring-boot-cache-ehcache

ehcache3 적용 방법 (2) : springframework.guru/using-ehcache-3-in-spring-boot/

ehcache3 적용 방법 (3) : medium.com/finda-tech/spring-로컬-캐시-라이브러리-ehcache-4b5cba8697e0

ehcache3 Key 설정 tip. : https://stackoverflow.com/questions/33383366/cacheble-annotation-on-no-parameter-method

ehcache 구글 커뮤니티 : https://groups.google.com/g/ehcache-users

 


 

반응형

❥ CHATI Github