Read-Through
캐시에 값이 없을 때 EntityMapLoader가 Exposed로 DB를 조회하고 Redis와 로컬 캐시에 적재한다.
- 반복 조회 비용 감소
- 첫 조회는 DB 지연 포함
Exposed + Local Cache + Redis
캐시는 단순히 조회 속도를 높이는 기능이 아니다. 어떤 값을 어디에서 읽고, 변경 결과를 언제 DB에 반영하며, 실패 시 어느 계층을 다시 맞출지 결정하는 데이터 처리 전략이다.
JdbcCacheRepository 구현은 캐시 조회와 DB 반영 시점을 Read-Through, Write-Through, Read-Only, Write-Behind로 구분한다. 데이터 변경 빈도와 허용 가능한 DB 반영 지연에 맞춰 전략을 선택한다.
캐시에 값이 없을 때 EntityMapLoader가 Exposed로 DB를 조회하고 Redis와 로컬 캐시에 적재한다.
EntityMapWriter가 캐시 변경과 DB 반영을 같은 요청 흐름에서 처리한다.
자주 읽고 거의 바뀌지 않는 값을 캐시한다. 변경 시 명시적으로 무효화한 뒤 다음 조회에서 다시 적재한다.
요청 수락 후 Redis에 먼저 기록하고 배치가 Exposed 트랜잭션으로 DB에 반영한다.
요청 처리, 저장소 전략, 저장 및 검증 계층을 연결해 각 캐시 전략이 어느 경로에서 데이터를 조회하고 DB에 반영하는지 보여준다.
UserCacheRepository, UserCredentialsCacheRepository, UserEventCacheRepository가 각각 Read/Write-Through, Read-Only, Write-Behind를 선택하고, 공통 Redisson 저장소와 Exposed DB 처리로 연결되는 구조를 보여준다.
다이어그램 크게 보기
Near Cache는 로컬 캐시(L1)와 Redis(L2)로 구성한다. Lettuce 구현은 Caffeine을 L1으로 사용하고, Redisson 구현은 RLocalCachedMap의 로컬 캐시를 사용한다. Exposed는 Near Cache의 구성 요소가 아니라 캐시 미스와 쓰기 작업에서 DB를 조회·변경하는 접근 계층이다.
UserCacheRepository는 READ_WRITE_THROUGH_WITH_NEAR_CACHE를 사용한다.RLocalCachedMap을, Lettuce의 suspend 경로는 LettuceSuspendNearCache와 Caffeine을 사용한다.EntityMapLoader가 캐시 미스 시 Exposed SELECT를 실행한다.EntityMapWriter가 Write-Through 또는 Write-Behind 배치에서 Exposed 트랜잭션을 실행한다.deleteFromDBOnInvalidate가 단순 캐시 제거인지 DB 삭제까지 포함하는지 결정한다.UserCacheRepository
READ_WRITE_THROUGH_WITH_NEAR_CACHE
UserCredentialsCacheRepository
READ_ONLY_WITH_NEAR_CACHE
UserEventCacheRepository
WRITE_BEHIND_WITH_NEAR_CACHE
HTTP → JdbcCacheRepository
→ RLocalCachedMap
→ EntityMapLoader / EntityMapWriter
→ Exposed transaction → DB
Redisson local cache (L1)
+ Redis RLocalCachedMap (L2)
+ AbstractJdbcRedissonRepository
+ Exposed DB loader / writer
Caffeine (L1)
+ Redis via Lettuce (L2)
+ LettuceSuspendNearCache
+ AbstractSuspendedJdbcLettuceRepository
+ Exposed DB loader / writer
LettuceCacheConfig
+ RedisCacheManager
+ Exposed CountryRepository
+ LettuceSuspendedCache
+ CachedCountrySuspendedRepository
+ Exposed suspended repository
+ 로컬 L1 캐시 없음
같은 JVM에서 반복되는 조회는 로컬 캐시에서 끝나므로 Redis 네트워크 왕복을 줄일 수 있다. 다만 여러 인스턴스의 로컬 값이 언제 무효화되는지, 재연결 후 값을 어떻게 맞추는지까지 운영 지표에 포함해야 한다.
Redisson 무효화 메시지나 Lettuce RESP3 추적 알림이 지연되면 JVM별 로컬 캐시가 일시적으로 다른 값을 반환할 수 있다. 허용 가능한 최대 지연을 먼저 정한다.
Redis 적재 후 DB 배치 전에 프로세스나 Redis가 복구되지 못하면 아직 DB에 저장되지 않은 변경이 유실될 수 있다. “요청 수락”을 “DB 반영 완료”로 표시하지 않는다.
deleteFromDBOnInvalidate 설정을 확인하지 않고 invalidate를 호출하면 기대와 다른 DB 삭제가 발생할 수 있다. 캐시 제거 API와 업무 삭제 API를 구분한다.
CacheStrategyComparisonBenchmark가 실제 PostgreSQL Testcontainers와 HikariCP를 사용해 NoCache, ReadThrough, WriteThrough를 비교한 결과다. 값은 평균 처리 시간(us/op)이며 낮을수록 빠르다.
차트 크게 보기
| 전략 / Payload | READ_HEAVY | WRITE_HEAVY |
|---|---|---|
| NoCache / 256B | 547.7 us/op | 505.3 us/op |
| ReadThrough / 256B | 87.5 us/op | 478.6 us/op |
| WriteThrough / 256B | 50.5 us/op | 448.6 us/op |
| NoCache / 4KB | 482.6 us/op | 529.4 us/op |
| ReadThrough / 4KB | 95.9 us/op | 497.9 us/op |
| WriteThrough / 4KB | 55.9 us/op | 491.9 us/op |
해석 범위: READ_HEAVY는 읽기 90%·쓰기 10%, WRITE_HEAVY는 읽기 10%·쓰기 90% 조건이다. 이 결과는 Redis Near Cache, Write-Behind, 운영 환경의 처리량이나 SLA를 측정하지 않는다.
Redis는 Testcontainers로 실행된다. 01과 02는 Redisson 기반 저장 전략을 검증한다. 06은 Spring Cache와 Lettuce를, 07은 코루틴용 LettuceSuspendedCache를 Exposed 저장소에 적용한다. 06과 07은 Redis 전용 캐시 예제이며 로컬 L1 캐시는 포함하지 않는다.
# Redisson + Exposed 저장 전략
./gradlew :01-cache-strategies:test
./gradlew :01-cache-strategies:bootRun
./gradlew :02-cache-strategies-coroutines:test# Spring Cache + Lettuce + Exposed
./gradlew :06-spring-cache:test
# LettuceSuspendedCache + Exposed
./gradlew :07-spring-suspended-cache:test