[Ehcache] 간단한 캐시 구현 (@Cacheable, @CacheEvict)
캐시(Cache)란?
자주 사용하는 데이터나 값을 미리 복사해 놓는 임시 장소이다.
DBMS의 부하를 줄이고 성능을 높이기 위해 사용한다.
Ehcache는 Spring에서 사용할 수 있는 캐시 중 하나이다.
1. dependency 추가
우선 Maven환경에서 Ehcache를 사용할 수 있도록 dependency를 추가해야한다.
<!-- Ehcache -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.2</version>
</dependency>
<!-- Ehcache 적용을 위한 Spring artifact -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<!-- EHCache Support 모듈, 다른 Caching 지원모듈 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
2. ehcache.xml 파일 추가
캐시를 설정하기 위한 ehcache.xml 파일을 만들어서 추가해준다.
<defaultCache>는 반드시 설정해주어야하는 부분이고,
<cache name="">에는 각각 캐시에 대한 맞춤 설정을 해주는 부분이다.
주석처리가 되어있는 옵션을 참고하여 설정을 진행하면 된다.
<?xml version="1.0" encoding="UTF-8"?>
<!-- CacheManager에 의해 관리되는 캐시의 메모리를 300M로 제한 -->
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
maxBytesLocalHeap="300M"
updateCheck="false">
<!-- 임시저장 경로를 설정 -->
<diskStore path="java.io.tmpdir" />
<!-- Cache에 저장할 레퍼런스의 최대값을 100000으로 지정,
maxDepthExceededBehavior = "continue"
: 초과 된 최대 깊이에 대해 경고하지만 크기가 조정 된 요소를 계속 탐색
maxDepthExceededBehavior = "abort"
: 순회를 중지하고 부분적으로 계산 된 크기를 즉시 반환 -->
<sizeOfPolicy maxDepth="100000" maxDepthExceededBehavior="continue" />
<!-- 캐시 정의 -->
<!--
name : 캐시의 이름이다. @Cacheable("캐시의 이름") 와 일치시켜줘야한다.
diskExpiryThreadIntervalSeconds : 디스크에 저장된 캐시들에 대해 만료된 항목을 제거하기 위한 스레드를 실행할 주기 설정
diskSpoolBufferSizeMB : 디스크 캐시에 쓰기 모드로 들어갈 때 사용될 비동기 모드의 스폴 버퍼 크기 설정 (OutOfMemory 발생시 수치 낮추도록 함)
diskPersistent : VM이 재기동할 때 캐싱된 객체를 계속 유지할지 여부
eternal : 한번 캐시하면 영원히 유지할 것인지의 여부
maxElementsInMemory : 메모리에 캐싱될 최대 객체 수
maxEntriesLocalHeap : 힙메모리 최대량
overflowToDisk : 메모리저장공간이 부족할때 Disk 사용여부
memoryStoreEvictionPolicy : 최대 개수에 도달할 때, 제거 알고리즘
- FIFO : 먼저 저장된 데이터를 우선 삭제
- LFU : 데이터의 이용 빈도 수를 기준으로 이용 빈도가 가장 낮은 것부터 삭제
- LRU : 데이터의 접근 시점을 기준으로 최근 접근 시점이 오래된 데이터부터 삭제
overflowToDisk : maxElementsInMemory이 옴계량에 가까우면 오버플로우되는 객체들을 디스크에 저장할지 결정
timeToIdleSeconds : 다음 시간동안 유휴상태 후 갱신할지 결정 (데이터가 지정된 시간(초단위)동안 재호출되지 않으면 휘발됨)
timeToLiveSeconds : 한번 저장된 데이터의 최대 저장 유지 시간(초단위)
maxBytesLocalHeap : 최대 로컬 힙메모리 사용량 설정 (사용 시 maxEntriesLocalHeap 사용 불가)
maxBytesLocalDisk : maxBytesLocalHeap에 설정된 캐시 사용 이후 디스크 스토리지 한계 설정
-->
<!-- default Cache 설정 (반드시 선언해야 하는 Cache) -->
<defaultCache
eternal="false"
maxEntriesLocalHeap="10000"
maxEntriesLocalDisk="10000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="1800"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU" >
</defaultCache>
<!-- 사용하고자 하는 캐시 별 설정 -->
<cache
name="cmmnCdList"
eternal="false"
timeToIdleSeconds="1800"
memoryStoreEvictionPolicy="LFU" >
</cache>
</ehcache>
3. servlet.xml 파일에 빈 등록
servlet.xml 파일에 어노테이션 기반 캐시를 사용할 수 있는 태그를 추가하고, 앞서 만든 ehcache.xml 파일 위치를 설정 해 빈을 등록한다.
<!-- Annotation 기반 캐시 사용 (@Cacheable, @CacheEvict..) -->
<cache:annotation-driven cache-manager="cacheManager"/>
<!-- EHCache 기반 CacheManager 설정 -->
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="ehcache" />
</bean>
<!-- ehcache.xml 설정 로드 -->
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="/WEB-INF/config/egovframework/cache/ehcache.xml" />
<property name="shared" value="true" />
</bean>
Ehcache 사용을 위한 설정을 완료했다.
이제 DB에서 데이터를 불러오는 과정에서 캐시를 적용해보자.
4. 캐시 적용
@Cacheable : 캐시 기능 수행
@CacheEvict : 캐시를 적절한 시점에 제거
여기서는 전체 코드 데이터를 조회하기 위해 selectCmmnCdList() 에서 @Cacheable 어노테이션을 추가해주었다. 별도로 key를 설정해주는 것이 아니라 value 값만 등록해주었다.
cmmnCdList 이름의 캐시에 공통 코드 리스트가 저장될 것이다.
등록, 수정, 삭제 시에는 캐시 값이 달라지기 때문에 적절한 시점에 업데이트를 해주어야 한다. 따라서 해당 요청이 들어올 경우 @CacheEvict 어노테이션을 추가하고 allEntries를 true로 주어 해당 이름의 캐시가 전체 삭제되도록 한다.
/**
* 코드 데이터 조회
* @return
* @throws Exception
*/
@Cacheable(value="cmmnCdList")
public List<CmmnCdVO> selectCmmnCdList() throws Exception {
List<CmmnCdVO> list = selectList("commonMapper.selectCmmnCdList");
return list;
}
/**
* 코드 데이터 등록
* @param cmmnCdVO
* @throws Exception
*/
@CacheEvict(value="cmmnCdList", allEntries=true)
public void insertCmmnCd(CmmnCdVO cmmnCdVO) throws Exception {
insert("commonMapper.insertCmmnCd", cmmnCdVO);
}
/**
* 코드 데이터 수정
* @param cmmnCdVO
* @throws Exception
*/
@CacheEvict(value="cmmnCdList", allEntries=true)
public void updateCmmnCd(CmmnCdVO cmmnCdVO) throws Exception {
update("commonMapper.updateCmmnCd", cmmnCdVO);
}
/**
* 코드 데이터 삭제
* @param cmmnCd
* @throws Exception
*/
@CacheEvict(value="cmmnCdList", allEntries=true) // 등록 시 키 값으로 등록한 것이 아니라 전체 삭제해야 함
public void deleteCmmnCd(String cmmnCd) throws Exception {
delete("commonMapper.deleteCmmnCd", cmmnCd);
}
내가 여기서 헷갈렸던 것이 삭제 시 삭제되는 row만 삭제되도록 키 값을 주어 삭제했는데, 애초에 @Cacheable을 통해 저장할 때, 별도로 key 값을 주고 저장한 것이 아니라 전체를 한번에 저장했기 때문에 어쩔 수 없이 전체를 지워줘야 한다. 후에 전체 조회하도록 하여 새로운 값의 코드가 저장될 수 있도록 한다.
5. 테스트
모든 코드를 작성 완료했으면, 테스트를 해보겠다.
아래처럼 첫 요청에는 SQL 쿼리를 반환해 DB에서 데이터를 가져온 것을 확인할 수 있고,
두번째 요청에는 쿼리를 반환하지 않는 것으로 보아 DB에 접속하지 않고 캐시에 있던 데이터를 반환하는 것을 확인할 수 있다.
2020-09-04 16:24:07,725 INFO [com.common.controller.CommonController] ==== Common Controller Select Start ====
2020-09-04 16:24:07,726 INFO [jdbc.sqlonly] SQL : SELECT
CMMN_CD cmmnCd,
GRP_CD grpCd,
CD_NM cdNm
FROM
T_CMMN_CD_D T1
2020-09-04 16:24:07,752 INFO [com.common.controller.CommonController] list => [CmmnCdVO(~~~)]
2020-09-04 16:24:07,755 INFO [com.common.controller.CommonController] ==== Common Controller Select End ====
2020-09-04 16:24:10,829 INFO [com.common.controller.CommonController] ==== Common Controller Select Start ====
2020-09-04 16:24:10,830 INFO [com.common.controller.CommonController] list => [CmmnCdVO(~~~)]
2020-09-04 16:24:10,831 INFO [com.common.controller.CommonController] ==== Common Controller Select End ====