캐시는 쉬워 보이지만, 한 번씩은 데인다
내가 Redis를 처음 붙인 것도 캐시 때문이었다. 특정 조회가 느려서 “이거 Redis로 캐싱하면 되잖아” 하고 시작했다. 값을 넣고 빼는 것 자체는 #1에서 본 SET/GET이 전부라 30분이면 붙인다.
진짜 어려운 건 그 다음이었다. 언제 채우고, 언제 비우고, 원본이 바뀌면 어떻게 맞출 것인가. 캐시로 데이는 사람은 대부분 이 정합성에서 데인다. 이번 편은 그걸 순서대로 짚는다.
Cache-Aside — 캐시의 기본기
가장 널리 쓰는 패턴이다. 이름은 거창하지만 하는 일은 단순하다. “먼저 캐시를 보고, 없으면 DB에서 가져와 캐시에 채운다.”
1. Redis에 있나? (GET)
→ 있으면 그대로 반환 (Cache Hit)
→ 없으면 ↓
2. DB에서 조회
3. 결과를 Redis에 저장 (SET, TTL 필수)
4. 반환
코드로도 이 흐름 그대로다.
public User getUser(Long id) {
String cached = redis.get("user:" + id);
if (cached != null) {
return deserialize(cached); // 캐시 히트
}
User user = userRepository.findById(id); // 캐시 미스 → DB
redis.set("user:" + id, serialize(user), Duration.ofHours(1)); // TTL 1시간
return user;
}
처음 캐시를 붙이면 딱 여기까지 짜고 “됐다” 한다. 나도 그랬다. 근데 여기엔 함정이 두 개 숨어 있다.
함정 1: TTL을 안 걸면 언젠가 터진다
위 코드에서 Duration.ofHours(1)을 빼면 어떻게 될까. 캐시가 영원히 남는다. 처음엔 아무 문제 없어 보인다. 그러다 서서히 두 가지가 온다.
- 메모리가 계속 찬다. 캐시가 쌓이기만 하고 안 비워져서 결국
maxmemory에 도달한다. 그때부터 Redis가 키를 쫓아내거나 쓰기를 거부하기 시작한다. - 낡은 데이터가 안 죽는다. 원본이 바뀌어도 캐시는 옛 값을 계속 준다.
그래서 캐시엔 거의 항상 TTL을 건다. 기준은 간단하다. “이 데이터, 최대 얼마나 낡아도 사용자가 괜찮은가?” 자주 안 바뀌면 길게(몇 시간), 자주 바뀌면 짧게(몇십 초~몇 분). 캐시는 원래 비워지라고 있는 거다. 안 비워지는 캐시는 캐시가 아니라 메모리 누수다.
SET user:1 "..." EX 3600 # 저장과 동시에 TTL 1시간
함정 2: 원본이 바뀌면 캐시는 어긋난다
이게 진짜다. DB의 사용자 정보를 수정했는데 Redis엔 옛 값이 남아 있으면, 사용자는 방금 자기가 바꾼 값이 아니라 낡은 값을 본다. “분명 저장했는데 왜 그대로예요?” 문의가 이렇게 나온다.
가장 단순하고 안전한 해법은 수정할 때 그 캐시를 지우는 것이다.
public void updateUser(User user) {
userRepository.save(user); // 1. DB 먼저 수정
redis.delete("user:" + user.getId()); // 2. 캐시는 갱신(X) 삭제(O)
}
여기서 두 가지가 포인트다.
첫째, 캐시를 새 값으로 “덮어쓰기"가 아니라 “삭제"한다. 처음엔 “수정했으니 캐시도 새 값으로 업데이트하면 되잖아?” 싶은데, 그러면 동시에 여러 요청이 수정할 때 어느 값이 최종인지 꼬인다. 그냥 지워버리면 다음 조회가 Cache-Aside를 타고 DB에서 최신값을 다시 채운다. 훨씬 안전하다.
둘째, 순서가 중요하다. DB를 먼저 바꾸고 캐시를 지운다. 캐시를 먼저 지우면, 그 찰나에 다른 요청이 아직 안 바뀐 DB 값을 읽어 캐시를 옛 값으로 다시 채워버릴 수 있다. “DB → 캐시 삭제” 순서를 지키자.
미리 알아두면 좋은 함정들
위 두 개가 기본이고, 규모가 커지면 이런 것도 만난다. 지금 당장은 아니어도 이름은 알아두자.
- 캐시 스탬피드(Thundering Herd): 인기 데이터의 캐시가 만료되는 그 순간, 수많은 요청이 동시에 “캐시 없네?” 하고 전부 DB로 몰린다. TTL에 약간의 무작위(jitter)를 주거나, 캐시 갱신은 한 요청만 하도록 락을 걸어 완화한다.
- 히트율이 낮으면 오히려 손해: 캐시가 거의 안 맞으면 “캐시 확인 → 미스 → DB → 캐시 저장"이 그냥 DB 조회보다 느리다. 자주 조회되는 데이터에만 걸어야 한다. 안 그러면 Redis만 놀고 느려진다.
- 너무 큰 값: 수백 KB짜리를 캐시하면 꺼낼 때마다 네트워크·메모리를 크게 먹는다. 캐시는 작고 자주 읽히는 데이터일수록 이득이다.
마치며
캐시는 Redis의 시작이자 가장 실수하기 쉬운 부분이다. 정리하면 Cache-Aside + TTL + 수정 시 삭제, 이 세 개만 지켜도 대부분의 캐시는 조용히 잘 굴러간다. 나머지는 규모가 커진 다음 얘기다.
다음 편에서는 캐시 말고 Redis를 진짜 다양하게 쓰는 실전 패턴 — 세션, Rate Limiting, 분산 락, 랭킹 — 을 한 번에 훑는다. “이런 것도 Redis로 하네” 싶은 것들이다.