캐모마일 캐시 모듈 가이드
개요
어플리케이션이나 시스템에서 클라이언트에 대한 요청을 처리 할 때 자주 사용되는 데이터가 있을 수 있다. DB 데이터 조회 작업은 비용이 비교적 많이 들기 때문에 DB 조회를 자주 하면 시스템 성능에 영향을 미칠 수 있다.
주로 Update가 자주 일어나지 않으며 Read가 빈번하게 일어나는 데이터를 캐싱하면 어플리케이션의 속도를 개선 할 수 있다.
※ 단, 무분별하고 적절하지 못한 데이터를 캐싱하면 예상 외의 결과가 발생 할 수 있기 때문에 캐싱할 데이터를 신중하게 선별해서 개발해야 한다.
스프링은 CacheManager를 통해 추상화를 지원하기 때문에 실제 캐싱한 데이터의 저장이 일어나는 캐시 구현체는 별도의 설정을 통해 변경 가능하며, 캐시 구현체는 Ehcache, Redis, Hazelcast 등 다양한 캐시 매니저를 선정할 수 있다.
※ chamomile-cache 모듈에서는 개발을 위해 캐시 기능을 추상화한 spring-cache와 실제 캐시 구현체 중 하나인 Hazelcast를 내장하여 제공한다. (redis의 경우 chamomle-cache-redis로 제공)
Hazelcast는 Ehcache 개발자가 만든 오픈소스 인메모리 데이터 그리드(IMDG: In-Memory Data Grid)로서
Disk 대신 멀티 서버의 메모리를 클러스터링하여 하나의 큰 메모리 저장소를 구축하는 특징을 가지고 있다.
[사진 출처: https://hazelcast.com/glossary/in-memory-data-grid/]
- 데이터가 RAM에 저장되기 때문에 속도가 빠르다.
- Java의 Map, Set, List와 같은 Collection에 데이터 저장이 가능하다.
- 캐시의 Eviction, Expiration 등의 속성을 지원
- 단일/다중 서버에 장애가 발생할 경우를 대비해서 여러대의 머신에 사본을 저장하기 때문에 데이터 손실이 발생하지 않는다.
dependency
<dependency>
<groupId>net.lotte.chamomile.module</groupId>
<artifactId>chamomile-cache</artifactId>
</dependency>
설정 파일은 리소스에 hazelcast.xml 혹은 hazelcast.yml로 설정이 가능하다.
hazelcast.yml
hazelcast: # hazelcast-full-example.yaml에서 모든 설정에 대한 설명 찾을 수 있음
cluster-name: path # 클러스터 이름
network:
port: 5701 # 클러스터 간 통신에 쓰이는 포트 번호 (Inbound)
auto-increment: true # port에 설정된 값부터 1씩 증가하며 가능한 포트 찾음
port-count: 100 # 총 100개까지 찾음 (5701~5801).
outbound-ports: # 클러스터 간 통신에 쓰이는 포트 번호 (Outbound)
- 0 # 0은 시스템이 제공하는 포트를 그대로 사용
join: # 동일한 클러스터에 있는 멤버를 찾는 방식에 대한 설정
auto-detection:
enabled: false #자동 클러스터링 옵션 true일때 자동으로 연결되며 false 권장.
tcp-ip:
enabled: true #tcp-ip 기반 설정
member-list:
# 클러스터 핵심 멤버 IP, 새로운 멤버는 멤버 중 최소 하나와 연결되면 된다.
- 127.0.0.1
cache:
path: # 캐시 이름
eviction: # 캐시 삭제 정책에 대한 설정
size: 500000
max-size-policy: ENTRY_COUNT # 캐시 갯수를 기준으로 삼음
eviction-policy: LFU
expiry-policy-factory: # 언제 캐시가 만료될 것인지
timed-expiry-policy-factory:
expiry-policy-type: CREATED # 캐시가 생성된 후
time-unit: HOURS
duration-amount: 12 # 12시간이 지나면 만료
사용법
주요 캐시 Annotation
어노테이션 | 주요 기능 |
---|---|
@Cacheable | 캐시 저장 및 조회 |
@CacheEvict | 캐시 삭제 |
@CachePut | 메서드 실행을 방해하지 않고 캐시를 업데이트 |
@Caching | 여러개의 캐시 어노테이션을 함께 사용하고 싶을 때 사용 |
@Cacheable
@Cacheable 어노테이션은 캐시 할 메서드를 지정하는데 사용한다.
@Cacheable 어노테이션이 있는 메서드가 호출되면, 이미 실행된 메서드라면 여러 번 실행하지 않고 캐시 된 스토리지에서 결과를 반환한다.
아래의 코드는 가장 간단한 형식 @Cacheable을 통한 캐시 사용 방법이다. 메서드에 어노테이션과 함께 연결한 캐시의 이름을 지정한다.
@Cacheable(value = "samplecache")
Public Sample getById(int id) {...}
위의 코드에서 메서드 getById는 samplecache 캐시와 연결된다. 메서드가 호출 될 때마다 캐시를 검사하여 이미 실행되었고 반복 될 필요가 없는지 확인한다.
하나의 캐시만 선언하지 않고 어노테이션에서는 여러 개의 이름을 지정하여 두 개 이상의 캐시를 사용하면 메서드를 실행하기 전에 각 캐시를 검사한다. 적어도 하나의 캐시에 히트가 발생하면 연결된 캐시의 값이 반환된다.
@Cacheable(value = "samplecache1, samplecache2")
public Sample getById(int id) {...}
캐시는 기본적으로 key – value 저장소이기 때문에 캐시 된 메서드를 호출 할 때마다 캐시 액세스에 적합한 key로 변환 해야한다.
key의 명시적 선언은 SpEL 사용하여 가능하며, 명시적으로 key를 지정하지 않았다면 메서드의 인자를 key로 사용한다.
아래의 코드는 SpEL를 사용하여 명시적인 key 선언의 예이다.
@Cacheable(value = "samplecache", key = "#sample")
public Sample getById(Sample sample) {...}
@Cacheable(value = "samplecache", key = "#sample.id")
public Sample getById(Sample sample) {...}
@Cacheable(value = "samplecache", key = "#sample.name")
public Sample getById(Sample sample) {...}
개발 요건에 따라 메서드에 항상 캐싱되는 것이 적합하지 않을 수 있다. 이럴 경우 condition 속성을 통해 true나 false가 되는 SpEL 표현식을 받는 conditional 파라미터로 캐시 적용 유무를 선택 할 수 있다. SpEL 표현식에 의해 true이면 메서드를 캐시하고 false이면 메서드가 캐시 되지 않는다.
아래의 코드는 파라미터인 id가 200일 경우 캐시하지 않는다.
@Cacheable(value = "samplecache", key = "#id", condition="#id!=200")
public Sample getById(int id) {...}
아래의 코드는 파라미터인 id가 300 이상일 경우 캐시한다.
@Cacheable(value = "samplecache", key = "#id", condition="#id>=300")
public Sample getById(int id) {...}
@CachePut
메서드의 실행에 영향을 주지 않고 캐시를 업데이트해야하는 경우 @CachePut 어노테이션을 사용할 수 있다.
메서드는 항상 실행(조건이 맞으면)되며 그 결과는 캐시에 저장한다.
@Cacheable와 같은 옵션을 지원하며 메서드 흐름 최적화가 아닌 캐시 생성에 사용한다.
아래의 코드는 id를 키로 메서드의 결과를 캐시에 저장하며, 같은 key(id)로 호출되더라도 @Cacheable과는 다르게 메서드가 실행된다.
@CachePut(value = "samplecache", key = “3_4_5")
public Sample updateSample(Sample sample) {...}
주의 : @CachePut과 @Cacheable서로 다른 동작을 수행하기 때문에 같은 방법으로 어노테이션을 설정하는 것은 일반적으로 권장하지 않는다. @Cacheable은 캐시를 사용하여 메서드 실행을 건너 뛰게 되지만 @CachePut은 캐시 업데이트를 실행하기 위해 실행을 강제한다. 이로 인해 예상하지 못한 동작이 발생할 수 있다.
@CacheEvict
@CacheEvict 어노테이션은 캐시를 제거한다. 즉, 캐시에서 데이터를 제거하는 트리거로 동작하는 메서드다.
다른 캐시 어노테이션과 마찬가지로 @CacheEvict 또한 동작할 때 영향을 미칠 하나 이상의 캐시를 지정해야 한다.
또한 @CacheEvict에서도 key나 condition을 지정해야 할 수 있지만 key에 매핑된 하나의 엔트리 가 아니라 전체를 제거하기 위해서는 allEntries 속성을 추가로 사용할 수 있다.
아래의 코드는 samplecache 캐시의 모든 엔트리를 삭제의 예이다
@CacheEvict(value = "samplecache", allEntries = true)
public void clearCache() {...}
@CacheEvict를 다른 어노테이션과 다르게 void를 메서드에 적용 할 수 있다. 메서드가 트리거로 동작하므로 리턴값은 무시한다. 이는 캐시에 데이터를 넣거나 갱신해서 그 결과가 필요한 @Cacheable과 다른 점이다.
@Caching
동일한 메소드에서 @CacheEvict 또는 @CachePut과 같은 유형의 여러 어노테이션을 지정하려는 경우 유용하게 사용 할 수 있다.
같은 key를 사용하여 같은 아이템을 포함하는 두 개의 캐시가 있다고 가정 해 보자. 두 캐시에서 모두 아이템을 제거하려는 경우는 간단하다.
@CacheEvict(value = {"cache1", "cache2"}, key = "#sample.id")
public void refreshSample(Sample sample) {...}
만약 다른 key를 사용한다면 아래와 같이 @Caching 어노테이션을 통해서 여러유형의 어노테이션을 지정할 수 있다.
@Caching(evict = {
@CacheEvict(value = "cache1", key = "#sample.id"),
@CacheEvict(value = "cache2", key = "#sample.name")
})
public void refreshSample(Sample sample) {...}
CacheManager를 이용한 방법
CacheManager을 직접 사용하면 일반적인 컬렉션(HashMap 등)과 같이 개발이 가능하다.
CacheManager와 Cache 클래스에서 제공하는 주요 API는 아래와 같다.
API | 주요 기능 |
---|---|
CacheManager.getCache | 지정한 캐시를 리턴 |
CacheManager.getCacheNames | 등록된 모든 캐시의 이름을 리턴 |
Cache.put | 지정한 키로 Object를 캐시에 적재 |
Cache.get | 지정한 키에 매핑하는 캐시아이템을 리턴 |
Cache.evict | 지정한 키에 매핑된 캐시아이템을 삭제 |
Cache.clear | 적재된 캐시아이템을 모두 삭제 |
Cache.getName | 캐시명을 리턴 |
* 참고 : Ehcache와 Redis 등의 캐시구현체와 관계 없이 같은 인터페이스로 작동하기 위해서는 Spring이 제공하는 CacheManager와 Cache 클래스를 임포트 해야 한다.
import org.springframework.cache.CacheManager;
import org.springframework.cache.Cache;
CacheManager를 직접 사용하기 위해서는 빈으로 등록된 CacheManager 를 가져와야 한다.
[CacheManager.getCache(String name)]
cacheManager의 getCache 메서드를 통해 캐시를 가져온다.
CacheManager의 직접 사용을 위해 반드시 호출해야 하는 API이다.
Cache cache = cacheManager.getCache("캐시 이름");
[CacheManager.getCacheNames()]
저장된 모든 캐시 이름을 조회한다.
cacheManager.getCacheNames();
Cache.put(Object key, Object value)
앞서 호출한 getCache를 통해 리턴된 Cache 객체로 캐시아이템을 캐시에 저장 할 수 있다.
아래의 코드는 put을 통한 캐시아이템 저장 예시 이다.
// String “key1”를 키로 Sample 객체를 캐시에 저장
cache.put("key1", new Sample(100, "Developer_1", "java"));
// String “key2”를 키로 “Devleoper_2”를 캐시에 저장
cache.put("key2", "Developer_2");
// Integer 100을 키로 현재 시간을 캐시에 저장
cache.put(100, new Date().toString());
참고 : 캐시 업데이트는 별도의 API가 있지 않으며, 업데이트를 위한 새로운 캐시아이템을 같은 key로 put을 하게 되면 기존의 캐시는 삭제되고 새로 등록한 캐시아이템으로 업데이트 된다.
Cache.get(Object key)
캐시에 저장된 캐시아이템을 key를 통해 가져올 수 있다.
get을 통해 호출되는 리턴 타입은 ValueWrapper클래스이며,get을 한번 더 호출하여 실제 반환 값을 리턴 받는다.
아래의 코드는 get을 통한 캐시아이템 호출 예시이다.
cache.get("key1").get();
cache.get("key2").get();
cache.get(100).get();
만약 호출하는 key에 대해 저장된 캐시가 없다면 NullPointerException이 발생한다.
Cache.evict(Object key)
키에 매핑된 캐시아이템을 삭제한다.
해당 키에 저장된 캐시아이템이 없더라도 에러를 발생하지 않는다.
아래의 코드는 evict을 통한 특정 키에 대한 캐시아이템 삭제 예시이다.
cache.evict("key1");
cache.evict("key2");
cache.evict(100);
Cache.clear()
캐시에 있는 모든 캐시아이템을 삭제한다.
아래의 코드는 clear를 통한 캐시아이템 전체 삭제 예시이다.
cache.clear();
Cache.getName()
현재 연결된 캐시의 이름을 가져온다.
아래의 코드는 getName을 통한 캐시 이름 호출 예시이다.
cache.getName();
클러스터링
캐시 클러스터링은 이중화 된 서버간의 캐시 동기화를 의미한다.
캐시 클러스터링을 구성하면 연결된 모든 서버간의 캐시 및 엘리먼트가 동기화 되며 요건에 따라 보다 효율적으로 시스템을 구성 할 수 있다.
Hazelcast에서는 모든 클러스터 구성원의 RAM이 단일 메모리 내 데이터 저장소로 결합되어 데이터에 대한 빠른 액세스를 제공한다.
이 분산 모델을 캐시 클러스터라고 하며 데이터 확장 및 복제를 가능하게 한다.
클러스터 구성원 중 하나가 작동 중지되면 데이터는 나머지 구성원 간에 다시 분배된다.
또한 필요에 따라 클러스터 노드를 쉽게 추가하거나 제거할 수 있다.
[사진 출처] https://docs.hazelcast.com/hazelcast/5.3/cache/overview