캐시는 어디에 두고, 언제 지울 것인가
캐시를 쓰겠다는 말은 아무 정보도 아니다
면접에서 "여기에 캐시를 추가하겠습니다"라는 말을 정말 많이 듣습니다. 이 말 자체는 틀리지 않지만, 그것만으로는 아무것도 설계하지 않은 것과 같습니다. 캐시는 어디에 두는지, 무엇을 넣는지, 언제 지우는지에 따라 완전히 다른 물건이 됩니다. 이 세 가지 질문에 답하지 않고 "캐시를 씁니다"라고만 하면 면접관은 다음 질문으로 그 세 가지를 순서대로 물어볼 겁니다.
어디에 둘 것인가
캐시는 요청 경로의 여러 지점에 둘 수 있고, 각각 잡아주는 문제가 다릅니다.
- 클라이언트 캐시: 브라우저나 앱이 응답을 저장해두는 것. 네트워크 왕복 자체를 없앱니다. 정적 자산이나 자주 안 바뀌는 데이터에 적합합니다.
- CDN: 지리적으로 가까운 엣지 서버가 응답을 대신 줍니다. 이미지·비디오·정적 페이지처럼 사용자마다 결과가 같은 콘텐츠에 강력합니다.
- 애플리케이션 캐시(Redis, Memcached): 서버와 DB 사이에 둡니다. DB 쿼리 자체를 줄이는 게 목적입니다. 이 책에서 "캐시"라고 하면 대부분 이 계층을 가리킵니다.
- DB 자체 캐시: 데이터베이스 엔진이 내부적으로 자주 쓰는 페이지를 메모리에 올려둡니다. 애플리케이션이 신경 쓸 필요는 없지만 존재를 알아두면 "캐시를 껐는데도 왜 빨랐지" 같은 질문에 답할 수 있습니다.
인터뷰에서 자주 다루는 건 애플리케이션 캐시입니다. 여기서는 이 계층에 집중합니다.
캐시-어사이드가 기본값인 이유
캐시를 채우고 비우는 전략은 여러 가지가 있지만, 실무에서 가장 널리 쓰이고 인터뷰에서도 기본으로 요구되는 건 캐시-어사이드(cache-aside)입니다. 읽을 때는 캐시를 먼저 보고, 없으면 DB에서 가져와 캐시에 채웁니다. 쓸 때는 DB만 갱신하고 캐시는 지웁니다(갱신하지 않고 지우는 이유는 뒤에 나옵니다).
def get_user(user_id):
cached = redis.get(f"user:{user_id}")
if cached is not None:
return deserialize(cached)
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
redis.setex(f"user:{user_id}", ttl=300, value=serialize(user))
return user
def update_user(user_id, fields):
db.execute("UPDATE users SET ... WHERE id = %s", user_id)
redis.delete(f"user:{user_id}") # 갱신이 아니라 삭제갱신 대신 삭제를 선택하는 이유는 동시성 문제 때문입니다. 두 요청이 거의 동시에 같은 사용자를 수정한다고 해봅시다. 캐시를 직접 갱신하는 방식이면 DB에 나중에 반영된 값과 캐시에 나중에 쓰인 값의 순서가 어긋날 수 있고, 그 결과 캐시에는 이미 지나간 값이 남을 수 있습니다. 반면 삭제는 멱등적입니다. 여러 번 지워도 결과는 같고, 다음 읽기 요청이 DB에서 최신 값을 다시 채워 넣습니다.
TTL 없는 캐시는 시한폭탄이다
캐시 항목에 만료 시간(TTL)을 안 주면 무슨 일이 생길까요. 처음에는 잘 동작합니다. 문제는 DB에서 데이터가 삭제되거나 캐시 무효화 로직에 버그가 있을 때입니다. 이런 경우 캐시에 죽은 데이터가 영원히 남습니다. 저는 TTL을 안전망이라고 생각합니다. 무효화 로직을 아무리 잘 짜도 놓치는 경로가 반드시 생기고, TTL은 그 실수가 최대 얼마나 오래 지속되는지 상한을 정해줍니다. TTL을 짧게 잡으면 DB 부하가 늘고, 길게 잡으면 정합성이 흔들립니다. 이 트레이드오프에 정답은 없고, 데이터가 얼마나 자주 바뀌는지에 달려 있습니다.
캐시 스탬피드 — 캐시가 오히려 DB를 죽이는 순간
인기 있는 캐시 항목 하나의 TTL이 동시에 만료되면 어떻게 될까요. 그 순간 몰려 있던 수천 개의 요청이 전부 캐시 미스를 내고, 전부 동시에 DB로 향합니다. 캐시가 있을 때보다 캐시가 막 비워진 그 순간이 DB에는 더 위험합니다. 이걸 캐시 스탬피드(cache stampede) 또는 thundering herd라고 부릅니다.
sequenceDiagram participant U as 사용자 1000명 participant C as 캐시 participant D as DB U->>C: 인기 게시글 조회 (동시) Note over C: TTL 만료 직후, 캐시 미스 C->>D: 1000개 요청이 동시에 DB로 Note over D: DB 과부하, 응답 지연 또는 장애
해법은 몇 가지가 있습니다. 첫 번째 요청만 DB를 조회하고 나머지는 그 결과를 기다리게 하는 락(mutex) 방식, 만료 시간을 살짝씩 무작위로 흩어서 동시 만료를 막는 jitter 방식, 그리고 TTL이 지나도 이전 값을 잠깐 더 제공하면서 백그라운드로 갱신하는 stale-while-revalidate 방식이 대표적입니다. 인터뷰에서 "캐시를 쓰겠다"까지만 말하는 사람과 이 문제까지 짚는 사람은 확실히 다른 인상을 남깁니다.
캐시 적중률이 낮으면 캐시는 없는 것보다 못하다
마지막으로 짚을 건, 캐시가 항상 이득은 아니라는 점입니다. 접근 패턴이 골고루 퍼져 있어서 같은 키가 잘 재조회되지 않는 데이터(예: 각 사용자가 딱 한 번만 보는 추천 결과)라면 캐시 적중률이 낮고, 캐시를 채우고 관리하는 오버헤드만 남습니다. 캐시를 넣기 전에 "이 데이터가 얼마나 자주 재요청되는가"를 먼저 묻는 게 순서입니다. 이 질문에 답하지 못한 채 캐시부터 그리면, 면접관은 그 캐시가 실제로 무엇을 위한 것인지 되물을 겁니다.