Ehcache
Ehcache 는 자바 프로그램 안에 넣어 쓰는 캐시입니다. 한 번 구한 값을 담아 두었다가 다음에 다시 꺼내 씁니다. 담아 두는 자리는 한 곳이 아니라 여러 계층으로 나뉩니다. 의존성으로 박아 쓰는 라이브러리입니다.
쉽고 빠른 이해
자바 프로그램 안에서 함께 도는 캐시 라이브러리입니다. 값 하나를 캐시에 넣어 두면(put), 같은 키로 다음 요청에서 그대로 다시 꺼낼 수 있습니다(get).
값을 다시 구하는 데 비용이 든다면 — 계산이든 원본 조회든 — 캐시가 그 비용을 줄여 줍니다.
- 캐시를 묶어 관리하는 것 하나 아래에, 이름 붙은 캐시 여러 개가 매달립니다.
- 값은 프로그램 메모리 안, 그 메모리 밖의 별도 영역, 디스크, 여러 프로세스가 나눠 쓰는 공유 자리까지 네 계층 중 고른 곳에 담깁니다.
- 어느 계층이 통째로 실패해도 호출자에게는 값을 못 찾았다는 답만 돌아갑니다.
대가는 둘입니다. 힙을 벗어난 계층은 값을 저장 가능한 형태로 바꿔야(직렬화) 담깁니다. 그리고 디스크 계층은 프로세스가 갑자기 죽으면 재시작 때 저장소를 통째로 지웁니다.
상세
Ehcache 를 쓰기 시작하려면 먼저 CacheManager(이하 캐시 매니저) 와 Cache 를 구성해야 한다고 공식 문서가 적습니다. 구성 방법은 둘입니다. 자바 코드로 짜는 프로그래매틱 설정과 XML(eXtensible Markup Language, 확장 마크업 언어) 입니다. 그리고 Cache 를 다루는 정규 경로는 CacheManager 를 거치는 것이라고 못 박습니다. 이전 판들에서도 그랬다는 설명이 붙어 있습니다.
Cache 는 키 타입과 값 타입을 선언해서 받습니다. Cache<Long, String> 이면 키가 Long 이고
값이 String 인 캐시입니다. 캐시 하나에는 이름이 붙습니다. 그 이름으로 CacheManager 에서
꺼냅니다.
저장 계층
Ehcache 는 계층 캐싱이라는 개념을 지원합니다. 설정할 수 있는 자리는 넷입니다.
| 계층 | 문서가 적는 것 |
|---|---|
| heap | 모든 캐시의 출발점입니다(온힙 · 힙이라고도 부릅니다). 직렬화가 필요 없어서 다른 계층보다 빠른 자리입니다 |
| offheap | 힙 바깥의 메모리 영역에 값을 담는 계층입니다. heap 처럼 참조를 그대로 두지 못하고 직렬화한 값만 담습니다 |
| disk | 디스크에 값을 담는 계층입니다. JVM(Java Virtual Machine, 자바 가상 머신)이 재시작해도 살아남을 수 있는데, 살아남는 조건은 뒤의 「포기한 것」에서 다룹니다 |
| clustered | 클라이언트가 별도 서버인 Terracotta Server Array 에 붙고 캐시 데이터가 거기에 저장됩니다. JVM 사이에 캐시를 공유하는 방법이기도 합니다 |
계층 선택지는 전부 단독으로 쓸 수 있습니다. 데이터를 offheap 에만 두는 캐시도, clustered 에만 두는 캐시도 유효한 설정이라고 문서가 예로 듭니다.
소스는 GitHub 의 ehcache/ehcache3 저장소에 있습니다. 라이선스는 Apache License 2.0 입니다. 저장소 README 는 3.x 라인이 현재 개발 라인이라고 밝힙니다.
포기한 것
힙 밖으로 나가는 값의 참조
Ehcache 는 자바 캐시지만 캐시에 담는 키-값 짝(매핑)을 언제나 자바 객체로 담을 수는 없다고 문서가 스스로 적습니다. 온힙 저장소(앞서 다룬 heap 계층입니다)만 참조로 담을 수 있습니다. 주어진 키와 값의 참조를 그대로 저장하는 방식입니다. 같은 온힙에서 값으로 담는 선택지도 있습니다. 이때는 키와 값의 복사본을 만들어 그 복사본을 저장합니다. 나머지 저장소는 전부 키·값 쌍의 바이트 표현만 담을 수 있습니다.
그래서 온힙이 아닌 저장소는 전부 어떤 형태로든 직렬화와 역직렬화가 필요합니다. 평범한 자바 객체를 내부에 담을 수 없고 이진 표현만 담을 수 있기 때문입니다. 이 자리를 메우는 추상이 Serializer 와 Copier 입니다. 참조 공유를 포기하고 얻은 것이 힙 밖의 세 계층입니다.
캐시 매니저에는 최적화된 Serializer 가 미리 설정되어 있고, 다루는 타입은 순서대로
java.io.Serializable, java.lang.Long, java.lang.Integer, java.lang.Float,
java.lang.Double, java.lang.Character, java.lang.String, byte[] 입니다.
디스크 계층의 공유
디스크 계층은 캐시 매니저끼리 공유할 수 없습니다. 퍼시스턴스는 캐시가 JVM 재시작에서 살아남는다는 뜻이고, 같은 위치에 CacheManager 를 다시 만들면 캐시에 있던 것이 그대로 남아 있습니다. 퍼시스턴스 디렉토리 하나는 한 시점에 캐시 매니저 하나의 전용입니다. 공유를 포기하고 대신 얻은 것이 재시작을 넘기는 것입니다.
살아남는 조건도 좁게 잡혀 있습니다. Ehcache 3 는 깨끗한 종료의 경우에만 퍼시스턴스를 제공합니다.
close() 가 불린 경우입니다. JVM 이 크래시하면 데이터 정합성 보장이 없습니다. 재시작 때 Ehcache 는
CacheManager 가 깨끗하게 닫히지 않았다는 것을 감지하고, 디스크 저장소를 쓰기 전에 통째로 지웁니다.
flowchart TD
A["CacheManager 재시작"] --> B{"이전 종료가 깨끗했나"}
B -->|"close() 로 닫힘"| C["디스크 저장소를 그대로 사용"]
C --> D["이전 캐시 데이터가 남아 있음"]
B -->|"JVM 크래시"| E["Ehcache 가 불완전 종료를 감지"]
E --> F["디스크 저장소를 쓰기 전에 통째로 지움"]
캐시 실패를 호출자에게 알리는 것
실패가 생기면 Ehcache 는 두 가지를 하려고 최선을 다한다고 문서가 적습니다. 계층마다 일관된
상태를 유지하는 것, 그리고 요청에 답하는 것입니다. 밑에 깔린 계층이 답하지 못해도 캐시는
호출자에게 답을 줍니다. 예를 들어 캐시가 get() 에 실패하면 null 을 돌려줍니다.
이 동작을 맡는 것이 ResilienceStrategy 입니다. 백엔드 계층이 실패할 때마다 StoreAccessException 을 던지고, 그것을 이 전략이 받아 처리합니다.
기본 구현은 둘입니다. 일반 캐시가 쓰는 RobustResilienceStrategy 와, 로더-라이터(CacheLoaderWriter, 캐시가 값을 읽고 쓸 때 그 값을 캐시 밖 저장소와 맞추는 인터페이스)가 붙은 캐시가 쓰는 RobustLoaderWriterResilienceStrategy 입니다. RobustResilienceStrategy 는 언제나 비어 있는 캐시처럼 동작합니다. 넣은 것이 즉시 축출(캐시에서 밀려나 사라지는 것)되는 것과 같습니다. 그래서 호출자는 캐시가 꺼져 있는 것과 거의 같게 동작하게 됩니다.
값으로 말하면 이것이 포기한 자리는 오류의 가시성입니다. 캐시 계층이 통째로 죽어도 애플리케이션은 미스(찾는 값이 없다는 답만 돌아오는 것)만 겪고 계속 돕니다. 그 대신 캐시가 죽었다는 사실이 반환값으로는 드러나지 않습니다.
클러스터로 공유한 값의 즉시 가시성
clustered 계층은 여러 JVM 이 서버(Terracotta Server Array) 하나를 통해 캐시를 공유하지만, 쓴 값이 언제 남에게 보이는지는 따로 골라야 합니다. Ehcache 가 제공하는 일관성 수준은 둘입니다. Eventual 과 Strong 입니다.
| 수준 | 문서가 적는 것 |
|---|---|
| Eventual | 쓰기 작업이 반환될 때 그 쓰기의 가시성이 보장되지 않습니다. 다른 클라이언트는 여전히 오래된 값을 볼 수 있습니다 |
| Strong | 쓰기 작업이 반환되면 다른 클라이언트가 그 값을 즉시 관찰할 수 있습니다 |
sequenceDiagram
participant A as 클라이언트 A
participant 서버
participant B as 클라이언트 B
A->>서버: 쓰기
서버-->>A: 반환
B->>서버: 읽기
Note over B: Eventual 이면 옛 값이 보일 수 있습니다
Eventual 이 포기한 자리가 이 가시성입니다. 쓰기가 돌아와도 옆 JVM 은 같은 키에서 옛 값을 볼 수 있습니다. 대신 보장이 하나 남습니다. 키 K 에 물린 값이 갱신되어 옛 값에서 새 값으로 바뀐 뒤, 클라이언트가 한 번 새 값을 보면 다시 옛 값을 보는 일은 없다고 문서가 적습니다.
Strong 은 그 가시성을 되돌려 줍니다. 값은 쓰기 쪽에서 치릅니다. 그 보장을 주기 위해 쓰기 작업에 지연 비용이 따라온다고 문서가 적습니다. 쓴 값이 바로 보여야 하는 쪽이 그 비용을 골라 삽니다.
기본 배포물 밖의 트랜잭션
트랜잭션은 기본 배포물 밖으로 나가 있습니다. Ehcache 3.1 이후의 jar 에는 트랜잭션 관련 코드가
들어 있지 않습니다. org.ehcache:ehcache-transactions 라는 별도 바이너리로 제공됩니다.
그 모듈을 붙이면 XA(eXtended Architecture, 여러 자원을 하나의 트랜잭션으로 묶는 분산 트랜잭션 규격) 트랜잭션 문맥 안에서 도는 캐시를 쓸 수 있습니다. 그 문맥을 제어하는 것은 JTA(Java Transaction API, 자바 트랜잭션 API) 트랜잭션 매니저입니다. 문맥 안에서는 2단계 커밋 프로토콜을 크래시 복구까지 지원합니다.
붙이고 나면 제약이 따라옵니다. 지원하는 격리 수준은 Read-Committed 하나뿐입니다. JTA 트랜잭션 문맥 밖에서 캐시에 접근하는 것은 금지됩니다. ABA(ABA Problem, 두 번 바뀐 값이 마지막에 처음 값과 같아지면 그 사이의 변경을 비교만으로는 알아채지 못하는 문제)에 대한 보호는 없습니다.
트랜잭션을 뺀 자리에서 기본 jar 가 얻은 것이 그 제약들의 부재입니다. 트랜잭션 매니저 없이 캐시를 엽니다. 아무 문맥에서나 값을 꺼냅니다. 트랜잭션이 필요한 쪽은 바이너리를 하나 더 얹습니다.
정리하면, 크래시 뒤에도 데이터가 반드시 남아야 하는 자리나 여러 자원을 하나의 트랜잭션으로 묶어야 하는 자리에는 기본 구성 그대로의 Ehcache 가 안 맞습니다.
예시
캐시 하나를 짓고 값을 넣는 최소 코드
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
.withCache("preConfigured",
CacheConfigurationBuilder.newCacheConfigurationBuilder(
Long.class, String.class, ResourcePoolsBuilder.heap(10)))
.build();
cacheManager.init();
Cache<Long, String> preConfigured =
cacheManager.getCache("preConfigured", Long.class, String.class);
Cache<Long, String> myCache = cacheManager.createCache("myCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(
Long.class, String.class, ResourcePoolsBuilder.heap(10)));
myCache.put(1L, "da one!");
String value = myCache.get(1L);
cacheManager.removeCache("preConfigured");
cacheManager.close();
공식 시작 안내서가 싣는 프로그래매틱 구성입니다. CacheManagerBuilder 로 캐시 매니저를 짓습니다.
withCache 로 preConfigured 라는 캐시를 미리 붙입니다. 키는 Long, 값은 String, 자원 풀은
heap(10) 입니다. 단위를 생략한 표기라 엔트리 10개를 뜻합니다(뒤의 「계층 크기」에서 다시
다룹니다). build() 로 만든 뒤 init() 을 불러야 씁니다. 미리 구성한 캐시는
getCache 로 꺼냅니다. createCache 는 그 자리에서 캐시를 새로 만듭니다. 값은 put 과 get 으로
드나듭니다. removeCache 로 캐시를 떼고 cacheManager.close() 로 캐시 매니저를 닫습니다.
의존성 좌표
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.9.6</version>
</dependency>
공식 다운로드 페이지가 싣는 Maven 스니펫입니다. 예로 든 판 번호이고, 쓰려는 Ehcache 판 번호로 바꾸라는 주의가 함께 적혀 있습니다.
JCache 로 여는 캐시 매니저
CachingProvider provider = Caching.getCachingProvider();
CacheManager cacheManager = provider.getCacheManager();
같은 캐시를 Ehcache 자신의 API(Application Programming Interface, 응용 프로그램 인터페이스) 대신
JCache API 로 여는 코드입니다. 첫 줄은 애플리케이션 클래스패스에서 기본 CachingProvider 구현을
가져옵니다. 클래스패스에 제공자가 여럿이면 정규화된 이름 org.ehcache.jsr107.EhcacheCachingProvider
로 Ehcache 제공자를 지목합니다.
사용처
Ehcache 를 다른 물건에 꽂는 통로는 JSR-107 입니다. 문서는 JSR-107(Java Specification Request 107)을
JCache 라고도 부릅니다. 소프트웨어 구현이 아니라 javax.cache API 를 정의하는 명세라고 밝힙니다.
애플리케이션에서 JCache API 호출을 쓰려면 jar 두 개가 필요합니다. JCache API 를 정의하는 JCache jar,
그리고 그 API 를 구현하는 캐싱 제공자 jar 인 Ehcache jar 입니다. Ehcache jar 가 JCache API 호출을
대응하는 Ehcache API 로 옮깁니다. 그래서 Ehcache API 호출을 한 줄도 쓰지 않고 JCache API 만으로
애플리케이션 전체를 만들 수 있습니다. 아래 두 자리가 그 통로를 그대로 씁니다.
Hibernate ORM(Object-Relational Mapping, 객체-관계 매핑)은 2차 캐시 자리에 Ehcache 를
꽂습니다. 2차 캐시는 Hibernate 가 얹는 추가 캐시 계층을 가리키는 말입니다. Hibernate 사용자
안내서의 JCache 절은 hibernate.javax.cache.provider 를 org.ehcache.jsr107.EhcacheCachingProvider
로 두는 설정 예를 싣습니다. 같은 예에서 hibernate.javax.cache.uri 로 ehcache.xml 의 경로를
지정합니다. 안내서는 최근 판 Ehcache 가 기본 캐시에 디스크 퍼시스턴스를 켠다고 덧붙입니다.
그것이 성능 저하를 일으킵니다. 그래서 캐시를 명시적으로 정의하기를 강하게 권합니다.
Ehcache 쪽에서는 캐시 템플릿으로 그 기본 설정을 잡을 수 있다고 안내서가 짚어 줍니다.
Spring Framework 는 캐시 추상화 뒤에서 Ehcache 를 씁니다. 저장소 설정 문서는 Ehcache 3.x 가
JSR-107 을 완전히 준수하므로 전용 지원 코드가 필요 없다고 적습니다. 그러고는 JSR-107 캐시 절로
넘깁니다. Spring 의 캐시 추상화는 JSR-107 을 준수하는 캐시를 쓸 수 있습니다. 그 JCache 구현은
org.springframework.cache.jcache 패키지에 있습니다.
Ehcache 를 고른 이유가 아니라 고를 수 있는 이유가 여기 적혀 있는 셈입니다. 명세를 구현했다는 사실
하나로 프레임워크 쪽 코드가 필요 없어집니다.
명세 하나를 Ehcache 가 구현하고, 그 구현체 위에 두 소비자가 각자 다른 방식으로 올라탑니다. Hibernate 는 제공자 이름을 설정에 명시하고, Spring 은 JSR-107 을 준수한다는 사실 하나만으로 전용 코드 없이 그 위에 얹힙니다.
classDiagram
class JSR107["JSR-107(JCache) 명세"]
class Ehcache["Ehcache"]
class Hibernate["Hibernate 2차 캐시"]
class Spring["Spring 캐시 추상화"]
Ehcache ..|> JSR107 : 구현
Hibernate --> Ehcache : EhcacheCachingProvider 로 지정
Spring --> Ehcache : JSR-107 준수 캐시로 사용
운영
계층 크기
계층은 피라미드 모양으로 크기를 잡아야 한다고 문서가 적습니다. 피라미드 위쪽 계층일수록 아래쪽 계층보다 적은 메모리를 쓰도록 설정하는 것입니다. Ehcache 는 heap 계층의 크기가 offheap 계층보다 작기를, offheap 계층의 크기가 disk 계층보다 작기를 요구합니다.
여기에 확인 사각지대가 하나 있습니다. heap 을 개수 기준으로 잡고 다른 계층을 바이트 기준으로 잡으면, 어느 쪽이 큰지를 Ehcache 가 설정 시점에 확인할 수 없습니다. 그래서 테스트하는 동안 그 관계가 지켜지는지 직접 확인해야 한다고 문서가 적습니다.
ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, EntryUnit.ENTRIES);
ResourcePoolsBuilder.heap(10);
ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB);
ResourcePoolsBuilder.newResourcePoolsBuilder().offheap(10, MemoryUnit.MB);
heap 계층은 엔트리 수로도 크기로도 잡을 수 있습니다. 문서는 위 세 표기를 나란히 싣습니다.
EntryUnit.ENTRIES 가 엔트리 개수 쪽이고 MemoryUnit.MB 가 바이트 쪽입니다. offheap 은
MemoryUnit 으로 잡습니다.
만료
만료는 ExpiryPolicy 인터페이스가 다룹니다. 캐시 매핑의 나이를 그 인터페이스로 제어합니다. 자바와 XML 이 직접 지원하는 만료는 세 가지입니다.
| 만료 | 뜻 |
|---|---|
| no expiry | 캐시 매핑이 절대 만료되지 않습니다 |
| time-to-live | 생성 시점부터 정해진 기간이 지나면 만료됩니다 |
| time-to-idle | 마지막으로 접근된 시점부터 정해진 기간이 지나면 만료됩니다 |
두 축이 다른 것을 셉니다. time-to-live 는 얼마나 오래되었나를 보고, time-to-idle 은 얼마나 오래 안 건드렸나를 봅니다.
스레드 풀
어떤 서비스는 비동기로 돌아서 자기 일을 하려면 스레드 풀이 필요합니다. 스레드 풀링 기능은 전부 ExecutionService 인터페이스 뒤로 모여 있습니다. 이 인터페이스를 쓰는 서비스는 셋이고, 서비스마다 캐시 단위와 캐시 매니저 단위로 따로 스레드 풀을 고를 수 있습니다.
| 서비스 | 캐시 단위 설정 | 캐시 매니저 단위 설정 |
|---|---|---|
| 디스크 저장소. 디스크 쓰기가 비동기로 일어납니다 | OffHeapDiskStoreConfiguration | OffHeapDiskStoreProviderConfiguration |
| Write Behind. CacheLoaderWriter 의 쓰기 작업이 비동기로 일어납니다 | DefaultWriteBehindConfiguration | WriteBehindProviderConfiguration |
| 이벤팅. 생성된 이벤트가 큐에 쌓였다가 스레드 풀을 통해 리스너에게 전달됩니다 | DefaultCacheEventDispatcherConfiguration | CacheEventDispatcherFactoryConfiguration |
어떤 스레드 풀을 쓸지 알려 주지 않으면 셋 다 같은 기본 스레드 풀을 나눠 씁니다. 손잡이는 서비스마다, 그리고 캐시 단위와 캐시 매니저 단위로 따로 있어서, 한 서비스만 골라 다른 스레드 풀로 갈아 끼울 수 있습니다.
관련 항목
이 물건이 속하는 상위 분류
이 물건을 이루는 부품
CacheManager · Serializer · Copier · ResilienceStrategy · ExpiryPolicy · ExecutionService · CacheLoaderWriter
이 물건을 JSR-107 로 잇는 표준·소비자
JSR-107 · JCache · Hibernate · Spring Framework
이 물건이 지원하는 기법
2단계 커밋 · JTA · 클러스터 · Terracotta Server Array · Write Behind
다른 이름: Ehcache 3 · org.ehcache