콘텐츠로 이동

Bluetape4k Cache Part 2: Near Cache, Redis 왕복을 줄이는 현실적인 방법

Near cache workbench with robot workers connecting local and remote cache blocks
Remote cache는 창고입니다. Near Cache는 책상 위 작은 서랍입니다.

Remote cache는 좋은 출발점입니다. 여러 JVM이 같은 값을 보고, TTL을 줄 수 있고, Redis 같은 성숙한 시스템을 쓰면 운영 도구도 많습니다.

문제는 remote라는 말에 비용이 붙는다는 점입니다. 네트워크를 타고, 값을 직렬화하고, connection과 event loop를 공유합니다. Redis가 아무리 빨라도 “프로세스 안 메모리 읽기”와 같은 비용 구조로 움직이지는 않습니다. 물리 법칙은 설득이 안 됩니다. 회의에 불러도 안 옵니다.

Near Cache는 이 차이를 인정하는 설계입니다. 자주 읽는 값은 JVM 안 L1에 두고, 공유 상태와 무효화는 Redis 같은 L2와 protocol에 맡깁니다.

Near cache read, write, and invalidation flow with local L1 cache and Redis L2 cache
L1 hit는 JVM 안에서 끝나고, miss와 write만 L2로 갑니다. 핵심은 invalidation입니다.

Remote cache의 장점은 분명합니다.

  • 여러 애플리케이션 인스턴스가 같은 값을 공유합니다.
  • 재시작해도 캐시가 바로 사라지지 않습니다.
  • TTL, eviction, memory 정책을 중앙에서 다룰 수 있습니다.

하지만 hot read 경로에서는 아래 비용이 계속 쌓입니다.

비용설명
network RTTlocalhost Redis도 JVM memory read보다 훨씬 멉니다
serialization값이 커질수록 encode/decode 비용이 보입니다
connection contention요청이 몰리면 Redis connection과 event loop도 바쁩니다
tail latency평균은 괜찮아도 P99에서 튀는 값이 사용자 눈에 보입니다

Near Cache는 “전부 local에 넣자”가 아닙니다. “뜨거운 읽기는 local에서 끝내고, 공유 상태와 무효화는 remote cache와 protocol에 맡기자”에 가깝습니다.

Redisson의 RLocalCachedMap은 local cached map을 제공합니다. 각 JVM에 local map을 두고, 변경 이벤트를 Pub/Sub로 전파해서 다른 JVM의 local entry를 무효화합니다.

장점은 명확합니다.

  • 사용자가 직접 protocol을 설계하지 않아도 됩니다.
  • Redisson API 안에서 local cache와 invalidation을 함께 다룹니다.
  • write-heavy 서비스보다 read-heavy 서비스에서 효과가 큽니다.

관련 소스: RedissonNearCache.kt, LocalCachedMapOptionsExtensions.kt

주의점도 있습니다. Pub/Sub는 topic 기반 이벤트 전파입니다. 어떤 key를 누가 읽고 있는지 Redis 서버가 자동으로 아는 모델은 아닙니다. client와 library가 invalidation 체계를 함께 유지합니다. 그래서 설정은 간단해 보여도, 실제 의미는 “변경 이벤트를 받아 local entry를 제대로 지운다”입니다.

Lettuce NearCache는 Redis RESP3의 CLIENT TRACKING을 사용합니다. Redis 서버가 client가 읽은 key를 추적하고, 해당 key가 바뀌면 push invalidation을 보냅니다.

val cache = LettuceCaches.nearCache<String>(redisClient) {
cacheName = "catalog"
maxLocalSize = 50_000
frontExpireAfterWrite = Duration.ofMinutes(10)
redisTtl = Duration.ofHours(1)
useRespProtocol3 = true
}

관련 소스: LettuceNearCache.kt, LettuceNearCacheConfig.kt, LettuceNearCacheTrackingTest.kt

RESP3 방식의 매력은 서버가 key tracking에 참여한다는 점입니다. 애플리케이션은 L1을 빠르게 쓰고, Redis는 변경된 key에 대해 invalidation을 push합니다. “내가 뭘 읽었더라?”를 client가 일기장에 적는 대신 Redis가 출석부를 들고 있는 모양입니다. 물론 출석부도 잘 관리해야 합니다.

bluetape4k-cache-lettuce에는 Near Cache benchmark가 있습니다.

실행:

Terminal window
./gradlew :bluetape4k-cache-lettuce:benchmark

관련 소스: NearCacheBenchmark.kt, Benchmark.ko.md

Lettuce Near Cache benchmark summary showing L1 hit far above Redis-backed operations
L1 hit은 Redis-backed operation과 아예 다른 체급입니다.

요약하면 이렇습니다. 처리량은 ops/ms이고 높을수록 좋습니다. 원본 리포트의 오차 범위까지 같이 보면, L1은 압도적으로 빠르고 L2 계열은 Redis RTT 근처에 모입니다.

벤치마크512B4096B16384B
l1Hit65,560 ± 10,86163,458 ± 23,12064,580 ± 9,507
l2Hit4.067 ± 0.5324.130 ± 0.4523.930 ± 1.370
l2Miss3.961 ± 1.3943.917 ± 0.7844.208 ± 0.408
putSingle2.119 ± 0.1002.077 ± 0.2762.014 ± 0.152
putAll1.038 ± 0.2470.930 ± 0.1180.407 ± 0.281
removeSingle4.213 ± 0.1774.243 ± 0.3524.164 ± 0.271

읽기 경로에서 볼 숫자는 l1Hitl2Hit입니다. L1 hit은 약 64K ops/ms입니다. L2 hit/miss는 약 4 ops/ms입니다. 이 숫자를 보고도 hot read를 매번 Redis로 보내고 싶다면, Redis가 아니라 마음의 캐시를 먼저 비워야 합니다.

측정 환경도 같이 봐야 합니다. 결과는 Apple M4 Pro, GraalVM 21, Redis 7 Testcontainers, JMH 1.37, 단일 스레드 기준입니다. 다른 장비에서는 절대값이 바뀌지만, “JVM L1 memory path와 Redis-backed path는 체급이 다르다”는 결론은 쉽게 바뀌지 않습니다.

Benchmark의 결론은 “Redis가 느리다”가 아닙니다. Redis는 빠릅니다. 다만 JVM 안 memory read와 remote operation은 같은 경주가 아닙니다.

실무에서는 이렇게 해석하는 편이 좋습니다.

  • read-heavy hot key는 NearCache 후보입니다.
  • write-heavy 데이터는 invalidation 비용과 stale 허용 범위를 먼저 봅니다.
  • 큰 payload는 putAll에서 network bandwidth와 serialization 비용을 봅니다.
  • L1 hit ratio가 낮으면 NearCache는 예쁜 장식이 됩니다. 장식은 CPU를 줄이지 않습니다.
  • 장애 시 fallback 정책을 정하지 않으면 캐시가 장애 전파자가 됩니다.
상황추천
Redisson API를 이미 많이 사용Redisson local cached map 경로
Lettuce 중심이고 Redis RESP3를 쓸 수 있음Lettuce NearCache
JCache 표준 API가 중요NearJCache / SuspendNearJCache
coroutine 서비스SuspendNearCacheOperations
Redis 장애 graceful degradation 필요resilient decorator

다음 글에서는 이 NearCache를 Exposed repository에 붙입니다. 읽기 전략, 쓰기 전략, write-behind queue까지 가면 캐시는 성능 기능에서 데이터 일관성 설계로 넘어갑니다. 캐시가 갑자기 진지해지는 순간입니다.

댓글

GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.