Spring boot에 caffeine cache 적용기

sonnng·2024년 2월 7일
0

Spring

목록 보기
38/41
post-thumbnail

캐싱을 생각하게 된 이유

내가 프로젝트에서 진행하고 있는 내용은 토론 게시판 조회 api와 메인페이지상에서 추천 프로그램 api 가 포함되어있다. 이중에서 수정은 적지만 조회가 빈번하게 일어나는 서비스에 대해 캐싱을 적용해 성능을 최적화할 수 있다고 알게 되서 내 코드에도 적용해보려고 한다. 매번 사용자가 조회할 때마다 데이터를 전달하는 것보다 고정된 데이터를 주기적으로 업데이트-삭제하고 캐시데이터로 활용하면 좋겠다고 생각했다. 기존에는API에서 db에 반복적으로 select 쿼리가 발생하는 것을 볼 수 있었고 불필요한 일을 반복하는 문제가 있었다.

여기서 Spring Cache란 단순한 추상화(어노테이션 사용)로 캐시를 쉽게 사용할 수 있도록 지원하는 라이브러리를 활용할 거다. 선언적인 방식으로(어노테이션::무엇을 할지만 적는방식) 메서드 실행결과를 캐시 key에 대응하는 value로 저장하고 같은 메서드 호출시 캐시에 저장된 결과를 반환한다.

스프링에 적용할 수 있는 캐시들

1. EhCache 2.X

캐시 라이브러리 중에서 가장 많은 기능을 지원하며 서버간 분산캐시, 동기/비동기, 디스크 저장지원 등이 가능하다. Spring boot에서는 ehcache.xml이라는 classpath에 넣어두면 알아서 인식할 정도로 편리하기 때문에 많이 사용한다고 한다.

2. Redis Cache

스프링, 장고, 노드js 등 다양한 framework에서 사용되는 방법이고, in-memory에 자료구조와 DB처럼 사용하며 캐시용도, message broker 용도로 사용되기도 한다는데, 다양한 방면에서 두루두루 쓰이는 방법같다.

3. Spring Cache

단순한 추상화(어노테이션 사용)로 캐시를 쉽게 사용할 수 있도록 지원하는 라이브러리를 활용할 거다. 선언적인 방식으로(어노테이션::무엇을 할지만 적는방식) 메서드 실행결과를 캐시 key에 대응하는 value로 저장하고 같은 메서드 호출시 캐시에 저장된 결과를 반환한다.

4. Caffeine Cache

Java8 이후로 spring boot 문서상 spring-boot-starter-cache에 auto-configured 되어있을 정도로 신뢰성이 높은 캐시 라이브러리라고 한다. 또한 size-based eviction 설정으로 사이즈를 최초에 설정해두면 그 크기만큼만 저장된다고 한다. 이후 최대치를 초과되면 evict(삭제)해버린다고 한다. 첫번째 요청인 경우 비동기적으로 새로고침할 수 있다는 장점도 있다.

또, time-based eviction으로 만료시간을 설정해서 정해진 주기마다 삭제-다시 캐싱해오는 원리다.

  • read based : 캐시를 마지막으로 읽은 시간에서부터 특정시간
  • write based : 캐시에 writed한 시점에서부터 특정시간

캐시 호출에 대해 statistic으로 둘 수 있다고 한다. 캐시 도달률이나 요청률 들을 실시간으로 모니터링할 수 있다고 하는데, 우리 프로젝트에서 log 모니터링 할 수 있는 기회가 있다면 추가로 첨부하겠다.

캐싱 종류 선택

기존 Spring Cache만으로는 별도로 config를 설정해놓지 않는다면 ConcurrentMap으로만 in-memory 캐시를 관리하게 된다. 그렇다면 만료시간 구현 등이 어렵다고 한다.

Redis 캐싱은 다양한 용도로 사용할 수 있다는 장점이 있으나, over-spec으로 어플리케이션 자체가 무거워질 수 있다는 생각이 들었고 캐시를 읽고 쓰는 속도가 가장 좋은 성능을 가진 라이브러리이기도 하다. 현재 우리 프로젝트에서 Mem : available 용량을 살펴볼때 충분히 사용가능한 캐싱이라고 생각했다. 따라서 로컬캐싱을 하는 Caffeine cache를 사용하기로 결정!

설정 구현(java 11 기준)

의존성 설정

dependencies {
		// ...

    // Cache 라이브러리
    implementation "com.github.ben-manes.caffeine:caffeine:3.1.8"
		
		// ...
}

Cache config 빈 등록

import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tavebalak.OTTify.common.constant.CacheType;

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        List<CaffeineCache> caches = Arrays.stream(CacheType.values())
            .map(
                cache -> new CaffeineCache(cache.getCacheName(),
                    Caffeine.newBuilder()
                        .expireAfterWrite(cache.getExpiredAfterWrite(), TimeUnit.SECONDS)
                        .maximumSize(cache.getMaximumSize())
                        .build()
                )
            )
            .collect(Collectors.toList());
        cacheManager.setCaches(caches);
        return cacheManager;
    }


}

캐시가 세팅된 CacheManager를 bean으로 등록해 CacheType 인스턴스가 갖는 CacheName으로 추상화된 in-memory 캐싱을 사용할 수 있다.

@EnableCaching은 스프링 부트 캐시를 사용하기 위해 '캐시 활성화'를 하는 목적으로 사용된다. @Configuration 부분에 선언해 사용한다.

캐시에 저장할 내용을 ENUM으로 정의

ENUM대신 직접 설정파일에 입력을 해도 되지만 많은 캐시 저장소를 정의할 경우 ENUM을 많이 활용하고 있다. 만료시간이 0인 경우는 만료되지 않는 특징이 있다.
최대 사이즈를 초과하게 되면, 첫번째 캐싱 저장소를 삭제하고 새로 쓰여진다고 한다.

package tavebalak.OTTify.common.constant;

import lombok.Getter;

@Getter
public enum CacheType {

    PROGRAM_TRENDING("programTrending", 60 * 60, 100),
    DISCUSSION_SUBJECT("discussionSubject", 60 * 60 * 24 * 7, 100);

    CacheType(String cacheName, int expiredAfterWrite, int maximumSize) {
        this.cacheName = cacheName;
        this.expiredAfterWrite = expiredAfterWrite;
        this.maximumSize = maximumSize;
    }

    private String cacheName;
    private int expiredAfterWrite;
    private int maximumSize;
}

적용하려는 서비스 로직에 적용

    @Override
    @Cacheable(cacheNames = "discussionSubject", key = "#subjectId")
    public CommunityAriclesDTO getArticleOfASubject(Long subjectId) {
    
    @Override
    @CacheEvict(cacheNames = "discussionSubject", key = "#subjectId")
    public void deleteSubject(Long subjectId) {


프론트엔드와 기능 연동 후 깨달은 점..단건조회에서의 캐싱은 적합하지 않다!

프론트엔드 연동을 하면서 CRUD가 가장 많이 일어나는 단건조회에서는 적용하면 안된다는 점을 깨달았다. 조회이기 때문에 조회 성능 최적화를 하려고 캐싱을 적용했었으나, crd 이후에는 CachePut을 계속해서 진행해야하고 성능최적화한만큼 성능이 더 안좋아지는 것이 당연했다. 따라서 캐시를 적용하려면 페이징 처리에서 사용하거나 정말 R이 90% 이상인 곳에서 적용해야 한다는 점을 깨달았다.

0개의 댓글