
독서 집중 및 기록 서비스 NOOK에서는
홈 화면에 월별 독서 통계를 제공한다.
서재 월별 통계 API는 다음과 같은 특징을 가진다.
group by, sum 집계 연산 필요즉, 읽기 연산은 반복되고, 쓰기 연산은 비교적 적은 구조이다.
이 특성 때문에 단순 DB 조회만으로 운영하기에는
장기적으로 비효율적일 수 있다고 판단했다.
그래서 캐싱을 도입하기로 결정했고,
그 전에 캐싱 전략을 정리해보기로 했다.
캐싱 전략은 대표적으로 다음과 같다.
✔ 읽기 트래픽이 많고
✔ 데이터 변경이 자주 일어나지 않을 때 적합
→ 통계 API에 가장 일반적인 방식
✔ 데이터 정합성이 매우 중요할 때
✔ 캐시 미스를 최소화하고 싶을 때 사용
✔ 실시간 랭킹, 조회수, 좋아요 카운트 등에 적합
✔ 트래픽이 매우 많을 때 사용
NOOK의 월 통계는:
따라서 Cache-Aside 전략 + TTL + 이벤트 기반 무효화가 적합하다고 판단했다.
캐싱을 적용해도 되는 요구사항인지 먼저 확인했다.
다음 조건을 만족하면 캐싱을 적용하는 것이 좋다.
NOOK의 월 통계는 위 조건을 만족한다.
특히 포커스 기록은 로그 형태로 쌓이는 구조이기 때문에
수정/삭제가 거의 없다는 점이 캐싱에 매우 유리하다.
| 구분 | 로컬 캐시 | Redis |
|---|---|---|
| 저장 위치 | JVM 메모리 | 외부 인메모리 서버 |
| 속도 | 매우 빠름 | 빠름 |
| 서버 여러 대 | 불일치 발생 가능 | 공유 가능 |
| 블루그린 배포 | 캐시 초기화됨 | 유지 가능 |
NOOK는 블루그린 배포 전략으로 갈 예정이었기 때문에
글로벌 캐시인 Redis를 선택했다.
이유:
@Cacheable(
value = "libraryMonthlyCurrent",
key = "#userId + ':' + #yearMonth"
)
캐시별 TTL을 다르게 설정하기 위해
RedisCacheManager를 커스터마이징했다.
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaultConfig =
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5));
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
cacheConfigs.put("libraryMonthlyCurrent",
defaultConfig.entryTtl(Duration.ofMinutes(3)));
cacheConfigs.put("libraryMonthlyPast",
defaultConfig.entryTtl(Duration.ofHours(24)));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigs)
.build();
}
}
트래픽이 증가할 경우를 대비해 집계 테이블을 도입하는 것도 고려했다.
하지만 현재 트래픽 규모에서는
캐시 기반 집계로 충분하다고 판단하여 보류했다.
향후 트래픽 증가 시,
로컬 캐시+레디스 캐시를 혼합해서 사용하는 방식도 고려할 예정이다.
통계 관련 API를 개발하면서, 당연히 캐시를 도입해야겠지 라고만 생각하고 있었다.
그렇지만 생각보다 고려할 게 많았다.
까지 고려해야 한다는 점을 알게 되었다.