Redis 캐시 Q&A — 선택지, 실패 모드 8가지, 25문답
캐시 전략과 자료구조 선택지를 한 표로 모으고, 운영에서 실제로 밟는 실패 모드 여덟 가지와 대응, 그리고 설계·무효화·동시성·운영에 걸친 25개 질문에 결론부터 답한다.
Redis 캐시를 설계할 때 반복해서 돌아오는 질문들이 있다. "이걸 캐싱해도 되나", "캐시를 붙였는데 왜 더 느린가", "무효화를 어디까지 해야 하나" 같은 것들이다. 이 글은 앞 세 편의 결론을 질문 형태로 모아 결론부터, 근거는 그다음 순서로 답한다.
먼저 선택지 전체를 한 표에 모으고, 운영에서 실제로 밟는 실패 모드 여덟 가지를 정리한 뒤, 25개 질문에 답한다. 각 주제의 전체 맥락은 Redis 캐시 설계, 캐시 무효화와 동시성, Redis에 담을 것과 담지 않을 것에 있다.
선택지 한눈에
각 선택지는 무언가를 얻고 무언가를 내준다. 어느 쪽이 우월한 것이 아니라 어느 조건에서 맞는가의 문제다.
| 선택지 | 장점 | 단점 | 언제 쓰나 |
|---|---|---|---|
| Look-Aside | 캐시가 죽어도 서비스가 지속된다 | 첫 요청 miss, 스탬피드 노출 | 기본값 |
| Write-Back 카운터 | 쓰기 부하가 급감한다 | 배치 반영 전 유실 | 조회수·좋아요 수 |
| Spring Cache | 코드 침투가 최소다 | 프록시 한계, TTL 별도 구성 | 반환값 단위 캐싱 |
| RedisTemplate 직접 | 원자 연산·자료구조가 자유롭다 | 직렬화·TTL을 직접 관리 | 카운터·Set·랭킹 |
@RedisHash + @Indexed | 엔티티처럼 다룰 수 있다 | 인덱스 키 부산물, 정합성 관리 | 단순 조건 조회 |
| Pub/Sub | 가볍고 지연이 낮다 | 유실 허용이 전제다 | 캐시 무효화 전파·알림 |
| 커서 페이징 | 성능이 일정하고 중복이 없다 | 페이지 점프가 불가능하다 | 무한 스크롤 |
실패 모드 8가지
캐시를 붙인 시스템이 실제로 무너지는 방식이다. 증상만 보면 전부 "느려졌다"로 보이므로 원인 열이 진단의 핵심이다.
| 실패 모드 | 증상 | 원인 | 대응 |
|---|---|---|---|
| Redis 다운 | DB CPU 급등, 전체 지연 | 캐시 부재로 전 트래픽이 DB로 간다 | 서킷 브레이커로 DB 보호, 로컬 캐시 1차 방어, Sentinel/Cluster |
KEYS * 실행 | 전 API가 순간 타임아웃 | 단일 스레드가 블로킹된다 | SCAN 강제, 운영 계정 명령 차단(rename-command) |
| 캐시 무효화 누락 | 수정이 반영되지 않는다 | 연관 목록 캐시를 지우지 않았다 | @Caching으로 묶기, TTL 안전망 |
| 카운터 유실 | 조회수가 줄어든다 | Write-Back 반영 전 Redis가 재기동됐다 | flush 주기 단축, AOF 병행, 감소 방지 로직(max 비교) |
| 대형 키(hot key) | 특정 노드만 CPU 100% | 인기 채널 하나에 트래픽이 집중된다 | 키 분할(샤딩), 로컬 캐시로 흡수, 읽기 레플리카 |
| 직렬화 오류 | 배포 후 캐시 역직렬화 실패 | 클래스 필드가 바뀌었다 | JSON 직렬화 + 캐시 키에 버전 포함 |
| 메모리 초과 | 쓰기 실패 또는 대량 축출 | maxmemory-policy 오설정, TTL 없는 키가 쌓였다 | allkeys-lru, 전 키 TTL 강제, 키 증가 모니터링 |
| 양방향 구독 키 불일치 | 구독 목록과 구독자 수가 맞지 않는다 | 두 Set 중 한쪽만 갱신됐다 | Lua/MULTI로 묶기 + 정합성 배치 검증 |
캐싱 대상과 전략
| 질문 | 결론 |
|---|---|
| 어떤 데이터를 캐싱 대상으로 고르나 | 원본 접근 비용이 크고, 반복적으로 같은 결과를 주고, 읽기 대 쓰기 비가 높고, stale을 몇 초 허용할 수 있는 것 |
| 캐시를 붙였는데 더 느려졌다면 | 히트율이 낮은 것이다. 왕복 비용에 miss 시 이중 조회가 더해진다. 히트율 지표부터 확인하고 키 설계·TTL·대상을 재선정한다 |
| Look-Aside와 Read-Through의 차이는 | 캐시 miss를 앱이 처리하느냐(Look-Aside) 캐시 계층이 처리하느냐(Read-Through)의 차이다. @Cacheable은 후자 형태다 |
| Write-Back을 조회수에 쓰는 이유와 위험은 | 쓰기 폭주를 흡수하는 것이 이득이고, 위험은 배치 반영 전 유실이다. 조회수는 소량 유실을 감내할 수 있는 데이터라 성립한다 |
| 캐시를 갱신하지 않고 삭제하는 이유는 | 동시 쓰기 시 옛 값이 덮이는 경쟁 조건을 피하려는 것이다. 다음 읽기가 원본을 다시 채우게 한다 |
| 캐시 도입 성과를 무엇으로 증명하나 | 캐시 히트율, p95·p99 응답시간, DB QPS·CPU 감소, 에러율. 도입 전후를 동일 조건에서 비교한다 |
무효화와 3대 장애
| 질문 | 결론 |
|---|---|
| 캐시 스탬피드란 무엇이고 어떻게 대응하나 | 인기 키가 만료되는 순간 요청이 동시에 miss하는 현상이다. 분산 락으로 하나만 재계산하게 하거나, 논리적 만료·TTL 지터·사전 워밍을 쓴다 |
@Cacheable(sync=true)로 스탬피드가 해결되나 | 해결되지 않는다. 같은 JVM 안에서만 직렬화되므로 인스턴스가 여러 대면 그 수만큼 DB 조회가 발생한다 |
| Cache Penetration과 Avalanche를 구분하면 | 전자는 존재하지 않는 키를 반복 조회하는 것이고(null 캐싱·Bloom Filter), 후자는 다수 키의 동시 만료나 캐시 다운이다(TTL 지터·다층 캐시·서킷 브레이커) |
자료구조와 동시성
| 질문 | 결론 |
|---|---|
| 조회수 증가를 왜 별도 API로 뺐나 | 본문 조회와 카운팅은 요구 성능·정합성이 다르다. 합치면 조회마다 행 갱신과 캐시 무효화 연쇄가 일어난다 |
Redis INCR이 왜 동시성에 안전한가 | 단일 스레드 순차 실행이라 read-modify-write가 분할되지 않는다. 앱에서 GET 후 SET을 하면 lost update가 난다 |
| 좋아요에 Set을 쓰는 이유는 | 중복 불가와 순서 무의미라는 요구가 Set의 성질과 정확히 일치한다. SISMEMBER·SCARD가 O(1)이다 |
| 랭킹에 Sorted Set을 쓰는 이유는 | 정렬 상태를 항상 유지해 상위 N이 O(log N + M)이다. ZINCRBY로 증가와 재정렬이 원자적으로 처리된다 |
| 좋아요 API를 토글로 만들면 안 되는 이유는 | 비멱등이라 재시도·중복 클릭에 결과가 뒤집힌다. like/unlike를 분리해 멱등성을 확보한다 |
| 다대다(구독)를 Redis로 어떻게 푸나 | 조회 방향별로 양방향 Set을 유지한다. 대가는 정합성 책임이 애플리케이션으로 옮겨 오는 것이다 — Lua/MULTI와 검증 배치로 받는다 |
| 분산 락을 Redis로 구현할 때 필수 요소는 | NX + 만료 + 랜덤 토큰 + Lua 해제. Redisson watchdog을 권한다. 정합성의 최후 방어선은 DB다 |
Spring과 운영
| 질문 | 결론 |
|---|---|
KEYS *를 왜 금지하나 | O(N)이고 단일 스레드를 점유해 전 명령이 블록된다. SCAN 커서 방식을 쓴다 |
@RedisHash의 @Indexed는 어떻게 동작하나 | Redis에 인덱스가 없으므로 Spring Data가 보조 Set 키를 별도로 만들어 흉내 낸다. 정리 책임과 유령 참조 위험이 따라온다 |
| Spring Cache가 동작하지 않는 대표 사례는 | 같은 클래스 내부의 자기 호출이다. 프록시를 거치지 않아 애너테이션이 무시된다 |
| RDB와 AOF를 비교하면 | 스냅샷과 명령 로그의 차이다. 유실 범위·복구 속도·파일 크기·fork 부하가 서로 반대 방향이라 실무는 혼합해 쓴다 |
| 순수 캐시라면 영속화를 꺼도 되나 | 가능하다. 원본이 RDBMS에 있기 때문이다. 단 재기동 직후 전량 miss를 DB가 견딜 수 있어야 한다 |
| Sentinel과 Cluster의 차이는 | Sentinel은 HA 페일오버만 담당하고 Cluster는 샤딩까지 한다. Cluster는 멀티키 연산에 해시 태그 제약이 있다 |
| Redis Pub/Sub을 이벤트 버스로 써도 되나 | 전달 보장과 영속성이 없으므로 유실을 허용하는 용도에만 쓴다. 유실이 불가하면 Kafka 또는 Redis Streams다 |
| 페이지 기반 대신 커서 기반을 쓰는 이유는 | offset 스킵 비용과 total count를 제거하고, 신규 데이터로 인한 slice 중복을 막는다 |
| 부모 댓글 삭제 시 대댓글 처리는 | 전체 삭제, 삭제 금지, soft delete 마크의 세 가지가 있다. 맥락 보존과 사용자 경험 때문에 세 번째가 실무 표준이다 |
Q. 트래픽 피크마다 DB CPU가 튄다. 무엇부터 보겠는가
히트율이 언제 떨어지는가부터 본다. 평소에는 높은데 피크에만 떨어진다면 원인은 캐시 용량이 아니라 타이밍이다.
확인 순서와 각 단계의 처방은 다음과 같다.
| 순서 | 무엇을 보나 | 판단 지표 | 그렇다면 |
|---|---|---|---|
| 1 | 캐시 히트율과 miss가 몰리는 키 분포 | 시간대별 히트율 | 피크에만 떨어지면 2번으로 |
| 2 | 인기 키의 동시 만료 | 같은 시각에 적재된 키의 TTL | TTL에 지터를 주고, 상위 키는 분산 락 또는 논리적 만료로 재계산을 한 번만 수행하게 바꾼다 |
| 3 | 축출 | evicted_keys | maxmemory에 닿아 LRU가 인기 키까지 밀어내는 상황이다. TTL 조정이 아니라 메모리 증설 또는 캐시 대상 축소가 답이다 |
| 4 | Penetration | 존재하지 않는 ID 요청 비율 | null을 짧은 TTL로 캐싱하고 입력 검증을 앞단에 둔다 |
네 단계를 다 밟아도 DB가 위험하면 서킷 브레이커로 DB를 보호하는 것이 우선이다. 원인 규명보다 서비스 보전이 먼저다.
Q. 조회수를 카운터로 옮기면 유실이 생긴다. 어떻게 정당화하는가
데이터마다 요구되는 정합성 등급이 다르다는 전제에서 출발한다. 결제 금액은 1원도 틀리면 안 되지만, 조회수는 수십 건 오차가 사용자 경험이나 비즈니스 판단을 바꾸지 않는다.
| 항목 | 내용 |
|---|---|
| 무엇을 내주나 | 배치 반영 전 재기동 시 수 초 분량의 카운트 |
| 무엇을 얻나 | 인기 컨텐츠의 행 락 경합 제거, 본문 캐시가 조회마다 무효화되던 연쇄의 차단 |
| 유실을 어떻게 좁히나 | 배치 flush 주기 단축, AOF everysec 병행 |
| 어떤 사고를 막나 | 반영 시 max 비교로 카운트가 뒤로 가는 현상 차단 |
정리하면 이것은 정확도를 포기한 결정이 아니라 정확도 요구가 낮은 데이터에서만 성능을 사 온 교환이다. 요구 등급이 다른 데이터에 같은 패턴을 적용하면 안 된다는 선이 함께 그어진다.
Q. 캐시 무효화의 어려운 지점은 정확히 어디인가
단건이 아니라 파생 캐시다. 비디오 제목을 바꾸면 단건 키뿐 아니라 그 비디오가 들어간 목록 캐시, 검색 결과, 랭킹 스냅샷까지 낡는다. 무효화해야 할 키 집합을 코드가 정확히 알고 있어야 하는데, 기능이 늘수록 이 목록이 흩어진다.
규모에 따라 대응 단계가 올라간다.
| 단계 | 대응 | 무엇을 해결하나 |
|---|---|---|
| 1 | @Caching으로 연관 evict를 한 지점에 모아 선언 | 누락을 코드 리뷰에서 잡을 수 있게 한다 |
| 2 | 모든 캐시에 TTL 강제 | 무효화를 놓쳐도 유한 시간 내에 스스로 복구된다 |
| 3 | 키에 버전 포함(video:v3:{id}) | 스키마 변경·대량 무효화가 버전 증가 한 번으로 끝난다. 구 버전 키는 TTL로 소멸한다 |
| 4 | Pub/Sub으로 무효화 신호 브로드캐스트 | 다중 인스턴스의 로컬 캐시까지 닿는다 |
4단계에는 단서가 붙는다. Pub/Sub은 유실될 수 있으므로 로컬 캐시 TTL을 짧게 잡아 신호를 놓쳐도 오래 어긋나지 않게 설계해야 한다.
Q. 분산 락으로 재고 차감을 처리하면 안 되는 이유는
락의 상호 배제가 시간 가정에 의존하기 때문이다. 만료를 건 락은 보유자가 GC로 길게 멈추거나 네트워크가 지연되면 본인이 쥐고 있다고 믿는 사이에 TTL이 지나고, 다른 프로세스가 같은 락을 얻는다. 다중 마스터를 쓰는 Redlock도 노드 간 시계 오차와 프로세스 정지를 전제하면 안전성 증명이 성립하지 않는다는 지적을 받는다.
그래서 역할을 둘로 나눈다.
| 계층 | 역할 | 실패했을 때 |
|---|---|---|
| Redis 분산 락 | 불필요한 DB 경합을 줄이는 최적화 | 성능이 떨어질 뿐 데이터는 안전하다 |
| DB 조건부 갱신·유니크 제약 | 정합성의 최종 방어 | 여기가 뚫리면 돈이 나간다 |
UPDATE stock SET qty = qty - 1 WHERE id = ? AND qty > 0 같은 조건부 갱신으로 원자성을 확보하거나, 유니크 제약으로 중복 차감 자체를 불가능하게 만든다. 락 구현의 세부와 NX·랜덤 토큰·Lua 해제가 각각 무엇을 막는지는 캐시 무효화와 동시성에서 다룬다.
정리
이 시리즈를 관통하는 판단 기준은 세 개다.
| 기준 | 어디에 적용되나 |
|---|---|
| 이 데이터는 몇 초 낡아도 되는가 | 캐싱 대상 선정, TTL 값, Write-Back 채택 여부 |
| 요구사항의 성질이 어떤 자료구조의 성질과 일치하는가 | Set vs Sorted Set, String 카운터, Hash |
| 원본은 어디에 있는가 | Redis에 담을지 말지, 영속화를 켤지 말지, 유실을 감내할지 |
세 번째가 가장 자주 잊힌다. 원본이 RDBMS에 있는 한 Redis는 언제든 비우고 다시 채울 수 있는 계층이고, 그 성질이 영속화·축출·페일오버 결정을 전부 단순하게 만든다. 반대로 원본을 Redis에 두는 순간 캐시로 쓸 때는 필요 없던 운영 요구가 통째로 따라붙는다.