캐시 설계: 오래된 값·무효화·HTTP 헤더를 함께 정하기
가격 정보와 정적 파일의 캐시를 구분하고 갱신 실패와 개인정보 캐싱을 방지합니다.
캐시를 붙일 때 첫 질문은 얼마나 빨라지는지가 아니라 얼마나 오래된 값을 허용할 수 있는지입니다. 글 목록과 결제 직전 가격은 다른 기준을 가져야 합니다. 이 글에서는 애플리케이션 캐시와 HTTP 캐시를 구분해 갱신 규칙을 정합니다.
데이터별 허용 지연 정하기
| 데이터 | 설계 질문 | 갱신 전략 예시 |
|---|---|---|
| 공개 글 목록 | 몇 분 전 목록도 쓸 수 있는가? | 짧은 만료와 발행 시 무효화 |
| 해시가 붙은 CSS | 내용이 바뀌면 URL도 바뀌는가? | 긴 만료와 파일명 변경 |
| 로그인 사용자 정보 | 다른 사용자에게 노출될 위험은? | 공유 캐시 제외 검토 |
| 주문 최종 금액 | 오래된 값으로 결제해도 되는가? | 권위 있는 저장소에서 재확인 |
표는 설계 예시입니다. TTL을 일률적으로 정하기보다 잘못된 값을 읽었을 때의 결과를 먼저 적습니다.
캐시 무효화가 실패하는 순서
DB를 갱신한 뒤 캐시를 지웠다고 모든 경쟁 조건이 사라지지는 않습니다. 다른 요청이 이전 DB 값을 읽고 나중에 캐시에 다시 저장하면 오래된 값이 되살아날 수 있습니다. 쓰기 빈도와 허용 지연에 따라 버전 키, 짧은 TTL, 단일 갱신 경로 등을 검토합니다.
한 키가 만료될 때 요청이 동시에 DB로 몰리는 현상도 고려합니다. 갱신 요청 합치기나 만료 시간 분산이 후보가 될 수 있지만, 오류 상태를 영구히 캐싱하거나 권한이 다른 데이터를 섞지 않도록 조건을 분리합니다.
HTTP 헤더를 구분해서 사용하기
no-cache는 저장 자체를 금지한다는 뜻이 아니라 재사용 전 검증을 요구하는 의미입니다. 저장하지 않게 하려면 no-store를 검토합니다. private은 공유 캐시에 대한 범위를 제한합니다. 아래는 민감한 응답에 대한 설정 형태의 예제이며 실제 정책은 응답의 성격에 맞춰 정합니다.
Cache-Control: private, no-store
공개 정적 파일에 긴 만료를 쓸 때는 내용이 바뀌면 URL도 바뀌게 합니다. ETag로 재검증하면 변경이 없을 때 본문 전송을 줄일 수 있지만 검증 요청 자체가 없어지는 것은 아닙니다. CDN과 브라우저의 동작을 각각 확인합니다.
확인할 시나리오
처음 접근, 캐시 적중, 만료, 원본 갱신, 원본 장애, 로그인 사용자 변경을 나누어 검사합니다. 캐시 적중률만 높고 갱신된 글이 오래 안 보인다면 성공적인 최적화로 보기 어렵습니다.
수정 전후 응답 헤더와 본문 버전을 함께 기록하세요. DB 조회가 원래 비효율적이라면 캐시로 가리기 전에 쿼리 실행 계획도 확인합니다.
갱신과 읽기가 엇갈리는 타임라인
원본 DB 값이 v1이고 캐시가 비어 있다고 가정합니다. 다음 순서에서는 ‘쓰기 뒤 캐시 삭제’만으로 최신 값이 유지되지 않습니다.
| 순서 | 읽기 요청 A | 쓰기 요청 B |
|---|---|---|
| 1 | 캐시 미스 후 DB의 v1을 읽음 | 대기 |
| 2 | 응답이 지연됨 | DB를 v2로 갱신 |
| 3 | 아직 캐시에 쓰지 않음 | 기존 캐시 키 삭제 |
| 4 | 먼저 읽었던 v1을 캐시에 저장 | 완료 |
이후 요청은 캐시에서 v1을 읽을 수 있습니다. 짧은 TTL은 오래된 값이 남는 기간을 제한하지만 경쟁 조건 자체를 없애지는 않습니다. 최신성이 중요한 쓰기 경로는 원본을 확인하고, 다른 경로에서는 버전과 무효화 방식의 복잡도를 감수할지 결정합니다.
검사할 때는 ‘캐시 적중’ 한 경우뿐 아니라 위 순서를 의도적으로 지연시켜 재현해야 합니다. 갱신 작업을 잠금으로 보호한다면 잠금이 실제로 같은 키의 모든 작성자에게 적용되는지, 실패 시 만료되는지도 확인해야 합니다. 특정 캐시 제품을 선택한 것만으로 이 순서 문제가 해결되지는 않습니다.