백엔드 엔지니어링수정 2026-08-18

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인기 키의 동시 만료같은 시각에 적재된 키의 TTLTTL에 지터를 주고, 상위 키는 분산 락 또는 논리적 만료로 재계산을 한 번만 수행하게 바꾼다
3축출evicted_keysmaxmemory에 닿아 LRU가 인기 키까지 밀어내는 상황이다. TTL 조정이 아니라 메모리 증설 또는 캐시 대상 축소가 답이다
4Penetration존재하지 않는 ID 요청 비율null을 짧은 TTL로 캐싱하고 입력 검증을 앞단에 둔다

네 단계를 다 밟아도 DB가 위험하면 서킷 브레이커로 DB를 보호하는 것이 우선이다. 원인 규명보다 서비스 보전이 먼저다.

Q. 조회수를 카운터로 옮기면 유실이 생긴다. 어떻게 정당화하는가

데이터마다 요구되는 정합성 등급이 다르다는 전제에서 출발한다. 결제 금액은 1원도 틀리면 안 되지만, 조회수는 수십 건 오차가 사용자 경험이나 비즈니스 판단을 바꾸지 않는다.

항목내용
무엇을 내주나배치 반영 전 재기동 시 수 초 분량의 카운트
무엇을 얻나인기 컨텐츠의 행 락 경합 제거, 본문 캐시가 조회마다 무효화되던 연쇄의 차단
유실을 어떻게 좁히나배치 flush 주기 단축, AOF everysec 병행
어떤 사고를 막나반영 시 max 비교로 카운트가 뒤로 가는 현상 차단

정리하면 이것은 정확도를 포기한 결정이 아니라 정확도 요구가 낮은 데이터에서만 성능을 사 온 교환이다. 요구 등급이 다른 데이터에 같은 패턴을 적용하면 안 된다는 선이 함께 그어진다.

Q. 캐시 무효화의 어려운 지점은 정확히 어디인가

단건이 아니라 파생 캐시다. 비디오 제목을 바꾸면 단건 키뿐 아니라 그 비디오가 들어간 목록 캐시, 검색 결과, 랭킹 스냅샷까지 낡는다. 무효화해야 할 키 집합을 코드가 정확히 알고 있어야 하는데, 기능이 늘수록 이 목록이 흩어진다.

규모에 따라 대응 단계가 올라간다.

단계대응무엇을 해결하나
1@Caching으로 연관 evict를 한 지점에 모아 선언누락을 코드 리뷰에서 잡을 수 있게 한다
2모든 캐시에 TTL 강제무효화를 놓쳐도 유한 시간 내에 스스로 복구된다
3키에 버전 포함(video:v3:{id})스키마 변경·대량 무효화가 버전 증가 한 번으로 끝난다. 구 버전 키는 TTL로 소멸한다
4Pub/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에 두는 순간 캐시로 쓸 때는 필요 없던 운영 요구가 통째로 따라붙는다.