대부분의 팀이 HTTP 캐싱을 “붙이면 빨라지는 옵션” 정도로 생각합니다. 실제 운영에서는 반대입니다. 캐싱은 성능 기능이 아니라 일관성·비용·장애 반경을 동시에 건드리는 아키텍처 정책입니다. 5ms 빨라지는 것보다 더 중요한 건, 어떤 데이터는 30초까지 오래 보여도 되는지, 어떤 데이터는 1초만 늦어도 사고인지 합의하는 일입니다.
이 글은 CDN, 브라우저, API 서버가 각각 어떤 책임을 가져야 하는지, 그리고 ETag/재검증을 어디까지 자동화해야 하는지 실무 숫자 기준으로 정리합니다.
이 글에서 얻는 것
Cache-Control,ETag,Last-Modified를 문법이 아니라 의사결정 도구로 쓰는 기준을 가져갈 수 있습니다.- 캐시 적중률(히트율)만 보는 단순 지표에서 벗어나, 신선도 위반률·원본 부하·재검증 비용까지 함께 보는 운영 관점을 잡을 수 있습니다.
- 엔드포인트별로 캐시 전략을 구분하고, 장애 시 안전하게 강등(degrade)하는 **실무 우선순위(숫자·조건)**를 바로 적용할 수 있습니다.
핵심 개념/이슈
1) 캐시 정책은 URL 단위가 아니라 “데이터 위험도” 단위로 설계해야 한다
/api/products/{id} 같은 경로 기준으로 정책을 통일하면 거의 항상 실패합니다. 같은 경로여도 데이터의 위험도는 다릅니다.
- 재고/가격/주문 상태: 신선도 민감(짧은 TTL 또는 재검증 강제)
- 상품 설명/이미지 메타: 지연 허용(긴 TTL + stale 허용)
- 공지/정책 문서: 매우 긴 캐시(버전 파일 + immutable)
실무에서는 먼저 “허용 가능한 오래됨(staleness)“을 숫자로 잡고, 그 다음 캐시 전략을 매핑합니다.
- Tier A(강한 신선도): 허용 오래됨 1~3초
- Tier B(보통): 10~60초
- Tier C(정적/준정적): 1시간~7일
이 분류는 SLO/SLI/Error Budget과 같이 정의해야 조직 합의가 됩니다.
2) Cache-Control은 “성능 옵션"이 아니라 “계약서"다
자주 보이는 실수는 max-age=600만 넣고 끝내는 것입니다. 운영에서 필요한 건 캐시 생명주기 전체 계약입니다.
max-age: 사용자/엣지에서 신선하게 간주하는 시간s-maxage: CDN(shared cache) 전용 정책stale-while-revalidate: 백그라운드 재검증 허용 구간stale-if-error: 원본 장애 시 낡은 응답 임시 제공 구간must-revalidate: 만료 후 재사용 제한
권장 시작점(조회 API 기준):
- Tier A:
Cache-Control: private, max-age=0, must-revalidate - Tier B:
Cache-Control: public, max-age=30, s-maxage=60, stale-while-revalidate=30 - Tier C:
Cache-Control: public, max-age=3600, s-maxage=86400, immutable
핵심은 “모든 API 캐시"가 아니라, 사용자 영향과 장애 비용을 같이 보고 정책을 나누는 것입니다. API Rate Limit/Backpressure와 결합하면 원본 보호 효과가 커집니다.
3) ETag/Last-Modified는 트래픽 절감 장치이면서 동시성 보호 장치다
ETag를 단순 304 최적화로만 보면 절반만 본 겁니다. 실무에서는 두 가지 역할이 중요합니다.
- 읽기 재검증 비용 절감: 바뀌지 않은 응답은 본문 없이 304 반환
- 쓰기 충돌 방지:
If-Match로 낙관적 동시성 제어
운영 규칙 예시:
- 엔티티 버전이 있는 리소스: 강한 ETag(
"v42") 우선 - 집계/동적 조합 응답: 약한 ETag(
W/"...") 허용 - 수정 API(PUT/PATCH):
If-Match없는 요청은 428 또는 412 처리
즉, ETag는 성능과 정합성을 동시에 잡는 도구입니다. 이 부분은 Idempotency 설계나 DB 락/격리수준과 함께 설계해야 효과가 납니다.
4) 캐시 적중률이 높아도 서비스 품질은 나쁠 수 있다
히트율 95%인데 불만이 많은 서비스가 흔합니다. 이유는 “틀린 데이터를 빠르게 전달"하기 때문입니다. 그래서 최소 지표 세트가 필요합니다.
cache_hit_ratio(엔드포인트/테넌트별)revalidation_304_ratiostale_served_ratiofreshness_violation_rate(SLO 초과 오래됨)origin_qps_reduction
즉 성능 지표 + 정확성 지표를 같이 봐야 합니다. 관측 설계는 Observability Baseline과 알람 전략 기준으로 묶는 편이 안전합니다.
실무 적용
1) 엔드포인트 분류와 기본 정책(숫자·조건·우선순위)
우선순위는 정확성 > 원본 보호 > 평균 응답속도로 둡니다.
Tier A(결제/재고/주문 상태)
- 기본:
max-age=0, must-revalidate - 조건: 신선도 위반률 목표 0.1% 미만
- CDN 캐시: 필요 시 키 좁게(사용자/권한 포함)
- 기본:
Tier B(카탈로그/검색 결과 일부)
- 기본:
max-age=15~60s,stale-while-revalidate=30~120s - 조건: 원본 QPS 30% 이상 절감 목표
- 경고: stale 제공 비율이 20% 넘으면 TTL 재조정
- 기본:
Tier C(문서/정적 메타)
- 기본: 버전 해시 URL +
immutable - 조건: 배포 시 캐시 무효화 비용 최소화
- 기본: 버전 해시 URL +
2) 3단계 롤아웃 플랜
1주차: 측정 고정
- 주요 20개 API의 현재 헤더/TTL/히트율/오래됨 위반률 수집
- 캐시 키 설계 문서화(권한·언어·테넌트 분리 포함)
2주차: 정책 적용
- Tier B/C부터
s-maxage,stale-while-revalidate적용 - ETag 생성 로직 통일(버전 필드 또는 콘텐츠 해시)
- 수정 API에
If-Match검증 추가
3주차: 보호 자동화
- 원본 장애율 상승 시
stale-if-error임시 확장(예: 60→300초) - 신선도 위반률 초과 시 자동으로 TTL 축소
- 대시보드에 “원본 보호 효과 vs 오래됨 비용” 비교 차트 추가
3) 의사결정 게이트 예시
- 10분 이동창에서
freshness_violation_rate > 0.5%면 즉시 TTL 50% 축소 origin_cpu > 75%+tierB hit_ratio < 70%면 캐시 키 과분할 여부 점검stale_served_ratio > 30%30분 지속 시 revalidation 병목(ETag 계산, 조건부 GET) 우선 점검
트레이드오프/주의점
TTL을 짧게 줄이면 정확성은 좋아지지만 원본 비용이 급증한다
특히 트래픽 피크 시간에 재검증 폭증이 생기면 DB/앱 서버가 먼저 포화됩니다.stale-while-revalidate는 UX를 살리지만 오류를 가릴 수 있다
장애를 숨겨 평균 응답속도는 좋아 보여도, 실제 데이터 지연은 커질 수 있습니다.ETag 계산이 무거우면 최적화가 역효과가 날 수 있다
대형 응답의 전체 해시를 매번 계산하면 CPU를 더 씁니다. 버전 기반 ETag가 가능한 구조를 우선 고려하세요.캐시 키 설계가 느슨하면 데이터 누출 사고로 이어질 수 있다
권한/테넌트/언어 분리가 빠지면 성능 문제가 아니라 보안 사고가 됩니다.
체크리스트 또는 연습
체크리스트
- 엔드포인트를 데이터 위험도(Tier A/B/C)로 분류했다.
- 각 Tier별 허용 오래됨(초)을 문서로 합의했다.
-
Cache-Control정책에s-maxage,stale-*사용 여부가 명확하다. - ETag/If-Match를 읽기·쓰기 경로 모두에서 검토했다.
- 히트율 외에 신선도 위반률/원본 보호 지표를 함께 모니터링한다.
연습 과제
- 현재 운영 중인 조회 API 5개를 골라 Tier를 분류하고,
max-age/s-maxage/stale-*초깃값을 제안해 보세요. - 수정 API 1개를 골라
If-Match기반 충돌 방지를 설계하고, 412 응답 시 클라이언트 재시도 UX를 정의해 보세요. - “원본 CPU 80% 초과 시 stale-if-error를 5분까지 확장” 같은 긴급 정책을 만들고, 해제 조건(예: CPU 60% 10분 유지)을 숫자로 정해보세요.
💬 댓글