EHCache

Computer Science/Cache 2013. 2. 11. 08:02


참고 - http://linuxism.tistory.com/1265





EHCache를 이용한 기본적인 캐시 구현 방법 및 분산 캐시 구현 방법을 살펴본다.

EHCache의 주요 특징 및 기본 사용법

게시판이나 블로그 등 웹 기반의 어플리케이션은 최근에 사용된 데이터가 또 다시 사용되는 경향을 갖고 있다. 80:20 법칙에 따라 20%의 데이터가 전체 조회 건수의 80%를 차지할 경우 캐시를 사용함으로써 성능을 대폭적으로 향상시킬 수 있을 것이다.

본 글에서는 캐시 엔진 중의 하나인 EHCache의 사용방법을 살펴보고, Gaia 시스템에서 EHCache를 어떻게 사용했는 지 살펴보도록 하겠다.

EHCache의 주요 특징

EHCache의 주요 특징은 다음과 같다.

  • 경량의 빠른 캐시 엔진
  • 확장(scable) - 메모리 & 디스크 저장 지원, 멀티 CPU의 동시 접근에 튜닝
  • 분산 지원 - 동기/비동기 복사, 피어(peer) 자동 발견
  • 높은 품질 - Hibernate, Confluence, Spring 등에서 사용되고 있으며, Gaia 컴포넌트에서도 EHCache를 사용하여 캐시를 구현하였다.
기본 사용법

EHCache를 사용하기 위해서는 다음과 같은 작업이 필요하다.

  1. EHCache 설치
  2. 캐시 설정 파일 작성
  3. CacheManager 생성
  4. CacheManager로부터 구한 Cache를 이용한 CRUD 작업 수행
  5. CacheManager의 종료
EHCache 설치

EHCache 배포판은 http://ehcache.sourceforge.net/ 사이트에 다운로드 받을 수 있다. 배포판의 압축을 푼 뒤, ehcache-1.2.x.jar 파일이 생성되는 데, 이 파일을 클래스패스에 추가해준다. 또한, EHCache는 자카르타의 commons-logging API를 사용하므로, commons-logging과 관련된 jar 파일을 클래스패스에 추가해주어야 한다.

ehcache.xml 파일

EHCache는 기본적으로 클래스패스에 존재하는 ehcache.xml 파일로부터 설정 파일을 로딩한다. 가장 간단한 ehcache.xml 파일은 다음과 같이 작성할 수 있다.

<ehcache>
    <diskStore path="java.io.tmpdir"/>

    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="true"
            maxElementsOnDisk="10000000"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"
            />
    
    <cache name="simpleBeanCache"
            maxElementsInMemory="10"
            eternal="false"
            overflowToDisk="false"
            timeToIdleSeconds="300"
            timeToLiveSeconds="600"
            memoryStoreEvictionPolicy="LRU" />

</ehcache>

위 코드에서 <defaultCache> 태그는 반드시 존재해야 하는 태그로서, 코드에서 캐시를 직접 생성할 때 사용되는 캐시의 기본 설정값을 저장한다. <cache> 태그는 하나의 캐시를 지정할 때 사용된다. name 속성은 캐시의 이름을 지정하며, 코드에서는 이 캐시의 이름을 사용하여 사용할 Cache 인스턴스를 구한다.

설정 파일에 대한 자세한 내용은 뒤에서 살펴보기로 하자.

CacheManager 생성

ehcache.xml 파일을 작성했다면 그 다음으로 할 작업은 net.sf.ehcache.CacheManager 객체를 생성하는 것이다. CacheManager 객체는 다음의 두 가지 방법 중 한가지 방식을 사용하여 생성할 수 있다.

  • CacheManager.create() : 싱글톤 인스턴스 사용
  • new CacheManager() : 새로운 CacheManager 인스턴스 생성
CacheManager.create() 메소드는 싱글톤 인스턴스를 생성하기 때문에 최초에 한번 호출될 때에만 CacheManager의 초기화 작업이 수행되며, 이후에는 동일한 CacheManager 인스턴스를 리턴하게 된다. 아래는 CacheManager.create() 메소드의 사용 예이다.

CacheManager cacheManager = CacheManager.create();

싱글톤 인스턴스가 아닌 직접 CacheManager 객체를 조작하려면 다음과 같이 new를 사용하여 CacheManager 인스턴스를 생성해주면 된다.

CacheManager cacheManager = new CacheManager();

두 방식 모두 클래스패스에 위치한 ehcache.xml 파일로부터 캐시 설정 정보를 로딩한다.

만약 클래스패스에 위치한 ehcache.xml 파일이 아닌 다른 설정 파일을 사용하고 싶다면 다음과 같이 URL, InputStream, 또는 String(경로) 객체를 사용하여 설정 파일의 위치를 지정할 수 있다.

URL configFile = this.getClass().getResource("/ehcache_config_replicate.xml")
CacheManager cacheManager = new CacheManager(configFile);

Cache에 CRUD 수행

CacheManager 인스턴스를 생성한 다음에는 CacheManager 인스턴스로부터 Cache 인스턴스를 구하고, Cache 인스턴스를 사용하여 객체에 대한 캐시 작업을 수행할 수 있게 된다. 

Cache 구하기
net.sf.ehcache.Cache 인스턴스는 CacheManager.getCache() 메소드를 사용하여 구할 수 있다.

CacheManager cacheManager = new CacheManager(configFileURL);
Cache cache = cacheManager.getCache("simpleBeanCache");

CacheManager.getCache() 메소드에 전달되는 파라미터는 ehcache.xml 설정 파일에서 <cache> 태그의 name 속성에 명시한 캐시의 이름을 의미한다. 지정한 이름의 Cache 인스턴스가 존재하지 않을 경우 CacheManager.getCache() 메소드는 null을 리턴한다.

Create/Update 작업 수행
Cache 인스턴스를 구한 다음에는 Cache.put() 메소드를 사용하여 캐시에 객체를 저장할 수 있다. 아래 코드는 Cache.put() 메소드의 사용예이다.

Cache cache = cacheManager.getCache("simpleBeanCache");

SimpleBean newBean = new SimpleBean(id, name);
Element newElement = new Element(newBean.getId(), newBean);
cache.put(newElement);

Cache.put() 메소드는 net.sf.ehcache.Element 객체를 전달받는다. Element 클래스는 캐시에 저장될 원소를 나타내며, 키와 값을 사용하여 원소를 표현한다. Element 객체를 생성할 때 첫번째 파라미터는 원소의 키를 의미하며, 두번째 파라미터는 원소의 값을 의미한다.

EHCache는 캐시에 저장될 각각의 객체들을 키를 사용하여 구분하기 때문에, Element 객체를 생성할 때 (의미상) 서로 다른 객체는 서로 다른 키를 사용해야 한다.

Map과 마찬가지로 EHCache가 제공하는 Cache는 삽입을 하거나 기존의 값을 수정할 때 모두 Cache.put() 메소드를 사용한다. 기존에 캐시에 저장된 객체를 수정하길 원한다면 다음과 같이 동일한 키를 사용하는 Element 객체를 Cache.put() 메소드에 전달해주면 된다.

Element newElement = new Element(id, someBean);
cache.put(newElement);
...
Element updatedElement = new Element(id, updatedBean);
cache.put(updatedElement);

Read 작업 수행
Cache에 보관된 객체를 사용하려면 Cache.get() 메소드를 사용하면 된다. Cache.get() 메소드는 키를 파라미터로 전달받으며, 키에 해당하는 Element 객체를 리턴하며 관련 Element과 존재하지 않을 경우 null을 리턴한다. 아래 코드는 Cache.get() 메소드의 사용예이다.

Element element = cache.get(key);
SimpleBean bean = (SimpleBean) element.getValue();

Element.getValue() 메소드는 캐시에 저장된 객체를 리턴한다. 만약 Serializable 하지 않은 객체를 값으로 저장했다면 다음과 같이 Element.getObejectValue() 메소드를 사용하여 값을 구해야 한다.

Element element = cache.get(key);
NonSerializableBean bean = (NonSerializableBean) element.getObjectValue();

Delete 작업 수행
Cache에 보관된 객체를 삭제하려면 Cache.remove() 메소드를 사용하면 된다. 아래 코드는 Cache.remove() 메소드의 사용예이다.

boolean deleted = cache.remove(key);

Cache.remove() 메소드는 키에 해당하는 객체가 존재하여 삭제한 경우 true를 리턴하고, 존재하지 않은 경우 false를 리턴한다.

CacheManager의 종료

사용이 종료된 CacheManager는 다음과 같이 shutdown() 메소드를 호출하여 CacheManager를 종료해야 한다.

cacheManager.shutdown();

Cache 값 객체 사용시 주의사항

캐시에 저장되는 객체는 레퍼런스가 저장된다. 따라서, 동일한 키에 대해 Cache.put()에 전달한 Element의 값과Cache.get()으로 구한 Element의 값은 동일한 객체를 참조하게 된다.

SimpleBean bean = ...;
Element element = new Element(key, bean);
cache.put(element);

Element elementFromCache = cache.get(key);
SimpleBean beanFromCache = (SimpleBean)elementFromCache.getValue();

(bean == beanFromCache); // true
(element == elementFromCache); // false

위 코드에서 Cache.put()에 전달된 element 객체와 Cache.get()으로 구한 elementFromCache 객체는 서로 다른 객체이다. 하지만, 두 Element 객체가 갖고 있는 값은 동일한 객체를 참조하고 있다. 따라서, 캐시에 값으로 저장된 객체를 변경하게 되면 캐시에 저장된 내용도 변경되므로, 캐시 사용시 이 점에 유의해야 한다.

캐시 설정

캐시 설정 파일에 <cache> 태그를 이용하여 캐시를 설정했었다. 캐시 설정과 관련하여 <cache> 태그는 다양한 속성을 제공하고 있는데, 이들 속성에는 다음과 같은 것들이 존재한다.

name캐시의 이름필수
maxElementsInMemory메모리에 저장될 수 있는 객체의 최대 개수필수
eternal이 값이 true이면 timeout 관련 설정은 무시되고, Element가 캐시에서 삭제되지 않는다.필수
overflowToDisk메모리에 저장된 객체 개수가 maxElementsInMemory에서 지정한 값에 다다를 경우 디스크에 오버플로우 되는 객체는 저장할 지의 여부를 지정한다.필수
timeToIdleSecondsElement가 지정한 시간 동안 사용(조회)되지 않으면 캐시에서 제거된다. 이 값이 0인 경우 조회 관련 만료 시간을 지정하지 않는다. 기본값은 0이다.선택
timeToLiveSecondsElement가 존재하는 시간. 이 시간이 지나면 캐시에서 제거된다. 이 시간이 0이면 만료 시간을 지정하지 않는다. 기본값은 0이다.선택
diskPersistentVM이 재 가동할 때 디스크 저장소에 캐싱된 객체를 저장할지의 여부를 지정한다. 기본값은 false이다.선택
diskExpiryThreadIntervalSecondsDisk Expiry 쓰레드의 수행 시간 간격을 초 단위로 지정한다. 기본값은 120 이다.선택
memoryStoreEvictionPolicy객체의 개수가 maxElementsInMemory에 도달했을 때,모메리에서 객체를 어떻게 제거할 지에 대한 정책을 지정한다. 기본값은 LRU이다. FIFO와 LFU도 지정할 수 있다.선택

아래 코드는 몇 가지 설정 예이다.

<!--
sampleCache1 캐시. 최대 10000개의 객체를 저장할 수 있으며, 
5분 이상 사용되지 않거나 또는 10분 이상 캐시에 저장되어 있을 경우 
캐시에서 제거된다. 저장되는 객체가 10000개를 넘길 경우, 
디스크 캐시에 저장한다.
-->
<cache name="sampleCache1"
       maxElementsInMemory="10000"
       maxElementsOnDisk="1000"
       eternal="false"
       overflowToDisk="true"
       timeToIdleSeconds="300"
       timeToLiveSeconds="600"
       memoryStoreEvictionPolicy="LFU"
       />

<!--
sampleCache2 캐시. 최대 1000개의 객체를 저장한다. 
오버플로우 된 객체를 디스크에 저장하지 않기 때문에 
캐시에 최대 개수는 1000개이다. eternal이 true 이므로, 
timeToLiveSeconds와 timeToIdleSeconds 값은 무시된다.
-->
<cache name="sampleCache2"
       maxElementsInMemory="1000"
       eternal="true"
       overflowToDisk="false"
       memoryStoreEvictionPolicy="FIFO"
       />

<!--
sampleCache3 캐시. 오버플로우 되는 객체를 디스크에 저장한다.
디스크에 저장된 객체는 VM이 재가동할 때 다시 캐시로 로딩된다.
디스크 유효성 검사 쓰레드는 10분 간격으로 수행된다.
-->
<cache name="sampleCache3"
       maxElementsInMemory="500"
       eternal="false"
       overflowToDisk="true"
       timeToIdleSeconds="300"
       timeToLiveSeconds="600"
       diskPersistent="true"
       diskExpiryThreadIntervalSeconds="600"
       memoryStoreEvictionPolicy="LFU"
       />

분산 캐시

EHCache는 분산 캐시를 지원한다. EHCache는 피어(peer) 자동 발견 및 RMI를 이용한 클러스터간 데이터 전송의 신뢰성 등 분산 캐시를 위한 완전한 기능을 제공하고 있다. 또한, 다양한 옵션을 통해 분산 상황에 맞게 설정할 수 있도록 하고 있다.

참고로, EHCache는 RMI를 이용하여 분산 캐시를 구현하고 있기 때문에, Serializable 한 객체만 분산 캐시에서 사용 가능하다. 키 역시 Serializable 해야 한다.

분산 캐시 구현 방식

EHCache는 한 노드의 캐시에 변화가 생기면 나머지 노드에 그 변경 내용을 전달하는 방식을 사용한다. 즉, 클러스터에 있는 캐시 인스턴스가 n개인 경우, 한번의 변경에 대해 n-1개의 변경 통지가 발생한다.

각 노드의 캐시간 데이터 전송은 RMI를 통해서 이루어진다. EHCache가 데이터 전송 기술로서 RMI를 사용하는 이유는 다음과 같다.

  • 자바에서 기본적으로 제공하는 원격 메커니즘
  • 안정화된 기술
  • TCP 소켓 옵션을 튜닝할 수 있음
  • Serializable 한 객체를 지원하기 때문에, 데이터 전송을 위해 XML과 같은 별도의 포맷으로 변경할 필요가 없음
노드 발견

EHCache는 클러스터에 새로운 노드가 추가돌 경우 해당 노드를 자동적으로 발견하는 방식과, 지정된 노드 목록에 대해서만 클러스터의 노드로 사용하는 방식을 지원하고 있다.

멀티캐스트 방식

멀티캐스트 모드를 사용한 경우, 지정한 멀티캐스트 IP(224.0.0.1~239.255.255.255)와 포트에 참여하는 노드를 자동으로 발견하게 된다. 지정한 IP와 포트에 참여한 노드는 자기 자신을 다른 노드에 통지한다. 이 방식을 사용하면 클러스터에 동적으로 노드를 추가하거나 제거할 수 있다.

노드 목록 지정 방식

클러스터에 포함되는 노드 목록을 지정한다. 동적으로 새로운 노드를 추가하거나 기존 노드를 제거할 수 없다.

분산 캐시 설정

분산 캐시를 사용하기 위해서는 다음과 같은 세 개의 정보를 지정해주어야 한다.

  • CacheManagerPeerProvider - 피어 발견 관련 설정
  • CacheManagerPeerListener - 메시지 수신 관련 설정
  • 캐시별 CacheReplicator - 메시지 생성 규칙 설정
CacheManagerPeerProvider 설정

CacheManagerPeerProvider는 새롭게 추가된 노드를 발견하는 방식을 지정한다.

노드를 자동으로 발견하는 멀티캐스트 방식을 사용하려면 다음과 같이 설정한다.

<cacheManagerPeerProviderFactory
    class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
    properties="peerDiscovery=automatic, 
                    multicastGroupAddress=230.0.0.100, multicastGroupPort=1234" />

위 코드에서 properties 속성의 값에 사용된 프로퍼티는 다음과 같다.

peerDiscoveryautomatic으로 지정하면 멀티캐스트 방식을 사용한다.
multicaseGroupAddress멀티캐스트 IP
multicaseGroupPort포트 번호

하나의 클러스터에 포함될 노드들은 동일한 멀티캐스트 IP와 포트 번호를 사용해야 한다.

클러스터에 참여할 노드 목록을 지정하는 IP 방식을 사용하려면 다음과 같이 설정한다.

<cacheManagerPeerProviderFactory
    class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
    properties="peerDiscovery=manual, 
                    rmiUrls=//server2:12345/cache1|//server2:12345/cache2" />

위 코드에서 properties 속성의 값에 사용된 프로퍼티는 다음과 같다.

peerDiscoverymanual로 지정한 IP 지정 방식이다.
rmiUrls분산 노드에 참여할 서버 및 캐시 목록을 지정한다. 현재 노드의 정보는 포함시켜서는 안 된다.

이 경우, rmiUrls에 명시된 포트 번호는 뒤에 살펴볼 CacheManagerPeerListener가 사용할 포트 번호를 지정해주어야 한다.

CacheManagerPeerListener 설정

노드를 발견하는 방식을 지정했다면, 다음으로 할 작업은 클러스터에 있는 다른 노드에서 발생한 변경 정보를 수신할 때 사용할 포트 번호를 지정하는 것이다. 다음과 같은 코드를 이용하여 수신과 관련된 포트 번호를 설정할 수 있다.

<cacheManagerPeerListenerFactory
    class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
    properties="port=12345, socketTimeoutMillis=120000" />

위 코드에서 properties 속성의 값에 사용된 프로퍼티는 다음과 같다.

port메시지를 수신할 때 사용되는 포트
socketTimeoutMillis이 노드에 메시지를 보냈을 때 메시지 전송을 기다리는 시간. 기본값은 2000ms.

캐시별 CacheReplicator 설정

분산 환경에 적용되어야 하는 캐시는 캐시의 내용이 변경되었을 때 다른 노드에 있는 캐시에 변경 내역을 알려주어야 한다. <cacheEventListenerFactory> 태그를 사용하면, 언제 어떻게 캐시의 변경 내역을 통지할지의 여부를 지정할 수 있다. 아래 코드는 설정의 예이다.

<cache name="simpleBean"
      maxElementsInMemory="100"
      eternal="false"
      overflowToDisk="false"
      timeToIdleSeconds="300"
      timeToLiveSeconds="600"
      memoryStoreEvictionPolicy="LRU">
       <cacheEventListenerFactory 
           class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" 
           properties="replicateUpdatesViaCopy=true,replicateUpdates=true" />
</cache>

위 코드와 같이 <cacheEventListenerFactory>의 구현 클래스로 RMICacheReplicatorFactory를 지정하면 캐시에 변경이 생길 때 마다 해당 변경 내역을 클러스터에 참여하고 있는 노드의 캐시에 통지하게 된다. properties 속성에 프로퍼티를 지정하면, 캐시 요소의 추가, 변경, 삭제 등에 대해 통지 방식을 적용할 수 있다. 설정할 수 있는 프로퍼티는 다음과 같다.

replicatePuts캐시에 새로운 요소가 추가됐을 때 다른 노드에 복사할지의 여부
replicateUpdates캐시 요소의 값이 변경되었을 때 다른 노드에 값을 복사할지의 여부
replicateRemovals캐시 요소가 삭제되었을 때 다른 노드에 반영할지의 여부
replicateAsynchronously비동기로 값을 복사할지의 여부
replicateUpdatesViaCopy새로운 요소를 다른 노드에 복사할 지 아니면 삭제 메시지를 보낼지의 여부
asynchronousReplicationIntervalMillis비동기 방식을 사용할 때 변경 내역을 다른 노드에 통지하는 주기. 기본값은 1000.

위 속성의 기본값은 모두 true이다. 따라서, 기본 설정값을 사용하려면 다음과 같이 properties 속성을 사용하지 않아도 된다.

<cache name="simpleBean" ...
      memoryStoreEvictionPolicy="LRU">
       <cacheEventListenerFactory 
           class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" />
</cache>

어플리케이션 구동시 캐시 데이터 로딩하기

CacheManager가 초기화 될 때, 클러스터에 있는 다른 캐시로부터 데이터를 로딩할 수 있다. 이는 초기 구동이 완료된 후 곧 바로 서비스를 제공할 수 있음을 의미한다. 초기 구동시 다른 노드로부터 캐시 데이터를 로딩하려면 다음과 같이 <bootstrapCacheLoaderFactory> 태그의 구현 클래스를 RMIBootstrapCacheLoaderFactory로 지정해주면 된다.

<cache name="simpleBean" ...
      memoryStoreEvictionPolicy="LRU">
       <bootstrapCacheLoaderFactory
           class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"
           properties="bootstrapAsynchronously=true,
                       maximumChunkSizeBytes=5000000" />

       <cacheEventListenerFactory 
           class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" />
</cache>

RMIBootstrapCacheLoaderFactory에 전달 가능한 프로퍼티 목록은 다음과 같다.

bootstrapAsynchronously비동기적으로 수행할지의 여부를 지정
maximumChunkSizeBytes클러스터의 다른 노드로부터 로딩 가능한 데이터의 최대 크기

RMIBoostrapCacheLoaderFactory를 설정하면 캐시를 초기화 할 때, 원격지 노드의 캐시에 저장된 데이터를 로딩하여 로컬 캐시에 저장한다.

분산 캐시 고려사항

분산 캐시를 사용할 때에는 다음과 같은 내용을 고려해야 한다.

  • 노드 증가에 따라 네트워크 트래픽 증가:
    많은 양의 네트워크 트래픽이 발생할 수 있다. 특히 동기 모드인 경우 성능에 영향을 받을 수 있다. 비동기 모드인 경우 버퍼에 변경 내역을 저장하였다가 일정한 주기로 버퍼에 쌓인 내역을 다른 노드에 통지하기 때문에 이 문제를 다소 완하시킬 수 있다.
  • 데이터 불일치 발생 가능성:
    두 노드에서 동시에 동일한 캐시의 동일한 데이터에 대한 변경을 수행할 경우, 두 노드 사이에 데이터 불일치가 발생할 수 있다. 캐시 데이터의 불일치가 매우 심각한 문제가 될 경우, 동기 모드(replicateAsynchronously=false)와 복사 메시지 대신 삭제 메시지를 전송(replicateUpdatesViaCopy=false)함으로써 이 문제를 해결할 수 있다.
관련링크:


출처 - http://javacan.tistory.com/123






Ehcache는 가장 표준적인 캐시의 모습을 한 오픈소스 캐시 라이브러리입니다. 얼마전 JCO에 갔을때 보니 이번엔 정말 캐시가 화두인것 같더군요. 하지만 당시에 발표된 대부분의 캐시 시스템은 Memcached, Redis같은 클러스터 기반의 외부형 초대규모 캐시들이었습니다. 이번에 간단하게 쓸 수 있는 캐시 라이브러리가 없을까 검색하다 보니 발견한 ehcache-spring-annotation 라이브러리는 캐시사용을 정말 쉽고 간단하게 만들어 줍니다.

자주 바뀌지 않는 결과물인, 가령 공지사항이라던가 페이지의 첫화면(index)과 같은 경우에 적극적으로 활용할 수 있을것 같습니다. 이번에 쓰는 글은 메이븐 기반의 프로젝트 구성을 할 예정입니다. 지식이 부족하시다면 [관련글]을 좀 더 보시고 오시면 도움이 될것입니다. 스프링 프로젝트의 구성에는 충분히 이해도가 높은 분들이 보시는 글이라 생각하고 그런 부분은 종종 건너 뛰도록 하겠습니다.

Maven 프로젝트 구성

임시로 사용할 프로젝트이니 다음과 같은 정도만 등록해주시면 될것 같습니다. XML을 올릴까 싶었지만 글중에 스크린샷 하나쯤 등장하는게 아름다울것 같아 다음의 스크린샷으로 대신합니다.
사용자 삽입 이미지

스프링 프로젝트 컨텍스트 설정에 ehcache 설정 추가

스프링의 컨텍스트 설정 XML에 다음의 코드를 추가해 줍니다. 설정파일이 여러개로 분산되어있다면 어디에 들어가던지 순서는 상관없습니다.
<ehcache:annotation-driven />
 
<ehcache:config cache-manager="cacheManager">
 
<ehcache:evict-expired-elements interval="300" />
</ehcache:config>

<bean id="cacheManager"
 
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
 
<property name="configLocation" value="/WEB-INF/cache/ehcache.xml" />
</bean>

여기서 중요한점은 <beans> 네임스페이스 선언에 다음이 추가되어야 합니다.
xmlns:ehcache="http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring"
xsi
:schemaLocation="http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring/ehcache-spring-1.1.xsd"

보기가 어렵지만 달리 방법이 없네요. 결과적으로 다음과 같은 모습이 됩니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xmlns:ehcache="http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring"
        xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring/ehcache-spring-1.1.xsd">
       
<ehcache:annotation-driven />

<ehcache:config cache-manager="cacheManager">
 
<ehcache:evict-expired-elements interval="300" />
</ehcache:config>

<bean id="cacheManager"
 
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
 
<property name="configLocation" value="/WEB-INF/cache/ehcache.xml" />
</bean>

</beans>

Ehcache 설정 파일 생성

이번엔 Ehcache의 설정 파일을 추가해주어야 합니다. 위에서 /WEB-INF/cache/ehcache.xml에 선언해 두었으므로 해당 위치에 추가해 주어야 합니다.
<?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="32M" maxBytesLocalOffHeap="128M"
 
maxBytesLocalDisk="0M" updateCheck="false">
       
 
<cache
   
name="CacheExample"
        maxElementsInMemory="100"
        eternal="false"
        overflowToDisk="false"
        timeToLiveSeconds="30" />
   
</ehcache>

위의 설정을 대충 보자면 CacheExample이라는 이름의 캐시를 생성을 합니다. 한번에 최대 100개의 엘리먼트를 보관(maxElementsInMemory)할 수 있으며 영원히 저장하지 않고(eternal) 특정 상황에서 삭제 되어도 상관없다는 설정이 되어있습니다. 디스크까지 저장하면 캐시의 의미가 퇴색되므로 디스크는 절대로 사용하지 않을것이고(overflowToDisk) 한번 꺼내온 데이터는 30초동안만 유지하겠습니다(timeToLiveSeconds). 30초가 지나면 데이터를 파기합니다.

<ehcache> 선언부에 있는 설정들은 전체 캐시를 총괄하는 글로벌 설정입니다.

DAO 클래스에 캐시 추가하기

다음과 같이 크게 기억해둘 부분은 두가지 입니다. 설정된 캐시 정보대로 값을 캐시하며 실제 내부 로직이 실행될지 캐시가 반환 될지를 결정할 @Cacheable 어노테이션과 캐시 설정과 상관없이 당장 캐시를 삭제하는 @TriggersRemove 어노테이션을 잘 봐두시면 됩니다.
@Repository
public class CacheDao
{
 
@TriggersRemove(cacheName="CacheExample", removeAll=true, when=When.AFTER_METHOD_INVOCATION)
 
public void insertDummy()
 
{
        // 캐시 삭제 예시를 위해 존재하는 메소드
 
}

 
@Cacheable(cacheName="CacheExample")
 
public String getDummy()
 
{
   
// 캐시가 존재할 경우 이 로직은 실행되지 않고 캐시가 바로 반환됩니다.
   
return new Date().toString();
 
}
}

테스트용 컨트롤러 제작하기

지금 보여드릴 코드는 순전히 테스트를 위한 코드입니다. 데이터를 가져오는 부분과 데이터를 날리는 부분이 있습니다.
@Controller
@RequestMapping("/example")
public class CacheController
{
 
@Autowired private CacheService cacheService;
 
 
@RequestMapping("/main")
 
public String showIndex(Model model)
 
{
    model
.addAttribute("now", cacheService.getDummy());
   
return "index";
 
}
 
 
@RequestMapping("/flush")
 
public String flushCache()
 
{
    cacheService
.insertDummy();
   
return "redirect:/example/main";
 
}
}


showIndex가 실행이 되면 cacheService를 통해 cacheDao를 통해 값을 가져오게 됩니다. 아까 설정했던 캐시 규칙에 의거해 한번 캐시된 데이터는 100개의 데이터를 넘지 않는 이상 30초간 캐시 됩니다. 뷰에서는 꺼내온 데이터를 그대로 찍도록 만들어져 있습니다.

캐싱 테스트 해보기


사용자 삽입 이미지
실행을 해보니깐 현재의 시간이 잘 출력이 됩니다. 하지만 다시한번 재 요청을 해보시면 시간이 변하지 않는것을 확인하실 수 있습니다. 계속 재 요청을 해도 같은 화면이 출력이 되며 30초 뒤에 캐시가 사라졌을법한 뒤에 다시 요청을 해 보시면 다시 DAO의 내부 로직이 실행되어 현재 시간이 출력됩니다.

여기서 flushCache를 실행하게 되면 곧바로 캐시가 삭제되어(실제로는 새로운 데이터가 입력되어 전체 데이터에 변동이 일어나 더이상 기존의 캐시가 유효하지 않게 되었으므로 삭제하는것이겠죠) 더이상 캐시가 유효하지 않게 되고 이후 원래 화면으로 리다이렉트 되면 다시 캐시없는 상태에서 데이터 가져오기가 실행되게 됩니다. 예시로 만들었던 소스를 올려 놓겠습니다.



참고:
http://code.google.com/p/ehcache-spring-annotations/w/list
http://blog.goyello.com/2010/07/29/quick-start-with-ehcache-annotations-for-spring/ 


출처 - http://theeye.pe.kr/entry/simple-way-of-caching-my-data-access-object-with-spring-annotation






spring cache의 적용(ehcache)


처리 속도 개선을 위해 cache를 적용했다.

시스템간 연동으로 접속부하, 네트웍부하가 동시에 생겨 속도저하가 심각한 화면이 생김!!

문제는 조회조건부 - 회계년도, 부서, 코드관련..... 
가져오는 데이터는 거의 동일한데도...
화면이 바뀌거나, 조회콤보 선택처리에 DB까지 데이터 엑세스가 반복적으로 발생.

이 부분에 cache를 적용해볼 생각으로 자료를 찾았더니 가장 많이 선택된 솔루션이 ehcache.....
ehcache를 심도있게 학습하고 적용할 상황은 아니라서 
spring에 annotation기반으로 빠르게 적용할 수 있는 방법을 찾음 ehcache-spring-annotations

현재 개발중인 시스템에 적용된 spring버전은 3.0.5.RELEASE이다.
maven을 사용하고 있다면 다음과 같은 dependency추가가 필요하다.

<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context-support</artifactId>
   <version>3.0.5.RELEASE</version>
</dependency>

<dependency>
   <groupId>com.googlecode.ehcache-spring-annotations</groupId>
   <artifactId>ehcache-spring-annotations</artifactId>
   <version>1.1.3</version>
</dependency> 

위 2개의 dependency를 추가하면 그와 연결된 관련 dependency까지 setting이 완료된다. 
maven의 혜택...ㅋ~~~~ 

maven에 의해 ehcache관련 필수 라이브러리가 classpath상에 위치하게 되었으므로
설정과 코드수정만 남았다.

먼저 beans.xml 또는 applicationContext.xml 등 spring configuration파일의 
네임스페이스 부분에 추가설정이 필요

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:ehcache="http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring"
xsi:schemaLocation="http://www.springframework.org/schema/beans 
          http://www.springframework.org/schema/beans/spring-beans.xsd
          http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring
                  http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring/ehcache-spring-1.1.xsd">
 
짙게 표시된 부분이  추가한 내용이다.

그리고 동일 설정파일에 아래 코드를 추가한다.

  <!-- declare ehCache -->
  <ehcache:annotation-driven />
  
  <ehcache:config cache-manager="cacheManager">
    <ehcache:evict-expired-elements interval="60" />
  </ehcache:config>
    
  <!-- ehCache bean -->
  <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" >
    <property name="configLocation"  value="/WEB-INF/ehcache.xml"/>
  </bean> 
 
이제 설정 작업은 끝났다.
WEB-INF/ root 디렉토리에 다음과 같은 내용의 ehcache.xml파일을 생성하자.

<?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">   
    
    <defaultCache 
     eternal="false" 
     maxElementsInMemory="1000"
        overflowToDisk="false" 
        diskPersistent="false" 
        timeToIdleSeconds="0"
        timeToLiveSeconds="600" 
        memoryStoreEvictionPolicy="LRU"/>

    <cache 
     name="budgetCache" 
     eternal="false"
        maxElementsInMemory="100" 
        overflowToDisk="false" 
        diskPersistent="false"
        timeToIdleSeconds="0" 
        timeToLiveSeconds="300"
        memoryStoreEvictionPolicy="LRU" />
        
    <cache 
     name="accountCache" 
     eternal="false"
        maxElementsInMemory="100" 
        overflowToDisk="false" 
        diskPersistent="false"
        timeToIdleSeconds="0" 
        timeToLiveSeconds="300"
        memoryStoreEvictionPolicy="LRU" />
        
</ehcache>

배포파일의 element, 설정값 등은
환경에 맞게 수정해서 사용하면 될 것이다.

지금까지 한 작업을 정리해보면...
1. dependency추가 완료
2. spring configuration 파일(beans.xml 또는 applicationContext.xml 등 설정 파일) 설정 추가 완료
3. ehcache configuration 파일 작성 및 배포준비 완료(WEB-INF)

이제 실제 Java code에 적용하는 일만 남았다...
다음 코드는 @Cacheable annotation을 적용한 소스의 일부이다.

package pe.tuenas.prototype.soapservice.budget.impl;

import .... 생략

@WebService(
		endpointInterface="pe.tuenas.prototype.soapservice.budget.BudgetService", 
		serviceName="budgetService")
public class BudgetServiceImpl implements BudgetService {

    @Cacheable(cacheName = "budgetCache")
	@Override
	public List getBudgetTypeList(String workDate) {

		return getBudgetTypeDao().findBudgetTypeByFYear(
				getFinancialYearDao().findFinancialYearByDate(workDate));
	}
	
	@Cacheable(cacheName = "budgetCache")
	@Override
	public List getBudgetTypeListByBusinessYear(String businessYear) {
		return getBudgetTypeDao().findBudgetTypeByFYear(
				getFinancialYearDao().findFinancialYearByBusinessYear(businessYear));
	}
	
	@Cacheable(cacheName = "budgetCache")
	@Override
	public List getBusinessCodeList(String workDate) {

		return getBusinessCodeDao().findBusinessCodeByDate(workDate);
	}
}



@Cacheable annotation선언만으로 캐시기능은 훌륭하게 작동된다.
실제로 한게 너무 없어 황당할 정도...

서비스 영역의 캐시가 완료되면 client영역에서 최초 조회시 원래 속도로 나오고
반복적인 조회에서는 엄청난(?) 속도향상이 이뤄졌다.

cache적용이 유용한  부분은 
코드성 데이터 조회 부분, 
기본 데이터이지만 반복적으로 호출되는 부분  
출력물의 코드, 부서 매핑이 필요한 부분 등일 것이다..

웹상에 공개된 문서나 자료만을 참고해서 급하게 적용하느라 
심도있는 이해나 준비가 부족했다.
추후 보완이 필요한 부분이다.


출처 - http://ironheel.tistory.com/44











'Computer Science > Cache' 카테고리의 다른 글

Hazelcast 소개  (0) 2012.11.06
Memcached 설치 및 사용 방법  (0) 2012.11.06
Memcached의 확장성 개선  (0) 2012.11.06
memcached를 적용하여 사이트 성능 향상  (0) 2012.11.06
Memcached 구현 소개  (0) 2012.11.06
Posted by linuxism
,