[ bottle-note ] LocalCache 활용해보자

DeadWhale·2024년 4월 30일
0

bottle-note

목록 보기
4/4

보틀 노트 프로젝트에서는 지역정보가 검색조건으로 제공되는 경우가 있습니다.

이 국가(리전) 정보는

  • 검색 조건으로 사용되고
  • 자주 호출 될 수 있고 (접속한 사용자 수 만큼 호출)
  • 데이터의 변동이 극히 적다

이 3가지 조건에서 캐시를 활용하는걸 고려하게 되었습니다.

캐시는 다양한 방식이 있습니다.
가장 익숙한 방식이 Redis를 활용한 글로벌 캐싱인대.

전 이 선택지가 좀 마음에 들지 않았습니다.
일단 추가적으로 레디스 서버를 띄워야 하는데 이게 굉장히 오버 스펙인것 같습니다.
분산환경에서 데이터의 동기화가 그렇게까지 중요하지 않고 캐시 유효기간의 조절만으로도 충분히 제어가능 하다 생각이 드는데.
굳이 관리 포인트를 더 늘리고 싶지 않았습니다


Global Cache or Local Cache

캐시의 종류에 한번 정리하고 가는 시간이 필요할것 같습니다.
이름만 봐도 매우 직관적이지만
조금만 글로 비교하자면

-LocalGlobal
저장방식서버 자체적으로 저장별도의 캐시 서버에 캐시 저장
데이터 공유다른 서버에 있는 캐시를 확인하기 어렵다.캐시 서버에서 공유됩으로 매우 간단.
속도서버내에서 Heap영역에 접근함으로 상대적 빠름네트워크 IO가 필요하기 떄문에 상대적으로 느리다.
장비서버 자체적은 메모리와 디스크를 활용한다.캐시 서버의 할당된 자원을 사용
변경 전파A서버에서 변경 시 데이터 전파가 매우 복잡하다.분산 캐시 서버를 사용할 경우에만 전파하면되는데,
스케일 업/아웃이 편하다.

각 영역에 트레이드오프가 확실해 오히려 더 선택하기 쉽다.


Local Cache를 선택해보자.

Spring에서 지원해주는 로컬 캐시에 대한 문서도 매우 잘 되어있습니다.

여러 구현체들이 있는데 유형한 Redis도 있습니다.
저는 여기서 Caffeine를 선택했습니다.

Caffeine은 구글에서 만든 로컬 캐시 라이브러리입니다.
스프링의 공식 라이브러리는 아니지만 공식 문서 내에서도 사용되고 있습니다.

Caffeine은 다음과 같은 특징을 가지고 있는데.

  • 항목을 캐시에 자동 로드(선택적으로 비동기식)하고 캐시에 저장
  • 빈도 및 최신성을 기준으로 최대값을 초과하는 경우 크기 기반 퇴거
  • 마지막 액세스 또는 마지막 쓰기 이후로 측정된 시간 기반 항목 만료
  • 항목에 대한 첫 번째 오래된 요청이 발생할 때 비동기적으로 새로 고침
  • 약한 참조에 자동으로 래핑된 키
  • 캐시 접근 통계 축적

물론 라이브러리 없이도 ConcurrentMap 기반의 캐시를 만들 수 있지만
약간의 제약과 구현의 불편함등이 있어 편한 라이브러리를 사용하는게 좋다고 생각합니다.

적용하기

  1. 의존성 추가
// Cache
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation "com.github.ben-manes.caffeine:caffeine:3.1.8"
  • spring-boot-starter-cache : 스프링 캐시를 사용하기 위한 의존성
  • caffeine : Caffeine 라이브러리 의존성
  1. spring application cache 설정

@EnableCaching // 캐시 사용을 위한 어노테이션
@SpringBootApplication
public class BottleNoteApplication {
    public static void main(String[] args) {
        SpringApplication.run(BottleNoteApplication.class, args);
    }
}
  1. Enum을 통해 관리할 캐시 정의

@Getter
@AllArgsConstructor
public enum LocalCacheType {
    LOCAL_REGION_CACHE("LC-Region", 60 * 60 * 24, 1),
    LOCAL_ALCOHOL_CATEGORY_CACHE("LC-AlcoholCategory", 10, 1);

    private final String cacheName; // 등록할 캐시 이름
    private final int secsToExpireAfterWrite; // 캐시 만료 시간 ( 60 * 60 * 24  = 1일 )
    private final int entryMaxSize; // 캐시 최대 크기
}
  • region과 alcoholCategory 캐시를 정의했습니다.
  • 각각 별도의 캐시 스토리지로서 사용됩니다.
  • cacheName : 캐시 이름
  • secsToExpireAfterWrite : 캐시 만료 시간 초단위
  • entryMaxSize : 캐시 최대 크기
  1. CacheConfig 구현

@Configuration
@EnableCaching
public class LocalCacheConfig {
    @Bean
    public CacheManager cacheManager() {
        List<CaffeineCache> caches = Arrays.stream(LocalCacheType.values())
                .map(localCacheType ->
                        new CaffeineCache(
                                localCacheType.getCacheName(),
                                Caffeine.newBuilder()
                                        .recordStats() // 통계 정보를 수집하도록 설정
                                        .expireAfterWrite(localCacheType.getSecsToExpireAfterWrite(), TimeUnit.SECONDS) // 캐시 만료 시간 설정
                                        .maximumSize(localCacheType.getEntryMaxSize()) // 캐시 최대 크기 설정
                                        .build()))
                .toList();
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(caches);

        return cacheManager;
    }
}
  • CacheManager 빈을 등록합니다.
  • 위에서 정의한 캐시 Enum을 통해 Cache의 구현체 CaffeineCache 모델을 생성합니다.
  1. 캐시 사용

@Cacheable(value = "LC-Region")
public List<RegionsResponse> findAll() {
    log.info("RegionService.findAll() called , {}", now());
    return regionQueryRepository.findAllRegionsResponse();
}
  • 'LC-Region' 이라는 캐시를 사용하도록 설정합니다. (위의 enum에서 정의된 캐시 타입)
  • findAll() 메서드가 호출될 때 캐시가 존재하면 캐시된 데이터를 반환하고, 캐시가 존재하지 않으면 메서드를 실행하고 결과를 캐시에 저장합니다.

이런식으로도 사용 가능하다.


@Cacheable(value = "LC-Region", key = "#continent")
public List<RegionsResponse> findAll(String continent) {
    log.info("RegionService.findAll() called , {}", now());
    return regionQueryRepository.findAllRegionsResponse();
}
  • continent를 키로 사용하여 캐시를 구분합니다.
  • continent가 같은 경우 같은 캐시를 사용하게 됩니다.
  • 위의 코드의 경우 대륙별 region을 캐싱하게 됩니다.


  • 로컬 환경의 오차를 감안해주시길 바라겠습니다.
  • 캐싱되지 않은 요청은 400ms의 응답속도를 보이고있습니다.


  • 캐싱 후 17ms의 매우 빨라진 응답속도를 보이고 있습니다.
  • 서버에 로그도 찍히지 않았습니다 (서비스 코드 로직을 타지 않았다는 의미)

마치며

로컬 캐시를 사용하면 캐시 서버를 띄우지 않아도 되고, 캐시 서버를 사용하는 것보다 간단하게 캐시를 사용할 수 있습니다.
물론 캐시 서버를 사용하는 것이 더 많은 기능을 제공하지만, 간단한 캐싱이 필요한 경우 로컬 캐시를 사용하는 것도 좋은 방법이라고 생각합니다.

이번 기회에는 글로벌 캐시가 필요하지 않기 때문에 로컬 캐시를 사용하게 되었습니다.
이렇게 되어 400ms -> 17ms로 응답속도가 개선되었습니다.
이런 선택지가 주어진 상황에서 적절한 선택을 하는 것이 중요하다고 생각합니다.


0개의 댓글