본문 바로가기
TWIL

[TWIL] Ehcache Replication으로 세션공유하기

by amungstudy 2025. 2. 28.
캐시 라이브러리인 Ehcache를 이용하여 이중화 구성에서 세션을 공유하는 방법에 대해 정리해보았습니다.
기준 버전은 Ehcache 2.10 입니다.

만약 인증을 담당하는 인증 서버가 여러대일때, 우리는 어떻게 세션 공유를 할 수 있을까요?

저희 팀은 이를 위해 EHcache의 RMI Replication을 이용하고 있어요.

 

1. 클라이언트가 로그인 하면 인증토큰을 발급

2. Ehcache에 인증 토큰을 저장

3. Ehcache Replication을 사용하여 다른 서버에도 자동으로 복제

4. 이후 사용자가 다른 서버로 접속해도 동일한 인증토큰으로 인증 상태를 유지

 

이런 흐름으로 세션이 공유되고 있습니다. 

 

그럼 지금부터 Ehcache에 대해 알아보고 어떻게 사용하는지 알아볼게요.

 

 

#1. Ehcache가 뭐야?

Ehcache는 Java 기반의 인메모리 캐시 라이브러리입니다.

캐시를 저장할 때 Java의 Heap Memory를 사용하기 때문에

Ehcache를 사용하려면 Java 애플리케이션 내부에 포함이 되어 있어야 합니다. 

(Ehcache3에서는 Off-heap memory를 사용하지만 안타깝게도 우리팀은 Ehcache2를 사용...)

 

- 장점 : 로컬 메모리 기반 접근으로 매우 빠른 읽기, 쓰기 성능

- 단점 : 애플리케이션 인스턴스와 함께 동작하므로, 애플리케이션 인스턴스 장애 시 캐시 데이터도 손실 가능

Redis 와 EHCache 차이점

Redis EHcache
별도의 서버를 설치하고 관리하여야 함 별로의 서버 관리 없이 JVM 내에서 캐시를 사용할 수 있음
다양한 데이터 구조를 지원 Key,Value 형태의 Map 구조 지원
분산 캐시로 여러 애플리케이션 인스턴스 간 캐시 데이터 공유가 필요한 경우 적합 JVM 내에서의 빠른 로컬 캐시가 필요한 경우 적합

EHCache의 Storage Tier

EHCache에는 Storage Tier가 존재하며, 상위 단계일수록 빠릅니다

  • Memory Tier : In - Memory
  • OffHeap Tier : Memory Heap 외부에 저장. java GC가 적용되지 않음. 바이트 단위의 매우 큰 캐시를 생성.
  • Disk Tier : File 캐시와 유사. CacheManager를 이용하여 여러 디스크 저장소 경로를 구성 가능

 

#2. 어떻게 사용해?

[Cache 설정 속성]

공식문서를 참고하시는 것을 추천합니다.

  • timeToIdleSecond를 Cache에 설정하게 되면 모든 요소에 기본값으로 제공됩니다.
  • 만약 개별 Element에 timeToIdle 설정을 하는 경우 Element 수준의 설정이 더 우선적으로 적용됩니다.
Element element = new Element( member.getId(), member ); 
element.setTimeToIdle(14400); // 14400초 내에 캐시가 호출되지 않으면 삭제됨 
cache.put(element);

[Cache 작업 수행]

net.sf.ehcache.CacheManager인스턴스 생성 후 CacheManager 인스턴스로부터 Cache 인스턴스를 구하고,

Cache 인스턴스를 사용하여 객체에 대한 캐시 작업을 수행할 수 있습니다.

Cache 가져오기

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

파라미터로 Cache 설정에 들어간 캐시 이름을 전달합니다.

만약 지정한 이름의 Cache 인스턴스가 존재하지 않을 경우 null을 리턴합니다.

CacheManager cacheManager = new CacheManager( configFileURL ); // 다양한 방법 존재

Cache cache = cacheManager.getCache( <cache이름> );

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

Create/Update 작업

EHCache는 캐시에 저장될 각각의 객체들을 키를 사용하여 구분하고 있습니다.

net.sf.ehcache.Element 객체를 생성할 때 첫번째 파라미터가 바로 원소의 키를 의미하고, 두 번째 파라미터는 원소의 값을 의미합니다.

따라서 Cache에 객체를 저장하는 경우 Element 객체를 이용하여 Cache.put() 메서드를 사용합니다.

만약 기존에 캐시에 저장된 객체를 수정하길 원한다면 동일한 키를 사용하는 Element 객체를 Cache.put() 메서드에 전달하면 됩니다.

TestBean testBean = new TestBean( id, name );
Element newElement = new Element( testBean.getId(), testBean );
cache.put( newElement );

Read 작업

Cache.get() : key에 해당하는 Element 객체를 리턴, 존재하지 않을 경우 null을 리턴 합니다

Element.getValue() : 캐시에 저장된 객체를 리턴합니다

Element element = cache.get( <key> );
TestBean bean = ( TestBean ) element.getValue();

만약 Serializable 하지 않은 객체를 값으로 저장한 경우 Element.getObjectValue() 메서드를 사용해야합니다

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

getQuiet()

get()메서드와 달리 캐시 항목의 조회 횟수를 증가시키지 않고, 마지막 접근 시간을 업데이트 하지 않습니다.

따라서 캐시 만료 정책에 영향을 주지 않습니다.

Element element2 = cache.getQuiet( <key> );

Delete 작업

cache.remove( <key> );

정상 삭제 시 true 리턴, 존재하지 않는 경우 false을 리턴 합니다

 

[Cache EventListener]

캐시 이벤트 리스너를 사용하면 캐시에 이벤트가 발생했을때, 이벤트 처리를 할 수 있습니다. 

캐시 항목에 대해 추가, 갱신, 제거, 만료 등의 작업이 수행될 때 이를 감지하고, 이를 처리할 수 있는 메서드를 제공합니다.

 

주요 이벤트 종류는 다음과 같습니다:

  • notifyElementPut: 캐시에 항목이 추가될 때 호출됩니다.
  • notifyElementUpdated: 캐시 항목이 업데이트될 때 호출됩니다.
  • notifyElementRemoved: 캐시에서 항목이 제거될 때 호출됩니다.
  • notifyElementExpired: 캐시 항목이 만료될 때 호출됩니다.
  • notifyElementEvicted: 캐시 항목이 제거될 때(캐시의 eviction 정책에 의해) 호출됩니다.
  • notifyRemoveAll: 캐시에서 모든 항목이 제거될 때 호출됩니다

사용 방법

1) 캐시 이벤트 리스너를 사용하려면 CacheEventListener 인터페이스를 구현해야 합니다.

public class VerificationCacheEventListener implements CacheEventListener {


    @Override
    public void notifyElementRemoved(Ehcache cache, Element element) throws CacheException {

	// 이벤트 처리 코드 작성
        LOGGER.info("The verification-cache is unregistered. (key={})",element.getObjectKey());
    }
    ...

 

2) 캐시 이벤트 리스너를 CacheManager에 등록해줍니다

Ehcache cache = new net.sf.ehcache.Cache( config ); //  캐시 생성
cache.getCacheEventNotificationService().registerListener( new VerificationCacheEventListener() ); // 이벤트리스너 등록

cacheManager.addCache( cache );

 

* 주의점 : notifyElementExpired나 notifyElementEvicted와 같은 이벤트는 배경 스레드에서 비동기적으로 처리되기 때문에 즉시 이벤트가 발생하지 않을 수 있어요. ( 하지만 캐시에서 조회하면 값이 없는 것이 확인되고, 그제서야 Element 만료 이벤트가 발생하게 됨)

 

-> 즉시 이벤트 발생이 필요하다면, 다른 스레드로 주기적으로  evictExpiredElements() 메서드를 호출해서

Ehcache의 배경 스레드에서 만료된 항목을 즉시 처리하는 식으로 우회처리를 하는 작업이 필요합니다....

 

#3. Replication 은 어떻게 해?

앞서 다른 서버에 접근해도 인증 상태를 유지한다고 말씀드렸는데요,

이렇게 되려면 A서버의 캐시에 있는 데이터가 B서버의 캐시에도 동일하게 존재해야 되는거죠. 

이걸 Ehcache가 해줍니다. RMI를 이용해 분산캐시를 지원해요.

( RMI는 TCP/IP 기반으로 동작하는 원격 메소드 호출 기술입니다.

원격 객체와의 통신을 자동화하여 개발자가 복잡한 네트워크 프로그래밍을 하지 않도록 도와준다고 합니다. )

 

[RMI Replication의 동작 방식]

하나의 Ehcache 인스턴스에서 캐시 항목이 추가, 수정, 삭제되는 이벤트가 발생하면

다른 노드에 있는 캐시에 변경 내역을 알려줘야겠죠?

 

나 변경됐어~~~ 라고 알리는 것임....

 

1. replication 전파 

Ehcache에서는 RMI를 통해 복제된 인스턴스에 데이터 변경을 전파합니다. 

RMI URL을 통해 복제 대상 노드와 연결되며, 데이터가 변경될 때 마다 변경사항이 다른 노드로 전달됩니다.

 

2. 동기/비동기 복제 

Ehcache의 복제 방식은 두가지가 있는데요, 동기/비동기 방식입니다. 

- 동기 복제를 사용하면 데이터 일관성은 확보됩니다. 단, 캐시에 쓰기 지연 발생 가능(읽기작업은 즉시반환가능)

 

* RMI 설정 시 주의점 :

RMI는 네트워크를 통해 통신하기 때문에 노드 간 네트워크 통신이 정상적으로 이루어져야 합니다. 

(방화벽, 네트워트 설정 확인 필요)

 

 

더보기

저는 간단하게 로컬에서 애플리케이션 2개를 띄워서 테스트 해보려고 합니다.

 

피어 1 )

web서버 port : 8080

RMI URL : localhost:4001

 

피어 2 )

web서버 port : 8081

RMI URL : localhost:4002

 

[Replication 설정 방법]

1. ehcache.xml 파일에 RMI 관련 정보를 설정

RMI에 필요한 정보를 작성해줍니다.

Cache도 이 파일에 선언할 수 도 있는데, 저는 cache는 자바코드로 만들거라서 생략했습니다. 

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://ehcache.org/schema/ehcache http://ehcache.org/schema/ehcache/ehcache-2.10.xsd"
         xmlns="http://ehcache.org/schema/ehcache">

    <!--
    PeerProvider구성
    다른 서버의 rmiUrls=//서버:포트/캐시이름 를 작성
    -->
    <cacheManagerPeerProviderFactory
            class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
            properties="peerDiscovery=manual,rmiUrls=//localhost:4002/verification-cache"/>

    <!--
    PeerListener구성
    현재 CacheManager에 대한 피어의 메시지를 수신
    port= 리스너가 사용할 포트지정. 다른 캐시서버가 이 포트를 통해 연결함
    socketTimeoutMillis= 클라이언트 소켓이 이 리스너에 메시지를 보낼 때 적용되는 타임아웃
    -->
    <cacheManagerPeerListenerFactory
            class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
            properties="port=4001, socketTimeoutMillis=3000"/>

</ehcache>

 

그리고 CacheManager 인스턴스 생성할 때, 작성한 설정파일을 로딩해줍니다.

저는 /resources 경로 밑에 파일을 저장해서 이렇게 작성했습니다.

CacheManager cacheManager = CacheManager.create( getClass().getClassLoader().getResource("ehcache.xml") ); 
// XML 설정 파일 로드

 

 

2. 캐시별로 CacheReplicator 설정

cacheEventListenerFactory의 구현 클래스로 RMICacheReplicatorFactory를 지정하면 다른 노드의 캐시에 이벤트를 전파하게 됩니다. 

 

옵션을 사용해서 여러 설정이 가능합니다.

 

Ehcache 2.10 RMICacheReplicatorFactory 복제 관련 설정

옵션 설명 예시 기본값
replicatePuts PUT 요청에 대해 캐시 복제를 수행할지 여부 설정 true / false true
replicatePutsViaCopy PUT 요청에 대해 객체를 복사하여 복제할지 여부 설정 true / false true
replicateUpdates UPDATE 요청에 대해 캐시 복제를 수행할지 여부 설정 true / false true
replicateUpdatesViaCopy UPDATE 요청에 대해 객체를 복사하여 복제할지 여부 설정 true / false true
replicateRemovals REMOVE 요청에 대해 캐시 복제를 수행할지 여부 설정 true / false true
replicateAsynchronously 복제를 비동기 방식으로 수행할지 여부 설정 true / false true
asynchronousReplicationIntervalMillis 비동기 복제 수행 간격을 설정 (밀리초 단위) 1000 (1초) 1000

 

저는 ASYNC모드로 모든 것을 복제하고 싶어서 디폴트로 사용했어요.

CacheConfiguration config = new CacheConfiguration( name, 10000 ); // 로컬 힙 메모리 최대 항목 수

config.eternal( false ); // 만료되지 않는 항목이 아님
config.timeToLiveSeconds( 30 ); // TTL 설정
config.memoryStoreEvictionPolicy( "LFU" ); // 캐시 evict 방식

// 캐시별 CacheReplicator 설정
// ASYNC 모드로 모든것을 복제하는 replication 설정
CacheConfiguration.CacheEventListenerFactoryConfiguration cacheEventListenerFactoryConfiguration = new CacheConfiguration.CacheEventListenerFactoryConfiguration();
cacheEventListenerFactoryConfiguration.setClass("net.sf.ehcache.distribution.RMICacheReplicatorFactory");
config.addCacheEventListenerFactory( cacheEventListenerFactoryConfiguration );

 

 

3. 애플리케이션 구동 시 캐시 데이터 로딩 설정 

 

CacheManager가 초기화될 때, 다른 노드의 캐시로부터 데이터를 로딩할 수 있습니다.

이는 bootstrapCacheLoaderFactory의 구현 클래스로 RMIBootstrapCacheLoaderFactory를 지정하여 설정합니다.

 

RMIBootstrapCacheLoaderFactory 설정 옵션

옵션 설명 예시
bootstrapAsynchronously 캐시 데이터를 동기 또는 비동기로 로드할지 설정 true / false
maximumChunkSizeBytes 한 번에 로딩할 수 있는 최대 데이터 덩어리 크기 (바이트 단위) 5000000 (5MB)

 

저는 동기 수행으로 설정하였습니다.

// 애플리케이션 구동 시 다른 노드의 캐시 데이터 로딩 설정
// 동기 수행으로 설정하였음
CacheConfiguration.BootstrapCacheLoaderFactoryConfiguration bootstrapCacheLoaderFactoryConfiguration = new CacheConfiguration.BootstrapCacheLoaderFactoryConfiguration();
bootstrapCacheLoaderFactoryConfiguration.className("net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory");
bootstrapCacheLoaderFactoryConfiguration.properties("bootstrapAsynchronously=false, maximumChunkSizeBytes=5000000");
config.addBootstrapCacheLoaderFactory( bootstrapCacheLoaderFactoryConfiguration );

 

 

이렇게 설정해주면 애플리케이션이 시작될 때 서버 간 데이터 동기화가 이루어지기 때문에,

데이터 손실 없이 안정적으로 운영할 수 있습니다.

 

 

 

예를 들어 , 서버 1호기의 캐시에 데이터를 저장해놓은 상태에서 서버 2호기를 기동시키면

서버2호기에서도 1호기의 캐시에 저장해놓은 key로 데이터 조회가 가능합니다.

 

더보기

실제로 테스트 했을 때도 1호기에 캐시에 key=1로 저장한 후에

2호기를 실행시켜서 key=1인 element를 조회했을때

정상적으로 조회되는 것을 확인할 수 있었습니다~~!

 

****** 이렇게 끝나면 아쉬우니까, 간단하게 로그만 확인해보기******

1호기에서 인증토큰을 등록 요청하고

2호기에서 조회해볼게요

 

1호기 로그 ( 인증토큰 등록 )

2025-03-09T21:18:04.212+09:00  INFO 23744 --- [nio-8080-exec-2] s.com.VerificationCacheEventListener     : The verification-cache is registered. (key=123451)

 

2호기 로그 ( 복제 및 조회 )

 

2025-03-09T21:18:05.187+09:00  INFO 6916 --- [192.168.219.103] s.com.VerificationCacheEventListener     : The verification-cache is registered. (key=123451) (복제된로그)
...
2025-03-09T21:18:15.237+09:00  INFO 6916 --- [nio-8081-exec-1] study_web.com.HelloController            : userContexts=[UserContext{userId='11', name='kimsy1', token='123451'}] (select요청 정상 처리 완료)

 

이렇게 설정하고 방화벽과 네트워크 설정만 잘 되어있으면

캐시 replication이 정상적으로 동작하는 걸 확인하실 수 있습니다.

 

 

참고 :

https://www.ehcache.org/documentation/2.8/replication/rmi-replicated-caching.html#configuring-the-peer-provider

 

Ehcache

Replicated Caching using RMI Introduction Replicated caching using RMI is desirable because: RMI is the default remoting mechanism in Java it allows tuning of TCP socket options Element keys and values for disk storage must already be Serializable, therefo

www.ehcache.org

 

https://javacan.tistory.com/entry/133

 

EHCache를 이용한 캐시 구현

EHCache를 이용한 기본적인 캐시 구현 방법 및 분산 캐시 구현 방법을 살펴본다. EHCache의 주요 특징 및 기본 사용법 게시판이나 블로그 등 웹 기반의 어플리케이션은 최근에 사용된 데이터가 또 다

javacan.tistory.com