캐시가 뭐에요?
회사 서비스에 Redis를 도입하게 되었습니다. 자주 읽는 데이터를 메모리에 보관해서 응답을 빠르게 하고 DB 부하를 줄이려는 목적이었어요. 문서를 따라 도입은 했지만, 정작 “캐시가 뭐예요?”라는 질문에는 짧은 답밖에 하지 못했습니다.
캐시는 다시 얻는 비용이 큰 데이터를 재사용하기 위해 가까운 곳에 보관하는 것입니다. 그런데 캐시 설계에서 더 어려운 질문은 “언제까지 이 값을 믿고 써도 되나”입니다. 이 글에서는 캐시의 효과부터 읽기·쓰기 전략, 오래된 값을 다루는 방법까지 연결해보겠습니다.
2022년 도입 경험을 출발점으로 쓴 글입니다. 전략의 구분과 일관성에 관한 설명을 보완했습니다. 아래 시간과 비율은 이해를 위한 가정이며 당시 서비스의 측정값이 아닙니다.
캐시가 빨라지는 조건
집 앞 가게에서 자주 쓰는 물건을 사면 매번 먼 본점까지 가지 않아도 됩니다. 캐시도 비슷해요. 원본 조회나 계산을 생략할 수 있을 때 이득이 생깁니다. 반대로 매번 다른 데이터를 요청하면 캐시를 확인하는 비용만 추가될 수 있습니다.
캐시 조회가 2ms, 원본 조회가 100ms이고 요청의 90%가 캐시에 적중한다고 가정해보죠. 캐시 저장 비용과 대기열을 제외한 평균 조회 시간은 다음과 같습니다.
1
2
3
평균 조회 시간 = 캐시 조회 시간 + 미스 비율 × 원본 조회 시간
= 2ms + 0.1 × 100ms
= 12ms
이 계산은 캐시 적중률과 조회 비용의 관계를 보여줍니다. 실제 응답 시간에는 네트워크 왕복, 직렬화, 원본의 대기열도 들어갑니다. 평균이 빨라져도 캐시 미스 요청은 여전히 느릴 수 있으니 p95·p99 같은 느린 구간도 함께 봐야 해요.
Redis가 빠른 저장소라고 해서 항상 캐시인 것은 아닙니다. 다시 만들 수 없는 데이터를 Redis에만 저장한다면, 그 데이터에 대해서는 원본 저장소의 내구성을 설계해야 합니다.
무엇을 보관할까?
파레토의 법칙은 “일부 데이터에 요청이 몰릴 수 있다”는 가설로 참고할 수 있습니다. 모든 서비스가 정확히 80:20을 따른다는 뜻은 아닙니다. 실제 요청 로그로 다음을 확인하는 편이 좋겠습니다.
| 질문 | 캐시 후보를 고르는 데 주는 정보 |
|---|---|
| 같은 키가 반복해서 조회되는가? | 재사용 가능성 |
| 조회·계산 비용이 큰가? | 적중 한 번으로 절약하는 비용 |
| 값은 얼마나 자주 바뀌는가? | 무효화 비용과 오래된 값의 가능성 |
| 몇 초까지 오래된 값을 허용할 수 있는가? | TTL과 일관성 정책 |
| 사용자·조직마다 결과가 다른가? | 캐시 키와 접근 권한의 경계 |
시간 지역성은 최근 읽은 데이터를 곧 다시 읽는 성질입니다. 인기 게시물을 반복 조회하는 상황을 떠올리면 됩니다. 공간 지역성은 가까운 메모리 주소를 이어서 접근하는 성질이고, CPU 캐시가 캐시 라인으로 데이터를 가져오는 이유를 설명합니다. Redis에 관련 키를 나란히 이름 붙였다고 CPU 캐시와 같은 효과가 생기지는 않아요.
다 저장하면 좋겠지만 메모리는 유한합니다. TTL은 유효기간, eviction은 메모리 한도에 도달했을 때 무엇을 내보낼지 정하는 정책입니다. TTL이 남아 있어도 축출될 수 있고, Redis의 정책에 따라 메모리 한도에서 쓰기가 거절될 수도 있습니다. Redis의 메모리·축출 정책을 데이터 성격에 맞게 정해야 합니다. CACHE IS CASH라는 농담이 괜히 나온 게 아니네요.
캐싱 전략: 읽는 방법과 쓰는 방법을 나눠보자
네 가지 전략을 모두 같은 선택지로 외우면 헷갈립니다. Cache Aside와 Read Through는 주로 읽기 미스 때 누가 값을 채우는가, Write Through와 Write Behind는 쓰기를 원본에 언제 반영하는가에 관한 구분입니다. 읽기와 쓰기 전략을 조합할 수 있어요.
Cache Aside: 애플리케이션이 채운다
- 애플리케이션이 캐시를 조회합니다.
- 값이 있으면 반환합니다.
- 없으면 DB를 조회하고, 결과를 캐시에 넣은 뒤 반환합니다.
요청된 데이터부터 저장하므로 시작하기 쉽습니다. 대신 애플리케이션이 미스 처리와 무효화를 책임집니다. DB 수정 후 해당 캐시를 삭제하는 방식이 흔한 출발점이지만, 두 저장소의 상태를 원자적으로 바꾸는 것은 아닙니다. Microsoft의 Cache-Aside 설명도 일관성이 자동으로 보장되지 않는다고 명시합니다.
Read Through: 캐시 계층이 채운다
애플리케이션은 캐시 계층에 조회를 요청하고, 그 계층이 미스 처리와 원본 로딩을 맡습니다. 차이는 원본에서 값을 가져오는 책임의 위치입니다. Redis에 GET을 보냈다고 Redis가 임의의 DB를 알아서 조회하는 것은 아니므로, 이런 동작을 제공하는 라이브러리나 로더가 필요합니다.
Write Through: 원본 반영까지 기다린다
쓰기 경로에서 캐시와 원본에 반영한 뒤 성공을 반환하는 방식입니다. 쓰기 직후 캐시의 최신성을 유지하기 쉬워지지만, 원본 쓰기 지연이 요청에 포함되고 읽지 않을 값도 캐시에 들어갈 수 있습니다. 한쪽만 성공했을 때의 복구와 캐시를 우회한 DB 수정은 별도 문제예요.
Write Behind: 원본 반영을 뒤로 미룬다
캐시 계층이 쓰기를 받아들인 뒤 나중에 원본으로 전달합니다. 쓰기를 모으거나 같은 키의 변경을 합칠 여지가 있지만, 성공 응답 시점에 원본 DB에는 아직 없을 수 있습니다. 따라서 장애 시 유실 범위, 재시도, 순서 보장, 큐 적체를 함께 설계해야 합니다. 잃어버려도 되는 재생성 가능한 데이터와 결제·재고 데이터는 같은 기준으로 선택할 수 없습니다.
TTL을 넣으면 불일치가 해결될까?
TTL은 오래된 값이 남는 시간을 제한하는 장치이지, 동시 실행을 직렬화하는 장치는 아닙니다. 다음 상황은 DB를 먼저 갱신하고 캐시를 삭제해도 생길 수 있어요.
| 순서 | 조회 요청 A | 수정 요청 B |
|---|---|---|
| 1 | 캐시 미스 후 DB의 이전 값 읽기 | |
| 2 | DB를 새 값으로 갱신 | |
| 3 | 캐시 삭제 | |
| 4 | 앞서 읽은 이전 값을 캐시에 저장 |
이제 캐시에는 다시 이전 값이 들어갔습니다. 이 예시는 두 저장소의 갱신 순서만으로 모든 경쟁 조건을 해결할 수 없다는 뜻입니다.
허용 가능한 지연이 있는 조회라면 TTL과 재시도로 시작할 수 있습니다. 쓰기 직후 반드시 최신 값을 보여줘야 한다면 해당 조회를 원본으로 보내거나, 버전을 비교해 오래된 값의 덮어쓰기를 막는 등 요구 수준에 맞는 방법이 필요합니다. TTL을 갱신할 때마다 연장하거나 오래된 복제본에서 다시 채운다면 “최대 TTL만큼만 오래된다”는 가정도 깨질 수 있습니다.
또 하나는 인기 키가 만료되는 순간 수많은 요청이 DB로 몰리는 문제입니다. 같은 키를 채우는 요청을 합치고, 키별 만료 시각을 분산하고, 원본 호출의 동시 실행 수를 제한하는 방법을 검토할 수 있어요. 프로세스 안에서 요청을 합쳤다고 여러 서버 전체에서 하나만 실행되는 것은 아닙니다.
도입 후 무엇을 확인할까?
저라면 먼저 비용이 큰 읽기 한 종류를 골라 적용하고, 캐시 사용 전후의 응답 시간과 원본 부하를 비교하겠습니다. 적중률만 높다고 성공은 아닙니다.
- 캐시 미스 때의 p95·p99 응답 시간과 DB 쿼리 수
- 키 수, 메모리 사용량, 축출 수, 캐시 오류율
- 수정 직후 오래된 결과를 읽는 비율과 허용 범위
- 캐시가 비거나 장애가 났을 때 원본이 감당할 수 있는 요청량
글을 쓰며 회사에서 사용한 방식에 Cache Aside라는 이름이 있다는 것을 알게 됐습니다. 이제는 이름에서 한 걸음 더 나아가, 무엇을 얼마나 오래 재사용하고 실패하면 어떻게 복구할지 설명할 수 있어야 캐시를 이해했다고 말할 수 있겠습니다.



