SpringBoot

EhCache2로 캐싱 기능 구현해보기

파미페럿 2021. 8. 30. 16:01

https://pamyferret.tistory.com/8

 

[ SpringBoot ] SpringBoot의 기본 Cache 사용하기

개발을 하다보면 어라? 이 데이터 계속 똑같이 사용되고 업데이트 될 일이 없는데? 하는 것들이 보인다. 데이터 업데이트가 자주 이뤄지지도 않고 자주 호출되는 데이터인데 계속 DB에 가서 데이

pamyferret.tistory.com

 

https://pamyferret.tistory.com/25

 

[ SpringBoot ] Cache 기능 Redis로 구현하기

https://pamyferret.tistory.com/8 [ SpringBoot ] SpringBoot의 기본 Cache 사용하기 개발을 하다보면 어라? 이 데이터 계속 똑같이 사용되고 업데이트 될 일이 없는데? 하는 것들이 보인다. 데이터 업데이트가..

pamyferret.tistory.com

 

지난번에 SpringBoot 어플리케이션 내부에 저장되는 기본 캐싱 기능도 사용해보고 redis에 캐시 데이터가 저장되게 redis를 통해서 캐싱 기능을 구현해봤다.

이제는 분산 캐싱을 해보려고 하는데, 분산 캐싱을 해보려고 하니까 redis를 여러 개 띄우고 하기 보다는 redis와 같이 많이들 사용하는 캐시 데이터를 저장할 다른 방법인 EhCache라는 것을 알게 되었다.

 

 

EhCache란?

EhCache란 자바 기반 캐시로 오픈 소스이다. 클러스터 기능도 지원하며 로컬 오픈 소스 캐시 라이브러리라고 이해하면 좋다. 즉, 기본으로 스프링부트의 기본 캐싱 기능처럼 스프링 어플리케이션과 함께 존재하는 캐시로 사용할 수도 있고 별도 프로세스 외 배포까지 지원하는 등 메모리, 디스크 저장을 지원하며 멀티 CPU도 동시 접근하도록 할 수 있다.

기본 JVM 메모리에 저장된다.

redis처럼 별도 서버 설치 없이 기본 사용할 수 이써 가볍게 사용하기 좋은 캐시 엔진이다.

 

 

EhCache 2 구현하기

EhCache는 현재 버전 2와 버전 3가 있다. 버전 3의 경우 JSR-107과의 호환성이 좋아졌고 javax.cache 지원 등으로 ehcache 2보다 좀 더 발전했지만 ehcache에서는 기본적으로 serialize를 제공해 별도 serialize를 구현하지 않아도 되나, ehcache 3에서는 기본 serialize를 제공하는 타입이 그렇게 많이 않아서 사용하려면 저장할 데이터에 대해 serialize를 구현해야 한다는 단점이 존재한다.

 

즉, 이번에는 serialize를 구현하지 않아도 되는 ehcache 2로 우선 ehcache로 캐싱 기능을 구현해보도록 하겠다.

 

 

1. ehcache 2 사용을 위한 라이브러리 설정

ehcache를 사용하기 위해서는 redis와 마찬가지로 별도 라이브러리 설정을 해야한다.

참고로, ehcache 2와 3은 설정하는 라이브러리에서부터 차이가 나서 이를 통해 버전 2인지 버전 3인지 구분할 수 있다.

 

- gradle

implementation 'net.sf.ehcache:ehcache'

 

- maven

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>

 

 

2. ehcache 설정 xml 파일 작성

ehcache 설정은 그냥 자바코드에서도 할 수 있지만 가독성 면에서는 xml 파일이 더 좋아서 나는 xml 파일로 작성하길 추천한다.

참고로 버전 2와 버전 3 또한 각각 설정 xml 파일을 작성하는 법이 다르니 유의해야 한다.

 

 

내가 'menu'라는 캐시 이름으로 아래와 같이 서비스 단에서 캐시를 저장하고 있다면,

public class MenuService {
	private final MenuListRepository menuListRepository;
	
	// 메뉴 리스트 가져오기
	@Transactional(readOnly = true)
	@Cacheable(value = "menu")
	public List<MenuList> getMenuList() {
		return this.menuListRepository.findAll();
	}
    	...
}

 

 

ehcache xml 설정 파일은 아래와 같다. ehcache xml 설정 파일은 src/main/resources/config에 생성했다.

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         maxBytesLocalHeap="300M">

    <cache name="menu"
           eternal="false"
           timeToIdleSeconds="0"
           timeToLiveSeconds="1200"
           overflowToDisk="false"
           diskPersistent="false"
           diskExpiryThreadIntervalSeconds="0"
           memoryStoreEvictionPolicy="LRU">
    </cache>
    
</ehcache>

 

 

우선 ehcache xml 파일 작성을 위해서 아래와 같이 ehcache 태그와 그 안에 xsi와 noNamespaceSchemaLocation을 작성해준다.

(아래 작성은 진짜 최소한의 작성이다. 여러 글들을 보면 noNamespaceSchemaLocation 안에 더 많은 것들이 들어 있기도 한다.)

 

여기서 maxBytesLocalHeap는 이 xml 설정 파일로 생성할 cacheManager로 관리할 로컬 메모리 힙의 크기를 지정하는 것이다.

즉, 로컬(기본으로 저장되는)에 얼마만큼의 캐시를 최대한 저장할 것인지에 대한 설멍이다.

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         maxBytesLocalHeap="300M">
....
    
</ehcache>

 

 

그 안에는 이제 내가 사용할 캐시를 이름별로 정의를 해준다. name 부분은 캐시 데이터의 이름 즉, 캐싱 기능에서 사용할 이름을 정의하면 된다. 캐싱 기능에서 이름을 호출했으나 xml 파일에 설정되어 있지 않다거나 따로 코드로 캐시 데이터 공간을 생성해주지 않으면 에러가 발생한다.

...

    <cache name="menu"
           eternal="false"
           timeToIdleSeconds="0"
           timeToLiveSeconds="1200"
           overflowToDisk="false"
           diskPersistent="false"
           diskExpiryThreadIntervalSeconds="0"
           memoryStoreEvictionPolicy="LRU">
    </cache>
    
...

 

설정해놓은 옵션을 간단히 설명하면 아래와 같다. 참고로 설정 가능한 옵션 중에서 필수로 설정해야하는 것은 name과 eternal이다.

- eternal

ecache에서는 time을 설정해서 해당 캐시가 어느 시간동안 조회되지 않으면 삭제할 수 있다. 즉 쿠키처럼 캐시 데이터가 살아있을 시간을 정할 수 있다. 그 timeout 시간을 무시할지에 대한 설정이다.

false로 설정할 경우 해당 expire time을 인식하고 캐시 데이터가 자동으로 사라지며, true로 할 경우 해당 시간을 무시하고 캐시 데이터가 계속 살아 있다.

기본 값은 false이다.

 

- timeToIdleSeconds

캐시 데이터가 얼마 시간 동안 조회되지 않으면 사라지게 할 것인지에 대한 설정이다.

이를 0으로 설정해놓으면 조회가 오랫동안 되지 않아도 캐시 데이터를 지우지 않겠다는 뜻이다.

기본 값은 0이다.

 

- timeToLiveSeconds

캐시 데이터가 살아 있는 시간이다.(조회와 상관 없이) 1,200초로 설정했다는 것은 20분 동안 캐시 데이터를 유지하고 20분이 지나면 캐시 데이터를 지우겠다는 뜻이다. 데이터가 자주 바뀌어야하는 경우, 주기적으로 변경되어야 하는 경우에 유용하다. timeToLiveSeconds와 마찬가지로 0으로 설정해놓을 경우 캐시 데이터는 지워지지 않고 살아 있다.

기본 값은 0이다.

 

- overflowToDisk

maxElementsInMemory랑 같이 사용을 하는데 maxElementsInMemory로 설정한 크기보다 캐시 데이터가 커질 경우 디스크에 저장하기 시작한다.

(maxElementsInMemory는 요소의 개수를 의미한다.)

기본 값은 true이다.

 

- diskPersistent

ehcache는 JVM에 캐시 데이터가 생성되고 저장되는데 JVM을 중지시켰을 때 JVM에 있는 캐시 데이터를 디스크에 저장할지 말지 여부를 나타낸다.

기본 값 false이다.

 

- diskExpiryThreadIntervalSeconds

디스크에 저장된 캐시를 관리하기 위한 설정으로 디스크에 저장된 만료된 캐시 항목을 제거하기 위한 쓰레드를 실행할 주기를 입력한다.

나는 디스크에 캐시 데이터를 저장하지 않고 로컬 JVM에만 저장하므로 0으로 설정해놨다.

기본 값 120이다.

 

- memoryStoreEvictionPolicy

메모리에 캐시 데이터를 어떻게 저장할지에 대한 정책에 대한 설정이다. LRU, FIFO, LFU 세 가지 중에서 골라 설정할 수 있다.

아마 정처기든 컴활이든 컴퓨터 공부를 좀 했다면 어디서 들어봤을텐데 싶을 것이다. 

 

* LRU: 제일 최근에 들어온 데이터를 내보내는 방식

* FIFO: 처음 들어왔던 데이터를 내보내는 방식

* LFU: 제일 적게 쓰인 데이터를 내보내는 방식

 

어떤 정책으로 캐시 데이터를 메모리에서 관리하고 할 것인지 선택할 수 있다.

기본 값 LRU이다.

 

 

 

2. CacheConfig 작성

위 라이브러리 설정과 ehcache.xml을 작성 완료했다면 ehcache를 통해 캐싱 기능을 구현할 준비가 되었다.

이제 해당 xml 파일을 통해 스프링부트 어플리케이션에서 사용할 cacheManager를 정의해주면 된다.

(전에 redis에서는 CacheManager로 config 자바 클래스 파일을 생성해봤지만 이번에는 CacheConfig로 config 자바 클래스 파일을 생성해봤다.)

 

 

우선 ehcache 캐싱 기능을 사용하기 위해 작성한 config 자바 클래스 파일은 아래와 같다. ehcache.xml을 통해 EhCacheManager를 생성하는 EhCacheManagerFactoryBean과 그것을 이용해서 진짜 cacheManager를 만드는 ehcacheManager로 나뉘어진다.

@Configuration
public class CacheConfig {
	
	// ehcache2
	@Bean
	public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
		EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean();
		ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("config/ehcache.xml"));
		ehCacheManagerFactoryBean.setShared(true);
		return ehCacheManagerFactoryBean;
	}
	
	@Bean
	public EhCacheCacheManager ehcacheManager(EhCacheManagerFactoryBean ehCacheManagerFactoryBean) {
		EhCacheCacheManager ehCacheCacheManager = new EhCacheCacheManager();
		ehCacheCacheManager.setCacheManager(ehCacheManagerFactoryBean.getObject());
		return ehCacheCacheManager;
	}
 }

 

 

우선 xml 파일을 이용하는 ehcacheManagerFactoryBean()부터 살펴보자.

이미 xml 파일에 캐시데이터 관리에 대한 설정을 다 해놔서 복잡한 코드는 없다.

'setConfigLocation(new ClassPathResource("${xml 파일 경로}")'를 통해 미리 작성해놓은 ehcache 설정을 넣어주고 setShared를 통해 싱글톤으로 할지 말지 여부를 결정한다. 기본 값은 false이고 true로 설정할 경우 ehCacheManagerFactoryBean으로 만든 CacheManager를 코드로 호출하고 공유할 수 있다.

	...
	
	// ehcache2
	@Bean
	public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
		EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean();
		ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("config/ehcache.xml"));
		ehCacheManagerFactoryBean.setShared(true);
		return ehCacheManagerFactoryBean;
	}
	
	...

 

 

실제 cacheManager를 생성하는 ehcacheManager는 더욱더 간단하다.

위에서 정의한 ehCacheManagerFactoryBean을 가져와서 cacheManager에 설정하고 생성하는 것이다. 그리고 만들어진 EhCacheCacheManager를 반환한다. EhCacheCacheManager의 상위 클래스를 계속 거슬러 올라가다 보면 CacheManager 객체가 나온다. 즉, CacheManager 객체를 상속받은 EhCacheCacheManager를 생성해서 반환하는 것이다.

	...
	
	@Bean
	public EhCacheCacheManager ehcacheManager(EhCacheManagerFactoryBean ehCacheManagerFactoryBean) {
		EhCacheCacheManager ehCacheCacheManager = new EhCacheCacheManager();
		ehCacheCacheManager.setCacheManager(ehCacheManagerFactoryBean.getObject());
		return ehCacheCacheManager;
	}
	...

 

 

The type net.sf.ehcache.CacheManager cannot bt resolved.

위의 cacheManager config 작성 중에 위와 같은 에러가 발생할 수 있다. 이는 ehcache 버전 2와 버전 3를 혼동해서 생기는 에러로 각 버전에 맞는 라이브러리를 설정해주지 않았을 경우 발생하는 에러이다.

 

이와 같은 에러가 발생했을 경우 내가 'org.ehcache' 라이브러리 설정을 한 것은 아닌지 확인하고 만일 그렇다면 'net.sf.ehcache' 라이브러리로 설정을 바꿔주는 것이 필요하다. ('org.ehcache'는 ehcache 3에서 쓰이는 것이다.)

 

 

 

3. ehcache 설정 후 실행 결과

위의 ehcache 설정 후 스프링부트 어플리케이션을 실행해보면 아래와 같이 net.sf.ehcache.pool이 동작하면서 JVM에 내가 xml로 설정한 캐시 데이터를 빈 상태로 생성한다.

만일 여기서 VM에 대해 IOException에러가 발생하면 해당 프로젝트의 Build Path > Configure Build Path > 

 

만일 실행할 때 VM에 대해 IOException에러가 발생하면 해당 프로젝트의 Build Path > Configure Build Path > Libraries에서 JRE System Library가 jre가 아닌 jdk로 잘 설정 되어 있는지 확인이 필요하다.

(참고로 ehcache 3는 java 8 이상에서 구동해야 한다.)

 

 

간단하게 메뉴 리스트를 가져오는 부분에 캐싱 기능을 적용해봤는데 아래와 같이 정상적으로 캐싱 기능이 동작하는 것을 확인할 수 있다.

 

 

Ehcache 2 캐시 데이터 확인하기

ehcache의 경우는 redis처럼 별도 서버로 돌아가는 것이 아닌 JVM에서 돌아가는 캐시 엔진이다. 즉, 별도의 client console 같은 것이 없다. 그래서 ehcache로 캐시 데이터가 잘 저장되었고 현재 남아 있는 캐시 데이터는 무엇인지 확인을 위해서는 별도 코딩이 필요하다.

 

나는 간단하게 현재 ehcache에 저장되어 있는 캐시 키 값만 확인하도록 CacheService를 구현해 CacheController를 통해 호출할 수 있게 했다.

캐시에 대한 정보는 CacheManager를 통해 얻어올 수 있는데, 참고로 이 CacheManager는 'org.springframework.cache'의 CacheManager가 아닌 ehcache 라이브러리에서 제공하는 'net.sf.ehcache.CacheManager'이다.

@Service
@RequiredArgsConstructor
public class CacheService {
	private final CacheManager cacheManager;
	
	public void listAllCacheKeys() {
		Cache cache = cacheManager.getCache("menu");
		
		List keys = cache.getKeys();
		
		for(Object key: keys) {
			System.out.println("==> ehcache key: " + key + "\n");
		}
	}
}

 

 

위 서비스를 구동하면 아래와 같이 redis에서 cli를 통해 조회했던 키 값을 조회할 수 있다. 필요하다면 CacheManager의 get()함수를 통해 key 값을 통해 캐시 데이터를 가져올 수도 있다. (반환 값은 net.sf.ehcache.Element이다.)

 

 

처음에는 ehcache 3가 최신이길래 ehcache 3로 먼저 ehcache를 구현해보려고 했지만 ehcache 3는 객체별로 Serializer를 implement해서 오버라이딩해야하기 때문에 우선 간단하게 ehcache 2로 ehcache 기능을 사용해봤다.

다음에는 ehcache 3도 사용하면서 ehcache 2와 비교했을 때 왜 더 좋은지에 대해 비교도 해봐야겠다.

 

 

 

 

✋ EhCache 2 공식 문서

https://www.ehcache.org/generated/2.10.0/html/ehc-all/#page/Ehcache_Documentation_Set%2Fco-cache_basic_terms.html%23

 

Ehcache

 

www.ehcache.org

 

✋ net.sf.ehcache

https://mvnrepository.com/artifact/net.sf.ehcache/ehcache

 

 

 

 

반응형