보틀 노트 프로젝트에서는 지역정보가 검색조건으로 제공되는 경우가 있습니다.
이 국가(리전) 정보는
이 3가지 조건에서 캐시를 활용하는걸 고려하게 되었습니다.
캐시는 다양한 방식이 있습니다.
가장 익숙한 방식이 Redis를 활용한 글로벌 캐싱인대.
전 이 선택지가 좀 마음에 들지 않았습니다.
일단 추가적으로 레디스 서버를 띄워야 하는데 이게 굉장히 오버 스펙인것 같습니다.
분산환경에서 데이터의 동기화가 그렇게까지 중요하지 않고 캐시 유효기간의 조절만으로도 충분히 제어가능 하다 생각이 드는데.
굳이 관리 포인트를 더 늘리고 싶지 않았습니다
캐시의 종류에 한번 정리하고 가는 시간이 필요할것 같습니다.
이름만 봐도 매우 직관적이지만
조금만 글로 비교하자면
- | Local | Global |
---|---|---|
저장방식 | 서버 자체적으로 저장 | 별도의 캐시 서버에 캐시 저장 |
데이터 공유 | 다른 서버에 있는 캐시를 확인하기 어렵다. | 캐시 서버에서 공유됩으로 매우 간단. |
속도 | 서버내에서 Heap영역에 접근함으로 상대적 빠름 | 네트워크 IO가 필요하기 떄문에 상대적으로 느리다. |
장비 | 서버 자체적은 메모리와 디스크를 활용한다. | 캐시 서버의 할당된 자원을 사용 |
변경 전파 | A서버에서 변경 시 데이터 전파가 매우 복잡하다. | 분산 캐시 서버를 사용할 경우에만 전파하면되는데, 스케일 업/아웃이 편하다. |
각 영역에 트레이드오프가 확실해 오히려 더 선택하기 쉽다.
Spring에서 지원해주는 로컬 캐시에 대한 문서도 매우 잘 되어있습니다.
여러 구현체들이 있는데 유형한 Redis도 있습니다.
저는 여기서 Caffeine를 선택했습니다.
Caffeine은 구글에서 만든 로컬 캐시 라이브러리입니다.
스프링의 공식 라이브러리는 아니지만 공식 문서 내에서도 사용되고 있습니다.
Caffeine은 다음과 같은 특징을 가지고 있는데.
물론 라이브러리 없이도 ConcurrentMap 기반의 캐시를 만들 수 있지만
약간의 제약과 구현의 불편함등이 있어 편한 라이브러리를 사용하는게 좋다고 생각합니다.
// Cache
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation "com.github.ben-manes.caffeine:caffeine:3.1.8"
@EnableCaching // 캐시 사용을 위한 어노테이션
@SpringBootApplication
public class BottleNoteApplication {
public static void main(String[] args) {
SpringApplication.run(BottleNoteApplication.class, args);
}
}
@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; // 캐시 최대 크기
}
@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;
}
}
@Cacheable(value = "LC-Region")
public List<RegionsResponse> findAll() {
log.info("RegionService.findAll() called , {}", now());
return regionQueryRepository.findAllRegionsResponse();
}
이런식으로도 사용 가능하다.
@Cacheable(value = "LC-Region", key = "#continent")
public List<RegionsResponse> findAll(String continent) {
log.info("RegionService.findAll() called , {}", now());
return regionQueryRepository.findAllRegionsResponse();
}
로컬 캐시를 사용하면 캐시 서버를 띄우지 않아도 되고, 캐시 서버를 사용하는 것보다 간단하게 캐시를 사용할 수 있습니다.
물론 캐시 서버를 사용하는 것이 더 많은 기능을 제공하지만, 간단한 캐싱이 필요한 경우 로컬 캐시를 사용하는 것도 좋은 방법이라고 생각합니다.
이번 기회에는 글로벌 캐시가 필요하지 않기 때문에 로컬 캐시를 사용하게 되었습니다.
이렇게 되어 400ms -> 17ms로 응답속도가 개선되었습니다.
이런 선택지가 주어진 상황에서 적절한 선택을 하는 것이 중요하다고 생각합니다.