웹 서비스 운영에서 가장 중요한 목표 중 하나는 빠른 응답 속도와 서버 부하 감소다. 모든 요청을 서버까지 보내면 네트워크 지연(latency)과 서버 과부하가 필연적으로 발생한다. 이를 해결하는 핵심 전략이 바로 캐시(Cache)다. 캐시는 자주 사용되는 리소스를 중간 저장소에 보관하여 다시 요청이 왔을 때 서버까지 가지 않고 빠르게 응답할 수 있게 해준다. 단순 성능 최적화를 넘어서 대역폭 절약, 서버 비용 절감, 안정성 향상까지 기대할 수 있다.
캐시는 서버에서 내려준 응답을 일정 시간 동안 재사용할 수 있도록 저장해두는 임시 저장소이다. 브라우저, 프락시 서버, CDN, 애플리케이션 서버 등 다양한 계층에서 존재할 수 있으며 동일한 요청이 다시 들어왔을 때 서버에 가지 않고 빠르게 응답할 수 있게 해준다. 결국 캐시는 “자주 쓰이는 것을 가까운 곳에 두자”라는 단순한 아이디어지만 대규모 서비스의 성능과 비용을 결정짓는 핵심 요소다.
성능 향상 (Latency 감소)
캐시는 네트워크 왕복을 줄여주기 때문에 사용자는 더 빠른 응답 속도를 체감한다. 예를 들어, 같은 이미지를 10번 불러와야 한다면 서버가 아니라 브라우저 캐시에서 바로 가져오는 것이 훨씬 빠르다.
네트워크 트래픽 감소
동일한 데이터를 여러 번 내려줄 필요가 없으니 트래픽 사용량을 크게 줄일 수 있다. 이는 특히 대규모 서비스에서 CDN과 함께 사용할 때 효과가 크다.
서버 부하 감소
서버가 동일한 요청을 매번 처리할 필요가 없어지고 CPU와 DB 자원을 더 중요한 요청 처리에 사용할 수 있다. 결과적으로 더 많은 트래픽을 감당할 수 있다.
오프라인 접근성
서버와 연결이 끊겨도 브라우저 캐시에 남아있는 정적 자원을 기반으로 제한된 범위에서 애플리케이션이 동작할 수 있다. PWA(Progressive Web App)에서 오프라인 기능을 제공하는 것도 이 원리다.
캐시된 데이터는 무한정 쓸 수 없다. 신선도(Freshness)가 지나면 서버와 동기화해야 한다. HTTP는 이를 위해 만료(Expiration)와 재검사(Revalidation) 메커니즘을 제공한다.
서버가 응답 헤더에 캐시 유효 시간을 명시하면 캐시는 그 시간 동안 서버에 묻지 않고 데이터를 그대로 사용한다. 예를 들어, max-age=600 이라면 응답은 10분 동안 신선한 것으로 간주된다. 만료 시점 전에는 서버로 요청이 가지 않으므로 속도와 트래픽 측면에서 큰 이점이 있다.
Cache-Control: max-age=600 # 10분 동안은 캐시 사용 가능
Expires: Wed, 21 Oct 2025 07:28:00 GMT
만료된 캐시를 그냥 버리기보다는 서버에 변경 여부를 확인할 수 있다. 클라이언트는 If-None-Match나 If-Modified-Since 조건부 요청을 보내고 서버가 “변경 없음”을 응답하면(304 Not Modified) 캐시를 재사용한다. 이 방식은 트래픽을 최소화하면서 데이터의 최신성도 보장할 수 있다.
# 클라이언트 요청
GET /app.js HTTP/1.1
If-None-Match: "v2.4"
# 서버 응답 (변경 없음)
HTTP/1.1 304 Not Modified
| 헤더 | 설명 |
|---|---|
| Cache-Control | 캐시 동작 전반 제어 (max-age, no-cache, no-store, public/private 등) |
| Expires | 절대 만료 시각 (HTTP/1.0, 비추천) |
| ETag | 리소스 버전 식별자 (해시 값) |
| Last-Modified | 리소스 최종 수정 시각 |
| If-None-Match | 클라이언트가 가진 ETag와 비교 |
| If-Modified-Since | 클라이언트가 가진 최종 수정일과 비교 |
Cache-Control: no-cache
캐시된 사본이 있더라도 서버 검증 후에만 사용한다.
Cache-Control: no-store
아예 저장하지 않는다. 로그인, 결제 정보 같은 민감한 데이터에 사용.
Pragma: no-cache
HTTP/1.0 호환을 위한 옛날 방식.
캐시 무효화는 “컴퓨터 과학에서 가장 어려운 문제 중 하나”라는 말이 있을 정도로 까다롭다. 캐시를 언제, 어떻게 폐기할지는 서비스 특성에 따라 다르고 너무 빨리 폐기하면 성능이 떨어지고 너무 늦게 폐기하면 최신성이 깨진다. 운영자는 서비스 도메인에 맞는 무효화 정책을 반드시 고민해야 한다.
캐시의 효과는 적중률(hit ratio)로 측정할 수 있다. 전체 요청 중 캐시에서 바로 응답한 비율을 말하며 보통 80~90% 이상이 되면 매우 효율적이라고 본다. 적중률이 낮으면 캐시를 둔 의미가 줄어들고 불필요하게 복잡성만 증가할 수 있다. 따라서 운영자는 캐시 정책을 설계할 때 적중률을 꾸준히 모니터링해야 한다.
클라이언트 캐시
브라우저 로컬에 저장되는 형태로 사용자가 같은 페이지를 다시 열 때 바로 활용된다.
프락시 캐시
네트워크 중간(프락시 서버, CDN)에 존재하는 캐시로 여러 사용자가 동일 자원을 요청할 때 효과적이다. 즉, 클라이언트 캐시는 개인 단위 최적화, 프락시 캐시는 다수 사용자에게 공통 리소스를 제공할 때 최적화한다.
또한 HTTP/1.1의 Cache-Control은 public과 private 지시어를 지원한다.
public-private
특정 사용자만 사용하는 개인 데이터로 브라우저 캐시에만 저장 가능하다.
→ 예를 들어, 사용자별 장바구니 API는 private, 공개 이미지 리소스는 public 캐싱이 적합하다.
캐시는 한 계층에서만 존재하지 않고 여러 단계로 겹칠 수 있다. 예를 들어, 브라우저 캐시 → 로컬 프락시 → CDN 캐시 → 원 서버 순서로 계층화된 캐시 구조가 흔하다. 이런 계층 구조 덕분에 사용자는 지리적으로 가장 가까운 캐시에서 빠르게 응답을 받을 수 있고 전 세계 사용자에게 동일한 경험을 제공할 수 있다.
Spring Boot와 Nginx를 함께 운영할 때 정적 리소스(JS, CSS, 이미지)에는 일반적으로 해시 기반 버전 관리를 붙인다.
예: app.3fa4c1.js → 새 배포 시 파일명이 달라져 브라우저는 무조건 새로운 파일을 받는다.
API 응답에서는 Cache-Control: no-store를 붙여 캐싱되지 않도록 하거나 상품 목록처럼 변경 주기가 긴 데이터에는 ETag를 활용해 효율적으로 응답을 줄 수 있다. 즉, 정적 리소스는 장기 캐싱, 동적 데이터는 짧은 캐싱 또는 조건부 요청이라는 전략을 상황에 맞게 적용하는 것이 중요하다.
이번 장을 통해 캐시는 단순히 성능 최적화를 위한 보조 수단이 아니라 네트워크 비용 절감과 서버 안정성 확보까지 책임지는 핵심 인프라 요소라는 점을 알게 되었다. HTTP가 제공하는 만료(Expiration)와 재검사(Revalidation) 메커니즘은 최신 데이터 보장과 성능 최적화 사이에서 균형을 맞추는 훌륭한 장치라는 것도 이해할 수 있었다. 특히 ETag와 조건부 요청을 활용해 서버가 304 Not Modified만 내려주는 방식은 불필요한 데이터 전송을 줄이고 응답 속도와 트래픽 절감을 동시에 달성할 수 있다는 점이 인상 깊었다. 또한 책에서 강조하는 캐시 적중률, 프락시 캐시, 캐시 계층 구조 개념을 통해 캐시가 단순히 브라우저에 머무는 기술이 아니라 글로벌 서비스 품질을 결정짓는 핵심 요소라는 사실도 배웠다. 나아가 캐시 무효화 문제의 복잡성과 public/private 캐시 정책을 통해 캐시가 단순 기술이 아니라 서비스 도메인과 데이터 성격에 맞게 전략적으로 설계해야 하는 부분임을 깨달았다. 앞으로 Spring Boot 기반 API를 설계할 때는 단순히 응답 데이터를 내려주는 것에 그치지 않고 Cache-Control, ETag, max-age와 같은 캐시 정책을 적극적으로 활용하여 서비스의 성능과 운영 효율성을 함께 고려해야겠다고 느꼈다.