Cache를 적용하자 w Spring

Murphy·2023년 3월 23일
0

Spring Cache

목록 보기
1/1

들어가며

Application 개발에 있어 Cache는 application의 속도를 향상시키는데 아주 중요한 역활을 한다. 간단하게 내가 적용했던 Cache들에 대해서 정리를 하려고 한다. 특별한 내용은 없다.

역시 Spring에서는 간단한 가이드를 제공한다.
Caching Data with Spring
Spring Boot Caching

Why

내 경우 Cache를 적용하는 것을 고려하게 된 이유는, 담당하는 서비스가 다른 서비스에서 configuration을 읽어서 판단하는 성격을 가지고 있었고, 그 configuration을 읽는 시간이 너무 많이 소요되었기 때문이다.
Cache를 어디에 연결해서 어떻게 사용할지는 많은 의견이 있을 수 있다. 우선 참여하던 프로젝트에서 Cache에 대한 정책은 없었다. 이런 상황에서 실제 API를 호출했을때 응답속도가 너무 느렸고 일부 클라이언트는 실제 응답속도에 대한 불만을 표시하고 있었다.

어떤 정보를 Cache 해야 하는가.

위에서 언급한 것 처럼 담당하는 서비스의 특성상 여러 서비스에서 configuration을 읽고 그 값에 따라서 판단을 한 다음 정보를 처리하는 API가 많았다. 문제는 configuration이 하나만 있는 것이 아니고 그 양도 많아서 의존하고 있는 서비스의 퍼포먼스에 따라 담당하는 서비스의 응답속도가 결정되곤 하였다. 특히 서비스 초반 모든 서비스가 아직 안정적이지 않아 그 영향이 매우 컸다.

그럼 어떠한 정보를 cache 할 것인가?
대용량 트래픽을 처리하는 여러 서비스에서 다양한 방법으로 캐쉬 전략을 사용한다. 하지만 내 서비스는 그렇게 대용량 트래픽을 처리하는 서비스는 아니다. 최소한의 변경으로 최대의 효과를 얻고 싶었고 실제 변경이 자주 일어나지 않는 값을 선정해 Cache를 구성하기로 했다.

현재상황

위와 같은 문제를 풀기 위해 전임자는 이런 방법을 사용 한 것 같다.

  • 문제상황
    • 조건을 만족하는 값을 서비스A에서 받아 클라이언트에게 반환해야 한다.
    • 자주 변경되는 데이터가 아니다.
  • 해결방법
    • Google Guava 라이브러리에서 제공되는 LoadingCache 를 적용 했다.
    • Cache가 필요한 리소스에 대해서 CacheBuilder 를 만들어 LoadingCache 를 적용한다.
    • 조건을 만족하는 정보를 저장하는 것이 아닌, 데이터 Set 모두를 저장하는 방법을 적용하고 별도의 필터를 통해 원하는 정보를 얻는다.

Google Guava CachesExplained

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

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

caffeine을 프로바이더로 선정해 사용했다. 특별한 이유는 없고 기본에 사용하던 guava 기반의 프로바이더 였고 앞서 이야기 한대로 지금 단계에서는 in-memory cache를 유지하고자 했다.

적용하기

서론이 길었다.

시작

@EnableCaching은 적당한 위치에 추가한다. (XXXApplication.java 혹은 Configuration class) 그리고 다음과 같이 application.yml에 caffeine 사용을 지정한다.

spring:
	cache:
    	type: caffeine

CacheManager

CacheManager는 다음과 같이 설정을 통해 auto configuration을 통해 설정 할 수 있다.

spring.cache.cache-names=cache1,cache2
spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s

내 경우에는 직접 구현체를 통해서 설정했다.

CacheManager in Java

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;
    }
}

@Cacheable

아래와 같이 원하는 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을 뿌리면서 죽는 경우를 목격하기도 했다.

profile
Anything that can go wrong will go wrong.

0개의 댓글