Application 개발에 있어 Cache는 application의 속도를 향상시키는데 아주 중요한 역활을 한다. 간단하게 내가 적용했던 Cache들에 대해서 정리를 하려고 한다. 특별한 내용은 없다.
역시 Spring에서는 간단한 가이드를 제공한다.
Caching Data with Spring
Spring Boot Caching
내 경우 Cache를 적용하는 것을 고려하게 된 이유는, 담당하는 서비스가 다른 서비스에서 configuration을 읽어서 판단하는 성격을 가지고 있었고, 그 configuration을 읽는 시간이 너무 많이 소요되었기 때문이다.
Cache를 어디에 연결해서 어떻게 사용할지는 많은 의견이 있을 수 있다. 우선 참여하던 프로젝트에서 Cache에 대한 정책은 없었다. 이런 상황에서 실제 API를 호출했을때 응답속도가 너무 느렸고 일부 클라이언트는 실제 응답속도에 대한 불만을 표시하고 있었다.
위에서 언급한 것 처럼 담당하는 서비스의 특성상 여러 서비스에서 configuration을 읽고 그 값에 따라서 판단을 한 다음 정보를 처리하는 API가 많았다. 문제는 configuration이 하나만 있는 것이 아니고 그 양도 많아서 의존하고 있는 서비스의 퍼포먼스에 따라 담당하는 서비스의 응답속도가 결정되곤 하였다. 특히 서비스 초반 모든 서비스가 아직 안정적이지 않아 그 영향이 매우 컸다.
그럼 어떠한 정보를 cache 할 것인가?
대용량 트래픽을 처리하는 여러 서비스에서 다양한 방법으로 캐쉬 전략을 사용한다. 하지만 내 서비스는 그렇게 대용량 트래픽을 처리하는 서비스는 아니다. 최소한의 변경으로 최대의 효과를 얻고 싶었고 실제 변경이 자주 일어나지 않는 값을 선정해 Cache를 구성하기로 했다.
위와 같은 문제를 풀기 위해 전임자는 이런 방법을 사용 한 것 같다.
서비스A
에서 받아 클라이언트에게 반환해야 한다. LoadingCache
를 적용 했다. CacheBuilder
를 만들어 LoadingCache
를 적용한다.LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .removalListener(MY_LISTENER) .build( new CacheLoader<Key, Graph>() { @Override public Graph load(Key key) throws AnyException { return createExpensiveGraph(key); } });
서비스를 이관 받고 위 방법은 효과적인가에 대한 의문이 있었다.
성능의 향상은 있었다. 다만 Guava
를 직접 적용해 해당 데이터만 cache하는 제한적인 방법으로는 다른 데이터에 대한 cache 적용/확장에 어려움이 있다.
Cache를 적용하면 효과가 있어 보이는 다른 데이터도 발굴 되었고 cache 적용을 확장하기로 했다.
Spring Boot Cache
를 사용하면 @Cacheable
어노테이션을 사용해 쉽게 cache를 설정하고 이용 할 수 있다. 이 단계에서 나중에 Redis
를 통해 여러 POD 혹은 서비스에서 같이 사용 할 수 있는 방법도 고려 했으나, 우선 기존과 비슷한 in-memory로 접근했다.
자세한 설정 방법은 다른 문서를 참고하고 가볍게 지나간다.
우선 Spring Boot Cache를 사용하기 위해서 다음의 의존성을 추가해 준다.
implementation 'org.springframework.boot:spring-boot-starter-cache'
@EnableCaching
을 추가해 서비스에서 cache를 사용할 것임을 선언한다. 이제 @Cacheable
, @CachePut
그리고 @CacheEvict
과 같은 어노테이션을 이용해 cache를 제어 할 수 있다.
Cache의 프로바이더를 지정하지 않은 경우, 기본값으로 ConcurrentHashMap
을 이용해 기능을 제공하며, 지원하는 프로바이더의 목록은 여기서 확인 할 수 있다.
caffeine
을 프로바이더로 선정해 사용했다. 특별한 이유는 없고 기본에 사용하던 guava
기반의 프로바이더 였고 앞서 이야기 한대로 지금 단계에서는 in-memory cache를 유지하고자 했다.
서론이 길었다.
@EnableCaching
은 적당한 위치에 추가한다. (XXXApplication.java 혹은 Configuration class) 그리고 다음과 같이 application.yml에 caffeine 사용을 지정한다.
spring:
cache:
type: caffeine
CacheManager
는 다음과 같이 설정을 통해 auto configuration을 통해 설정 할 수 있다.
spring.cache.cache-names=cache1,cache2
spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s
내 경우에는 직접 구현체를 통해서 설정했다.
Cache할 항목의 이름과 스펙을 정의한 enum
을 먼저 생성했다. 어떤 항목이 어떤 스펙으로 관리되고 있는지 한번에 파악하기 위함이다.
@Getter
@AllArgsConstructor
public enum CacheType {
AAA("aaa", 60*60, 10),
BBB("bbb", 60, 1000);
private String cacheName;
private int expiredAfterWrite;
private int maximumSize;
}
위 CacheType
을 이용해 다음과 같이 CacheManager
를 생성 할 수 있다.
@Configuration
public class CaffeineCacheConfig {
@Bean
public CacheManager cacheManager() {
List<CaffeineCache> caches = Arrays.stream(CacheType.values())
.map(cache ->
new CaffeineCache(cache.getCacheName(),
Caffeine.newBuilder()
.recordStats()
.expireAfterWrite(cache.getExpiredAfterWrite(), TimeUnit.SECONDS)
.maximumSize(cache.getMaximumSize())
.build()
)
)
.toList();
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(caches);
return cacheManager;
}
}
아래와 같이 원하는 method에 어노테이션을 통해서 적용 할 수 있다. 이 때 적용할 cacheName을 지정한다.
@Cacheable(cacheNames = "aaa")
@Override
public String getAaa(String id) {
}
주의사항
@Cacheable
어노테이션을 통해 해당 method를 지정해도 객체안에서 참조하는 경우@Cacheable
과 별개로 실제 구현체가 실행된다. 이는 프록시를 통해서 method가 invoke 되는 경우에만@Cacheable
이 적용되기 때문이다.The @EnableCaching annotation triggers a post-processor that inspects every Spring bean for the presence of caching annotations on public methods. If such an annotation is found, a proxy is automatically created to intercept the method call and handle the caching behavior accordingly.
주의사항
in-memory cache의 경우 cache되는 항목의 개수에 주의해야 한다. 메모리는 한계가 있는 자원이고 무분별한 cache로 인해 시스템이 OOM을 뿌리면서 죽는 경우를 목격하기도 했다.