Development··6 min read

Redis 캐시 전략 실전 가이드

Redis를 캐시로 도입하면서 겪은 시행착오와 전략 선택 과정을 공유한다.

"캐시 붙이면 되죠"라고 쉽게 말했던 과거의 나에게

메인 API 응답 시간이 평균 800ms였다. PostgreSQL에서 여러 테이블 JOIN하고, 권한 체크하고, 데이터 가공해서 내려주는 과정이 복잡했다. DB 쿼리 자체는 200ms인데, 애플리케이션 레벨에서 권한 체크, 데이터 변환, 직렬화가 600ms를 잡아먹었다.

캐시는 붙이는 게 어려운 게 아니다. 잘 관리하는 게 어렵다. 이걸 1년 동안 뼈저리게 느꼈다.

Cache-Aside로 시작했더니

가장 흔한 패턴이다. 요청이 오면 먼저 Redis를 확인하고, 없으면 DB에서 조회한 뒤 Redis에 저장한다. TTL을 5분으로 설정하고 붙였더니 응답 시간이 800ms에서 50ms로 떨어졌다.

근데 문제가 바로 터졌다. 관리자가 데이터를 수정했는데 5분간 이전 데이터가 보이는 거다. "방금 수정했는데 반영이 안 돼요" 문의가 하루에 3번씩 들어왔다.

캐시 무효화라는 늪

TTL을 30초로 줄였다. 문의는 줄었는데 캐시 히트율이 60%로 떨어졌다. DB 부하가 다시 올라갔다.

결국 Write-Through 패턴을 섞었다. 데이터가 수정될 때 캐시도 함께 갱신하는 방식. 코드가 좀 복잡해졌지만, TTL을 10분으로 늘리면서도 실시간 반영이 가능해졌다.

근데 캐시 갱신 로직이 여러 곳에 흩어지면서, 한 곳에서 캐시를 갱신하지 않는 버그가 발생했다. 캐시 무효화가 컴퓨터 과학에서 가장 어려운 문제 중 하나라는 말이 괜히 있는 게 아니다.

(하루종일 "왜 데이터가 안 바뀌지?"를 추적한 적이 있다. 결국 캐시 갱신 한 줄이 빠져있었다.)

캐시 키를 대충 설계하면

처음에 키를 user:123처럼 단순하게 만들었다. 근데 같은 사용자인데 요청하는 사람의 권한에 따라 보여지는 데이터가 달라야 했다. 관리자는 모든 필드를, 일반 사용자는 공개 필드만.

키를 user:123:role:admin으로 바꿨더니 역할이 3개이고 사용자가 만 명이면 캐시 항목이 3만 개가 됐다. Redis 메모리가 급격히 늘어났다. 결국 공개 데이터와 비공개 데이터를 분리해서 캐싱하고, 응답 시점에 조합하는 방식으로 바꿨다.

캐시 키 설계를 처음부터 잘했으면 이 삽질을 안 했을 텐데.

캐시 스탬피드로 DB가 죽을 뻔

트래픽이 몰리는 시간에 캐시 TTL이 만료되면 동시에 수십 개의 요청이 DB로 몰린다. 캐시 스탬피드라고 한다. 실제로 이 현상 때문에 DB 커넥션 풀이 고갈된 적이 있다.

뮤텍스 패턴을 적용했다. 캐시가 만료되면 하나의 요청만 DB를 조회하게 하고, 나머지는 약간의 지연 후 갱신된 캐시를 읽도록 했다. Redis의 SETNX로 구현하면 된다. 간단한데 효과가 확실하다.

모니터링 없으면 의미가 없다

Redis를 붙이고 나서 가장 중요하게 본 지표는 세 가지다. 캐시 히트율, 메모리 사용량, 키 개수.

히트율이 80% 아래로 떨어지면 TTL이나 캐시 전략을 재검토했다. INFO stats 명령어로 히트율을 주기적으로 확인하는 스크립트를 만들어서 Slack 알림으로 보냈다.

1년 운영하고 내린 결론

캐시는 성능 문제의 만능 해결책이 아니라 새로운 복잡도를 도입하는 거다. DB 쿼리를 최적화하는 게 먼저이고, 그래도 부족할 때 캐시를 고려해야 한다.

도입한다면, 무효화 전략과 모니터링을 캐시 구현과 동시에 만들어야 한다. 나중에 하겠다고 미루면 결국 데이터 불일치로 사용자에게 피해가 간다.

관련 글