Bluetape4k Cache Part 4: Workshop 예제로 캐시 전략 감 잡기

이번 글은 실제로 실행해 볼 수 있는 예제 이야기입니다. bluetape4k-workshop에는 Spring Boot 기반 캐시 예제가 여러 개 있습니다.
목표는 “캐시가 좋다”를 반복하는 것이 아닙니다. 어떤 전략이 어떤 코드 모양을 만들고, 어떤 비용을 숨기고, 어떤 운영 계약을 요구하는지 보는 것입니다.
예제 위치
섹션 제목: “예제 위치”관련 예제는 아래에 있습니다.
| 예제 | 역할 |
|---|---|
| cache-caffeine | Spring Cache + Caffeine 로컬 캐시 |
| cache-redis | Spring Data Redis + Lettuce |
| cache-resilience | Redis 장애 시 Caffeine 대체 경로 |
Caffeine: 가장 가까운 캐시
섹션 제목: “Caffeine: 가장 가까운 캐시”cache-caffeine 예제는 Spring Cache abstraction에 Caffeine을 연결합니다.
@Configurationclass CaffeineConfig { @Bean fun cacheManager(caffeine: Caffeine<Any, Any>): CacheManager { return CaffeineCacheManager("cache:countries", "cache:cities").apply { setCaffeine(caffeine) } }
@Bean fun caffeineBean(): Caffeine<Any, Any> = caffeine { maximumSize(10_000) expireAfterWrite(30.minutes) }}전체 소스: CaffeineConfig.kt
이 예제는 단일 인스턴스 read-heavy 캐시에 좋습니다. 빠르고 단순합니다. 단점도 단순합니다. 인스턴스가 둘이면 캐시도 둘입니다. 둘은 서로 대화하지 않습니다. 의외로 많은 문제가 “서로 대화하지 않음”에서 시작합니다.
Redis: 공유되는 캐시
섹션 제목: “Redis: 공유되는 캐시”cache-redis 예제는 Redis를 Spring Cache와 RedisTemplate에 연결합니다.
@Beanfun redisCacheManager(connectionFactory: RedisConnectionFactory): CacheManager = RedisCacheManager.builder(connectionFactory) .cacheDefaults( RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofDays(1)) ) .build()전체 소스: LettuceRedisCacheConfiguration.kt
Redis는 여러 인스턴스가 같은 값을 보게 해줍니다. 대신 read path마다 remote hop이 있습니다. 그래서 Part 2에서 본 것처럼 hot read가 많으면 Near Cache가 후보가 됩니다.
정식 캐시 전략 예제: Exposed Workshop 11장
섹션 제목: “정식 캐시 전략 예제: Exposed Workshop 11장”Read-through, write-through, write-behind는 application service가 DB 접근 순서를 직접 조립하는 패턴이 아닙니다. application은 cache map을 사용하고, cache가 loader 또는 writer를 통해 DB 접근을 소유해야 합니다.
exposed-workshop 11장의 01-cache-strategies는 이 계약을 그대로 구현합니다. UserCacheRepository는 READ_WRITE_THROUGH_WITH_NEAR_CACHE, UserEventCacheRepository는 WRITE_BEHIND_WITH_NEAR_CACHE를 사용합니다. 실제 DB 읽기와 쓰기는 EntityMapLoader와 EntityMapWriter가 담당합니다.

참고자료
섹션 제목: “참고자료”- Exposed Workshop 11장 cache strategies 설명
- RedissonConfig.kt
- UserCacheRepository.kt와 UserCacheRepositoryTest.kt
- UserEventCacheRepository.kt와 UserEventCacheRepositoryTest.kt
- AbstractJdbcRedissonRepository.kt
- EntityMapLoader.kt와 EntityMapWriter.kt
resilience: 캐시 장애를 서비스 장애로 키우지 않기
섹션 제목: “resilience: 캐시 장애를 서비스 장애로 키우지 않기”cache-resilience는 Redis를 기본 캐시로 쓰고 Caffeine을 대체 경로로 둡니다.
suspend fun getProduct(id: String): String? { val redisKey = "$REDIS_KEY_PREFIX$id"
return SuspendDecorators.ofSupplier { redisTemplate.opsForValue().get(redisKey) } .withCircuitBreaker(circuitBreaker) .withFallback { localCache.getIfPresent(id) } .invoke()}전체 소스: ResilientProductService.kt
이 예제는 운영에서 바로 중요해집니다. 캐시는 성능을 위해 붙였는데, Redis가 잠깐 불안정해졌다고 서비스 전체가 같이 영향을 받으면 이상합니다. 캐시는 보조 인프라입니다. 보조 인프라가 서비스의 중심 장애가 되면 설계를 다시 봐야 합니다.
실전 체크리스트
섹션 제목: “실전 체크리스트”서비스에 캐시를 넣기 전에 최소한 아래 질문에는 답해야 합니다.
| 질문 | 답해야 하는 이유 |
|---|---|
| 기준 지연 시간은? | 캐시가 실제로 개선하는지 알아야 함 |
| hit ratio 목표는? | hit이 낮으면 비용만 늘 수 있음 |
| stale 허용 시간은? | Near Cache와 write-behind의 핵심 계약 |
| 쓰기 실패 정책은? | throw, retry, queue, best effort 중 선택 |
| invalidate 범위는? | 단일 key, bulk, prefix, 전체 clear 구분 |
| 장애 시 대체 경로는? | Redis 장애가 서비스 장애가 되지 않게 함 |
| 관측 지표는? | hit/miss, latency, queue depth, circuit state |
마무리
섹션 제목: “마무리”이 시리즈의 결론은 “무조건 Near Cache”가 아닙니다.
더 정확한 결론은 이렇습니다.
- 단일 JVM hot read면 Caffeine이 가장 단순합니다.
- 여러 인스턴스가 공유해야 하면 Redis 같은 remote cache가 필요합니다.
- remote read가 병목이면 Near Cache를 검토합니다.
- Exposed repository에 붙일 때는 read/write 전략을 분리합니다.
- 장애 동작을 정하지 않은 캐시는 아직 설계가 끝난 게 아닙니다.
캐시는 성능 최적화이기도 하지만, 운영 계약이기도 합니다. 운영 계약을 대충 쓰면 나중에 아주 또렷하게 돌아옵니다. 보통 새벽에요.
댓글
GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.