exposed-workshop / Visual Companion

Exposed + Local Cache + Redis

캐시 조회·갱신·비동기 저장 방식에 따라 Redis와 DB의 데이터 반영 시점이 달라진다

캐시는 단순히 조회 속도를 높이는 기능이 아니다. 어떤 값을 어디에서 읽고, 변경 결과를 언제 DB에 반영하며, 실패 시 어느 계층을 다시 맞출지 결정하는 데이터 처리 전략이다.

L1 / 로컬 캐시Caffeine 또는 Redisson 로컬 캐시
L2 / 원격 캐시Redis
DB 접근Exposed
사용자 이름 변경: Ada → Grace전략 시뮬레이션
API Cache DB
L1 / 로컬 캐시Caffeine / Redisson Local
miss
L2 / 원격 캐시Redis
user:42 = Ada
영구 저장 / DBExposed Table
users[42] = Ada
    캐시 미스가 발생하면 DB를 조회하고 두 캐시 계층을 채운다.조회 완료

    캐시 전략

    JdbcCacheRepository 구현은 캐시 조회와 DB 반영 시점을 Read-Through, Write-Through, Read-Only, Write-Behind로 구분한다. 데이터 변경 빈도와 허용 가능한 DB 반영 지연에 맞춰 전략을 선택한다.

    Read-Through

    캐시에 값이 없을 때 EntityMapLoader가 Exposed로 DB를 조회하고 Redis와 로컬 캐시에 적재한다.

    • 반복 조회 비용 감소
    • 첫 조회는 DB 지연 포함

    Write-Through

    EntityMapWriter가 캐시 변경과 DB 반영을 같은 요청 흐름에서 처리한다.

    • 응답 시점에 DB 반영 확인
    • 쓰기 지연 증가

    Read-Only

    자주 읽고 거의 바뀌지 않는 값을 캐시한다. 변경 시 명시적으로 무효화한 뒤 다음 조회에서 다시 적재한다.

    • 불필요한 쓰기 경로 제거
    • 무효화 누락 주의

    Write-Behind

    요청 수락 후 Redis에 먼저 기록하고 배치가 Exposed 트랜잭션으로 DB에 반영한다.

    • 쓰기 응답 시간 단축
    • DB 반영 완료와 응답 시점 분리

    캐시 전략 아키텍처

    요청 처리, 저장소 전략, 저장 및 검증 계층을 연결해 각 캐시 전략이 어느 경로에서 데이터를 조회하고 DB에 반영하는지 보여준다.

    요청 처리, 저장소 전략, 저장 및 검증 계층으로 구성된 캐시 전략 아키텍처 다이어그램
    요청에서 저장까지의 캐시 전략 구조 UserCacheRepository, UserCredentialsCacheRepository, UserEventCacheRepository가 각각 Read/Write-Through, Read-Only, Write-Behind를 선택하고, 공통 Redisson 저장소와 Exposed DB 처리로 연결되는 구조를 보여준다. 다이어그램 크게 보기

    Exposed와 Redis로 다양한 캐시 전략을 구현하는 방법

    Near Cache는 로컬 캐시(L1)와 Redis(L2)로 구성한다. Lettuce 구현은 Caffeine을 L1으로 사용하고, Redisson 구현은 RLocalCachedMap의 로컬 캐시를 사용한다. Exposed는 Near Cache의 구성 요소가 아니라 캐시 미스와 쓰기 작업에서 DB를 조회·변경하는 접근 계층이다.

    저장소 전략 선택UserCacheRepositoryREAD_WRITE_THROUGH_WITH_NEAR_CACHE를 사용한다.
    Redis 클라이언트와 로컬 캐시 선택Redisson은 RLocalCachedMap을, Lettuce의 suspend 경로는 LettuceSuspendNearCache와 Caffeine을 사용한다.
    DB 조회 연결EntityMapLoader가 캐시 미스 시 Exposed SELECT를 실행한다.
    DB 저장 연결EntityMapWriter가 Write-Through 또는 Write-Behind 배치에서 Exposed 트랜잭션을 실행한다.
    무효화 정책 확인deleteFromDBOnInvalidate가 단순 캐시 제거인지 DB 삭제까지 포함하는지 결정한다.
    11-high-performance / Redisson 전략
    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 Near Cache
    Redisson local cache (L1)
    + Redis RLocalCachedMap (L2)
    + AbstractJdbcRedissonRepository
    + Exposed DB loader / writer
    Lettuce Near Cache
    Caffeine (L1)
    + Redis via Lettuce (L2)
    + LettuceSuspendNearCache
    + AbstractSuspendedJdbcLettuceRepository
    + Exposed DB loader / writer
    09-spring / Lettuce Redis 캐시
    LettuceCacheConfig
    + RedisCacheManager
    + Exposed CountryRepository
    
    + LettuceSuspendedCache
    + CachedCountrySuspendedRepository
    + Exposed suspended repository
    
    + 로컬 L1 캐시 없음

    Near Cache의 효과는 Redis 요청 감소와 함께 측정해야 한다

    같은 JVM에서 반복되는 조회는 로컬 캐시에서 끝나므로 Redis 네트워크 왕복을 줄일 수 있다. 다만 여러 인스턴스의 로컬 값이 언제 무효화되는지, 재연결 후 값을 어떻게 맞추는지까지 운영 지표에 포함해야 한다.

    로컬 값의 일시적 불일치

    Redisson 무효화 메시지나 Lettuce RESP3 추적 알림이 지연되면 JVM별 로컬 캐시가 일시적으로 다른 값을 반환할 수 있다. 허용 가능한 최대 지연을 먼저 정한다.

    Write-Behind 유실 구간

    Redis 적재 후 DB 배치 전에 프로세스나 Redis가 복구되지 못하면 아직 DB에 저장되지 않은 변경이 유실될 수 있다. “요청 수락”을 “DB 반영 완료”로 표시하지 않는다.

    무효화와 삭제의 구분

    deleteFromDBOnInvalidate 설정을 확인하지 않고 invalidate를 호출하면 기대와 다른 DB 삭제가 발생할 수 있다. 캐시 제거 API와 업무 삭제 API를 구분한다.

    NoCache, ReadThrough, WriteThrough의 읽기 및 쓰기 중심 평균 처리 시간을 비교한 벤치마크 차트
    PostgreSQL + HikariCP smoke benchmark CacheStrategyComparisonBenchmark가 실제 PostgreSQL Testcontainers와 HikariCP를 사용해 NoCache, ReadThrough, WriteThrough를 비교한 결과다. 값은 평균 처리 시간(us/op)이며 낮을수록 빠르다. 차트 크게 보기
    전략 / PayloadREAD_HEAVYWRITE_HEAVY
    NoCache / 256B547.7 us/op505.3 us/op
    ReadThrough / 256B87.5 us/op478.6 us/op
    WriteThrough / 256B50.5 us/op448.6 us/op
    NoCache / 4KB482.6 us/op529.4 us/op
    ReadThrough / 4KB95.9 us/op497.9 us/op
    WriteThrough / 4KB55.9 us/op491.9 us/op

    해석 범위: READ_HEAVY는 읽기 90%·쓰기 10%, WRITE_HEAVY는 읽기 10%·쓰기 90% 조건이다. 이 결과는 Redis Near Cache, Write-Behind, 운영 환경의 처리량이나 SLA를 측정하지 않는다.

    예제를 실행하면 전략별 저장 시점과 Lettuce 적용 방식을 확인할 수 있다

    Redis는 Testcontainers로 실행된다. 0102는 Redisson 기반 저장 전략을 검증한다. 06은 Spring Cache와 Lettuce를, 07은 코루틴용 LettuceSuspendedCache를 Exposed 저장소에 적용한다. 0607은 Redis 전용 캐시 예제이며 로컬 L1 캐시는 포함하지 않는다.

    Redisson 예제 실행
    # Redisson + Exposed 저장 전략
    ./gradlew :01-cache-strategies:test
    ./gradlew :01-cache-strategies:bootRun
    ./gradlew :02-cache-strategies-coroutines:test
    Lettuce 예제 실행
    # Spring Cache + Lettuce + Exposed
    ./gradlew :06-spring-cache:test
    
    # LettuceSuspendedCache + Exposed
    ./gradlew :07-spring-suspended-cache:test