HTTP 캐시 완벽 정리: Cache-Control·ETag·304 동작 원리와 Spring Boot 구현
게시글 상세나 공지처럼 변경은 적고 조회가 많은 API가 매번 같은 JSON을 보낸다면 네트워크와 서버 자원을 낭비합니다. HTTP 캐시는 Cache-Control로 재사용 규칙을 알리고, ETag로 변경을 검증해 필요하면 본문 없는 304 Not Modified를 반환하는 표준 기능입니다.
HTTP 캐시란
HTTP 캐시는 이전 응답을 브라우저나 CDN에 보관했다가 같은 요청에 재사용합니다.
| 구분 | 위치 | 대표 예시 |
|---|---|---|
| Private Cache | 한 사용자에게만 사용하는 캐시 | 브라우저 캐시 |
| Shared Cache | 여러 사용자가 함께 사용하는 캐시 | CDN, 리버스 프록시 |
Cache-Control 핵심 지시어
Cache-Control은 응답을 저장하고 재사용하는 규칙을 전달합니다.
| 지시어 | 의미 | 사용 예 |
|---|---|---|
max-age=300 |
300초 동안 새 응답으로 간주 | 자주 조회되는 공개 데이터 |
private |
브라우저 같은 개인 캐시에만 저장 | 사용자별 응답 |
public |
CDN 같은 공유 캐시에도 저장 가능 | 공개 이미지·공지 |
no-cache |
저장은 가능하지만 재사용 전에 서버 검증 | 변경 여부를 반드시 확인할 데이터 |
no-store |
응답을 저장하지 않음 | 결제·민감 개인정보 |
must-revalidate |
만료 후 서버 검증 필수 | 정합성이 중요한 조회 |
no-cache는 저장 금지가 아니라 사용 전 검증 의무이며, 저장 자체를 막는 지시어는 no-store입니다.
ETag와 304의 동작 과정
ETag는 현재 표현의 버전 식별자입니다. DB 버전처럼 내용이 바뀔 때 함께 바뀌는 값을 사용합니다.
첫 요청은 다음처럼 처리됩니다.
GET /api/posts/10
HTTP/1.1 200 OK
Cache-Control: private, max-age=300, must-revalidate
ETag: "10-7"
Content-Type: application/json
{"id":10,"title":"HTTP 캐시 정리"}
캐시가 만료된 뒤 브라우저는 보관한 ETag를 If-None-Match에 넣어 다시 확인합니다.
GET /api/posts/10
If-None-Match: "10-7"
서버의 현재 ETag도 "10-7"이면 내용이 바뀌지 않았다는 뜻입니다.
HTTP/1.1 304 Not Modified
ETag: "10-7"
Spring Boot 구현 예제
Spring MVC의 WebRequest.checkNotModified()를 사용하면 If-None-Match와 If-Modified-Since 검사를 직접 작성하지 않아도 됩니다.
@RestController
@RequestMapping("/api/posts")
public class PostController {
private final PostService postService;
public PostController(PostService postService) {
this.postService = postService;
}
@GetMapping("/{id}")
public ResponseEntity<PostResponse> getPost(
@PathVariable Long id,
WebRequest request) {
Post post = postService.getPost(id);
String etagValue = post.getId() + "-" + post.getVersion();
String quotedEtag = "\"" + etagValue + "\"";
long lastModified = post.getUpdatedAt()
.toInstant()
.toEpochMilli();
if (request.checkNotModified(etagValue, lastModified)) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.eTag(quotedEtag)
.build();
}
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofMinutes(5))
.cachePrivate()
.mustRevalidate())
.eTag(quotedEtag)
.lastModified(lastModified)
.body(PostResponse.from(post));
}
}
엔티티의 version이 바뀔 때 ETag도 바뀐다고 가정한 예제입니다. 큰 응답 전체를 매번 해시하지 말고 DB 버전 같은 안정적인 식별자를 사용합니다.
언제 어떤 정책을 사용할까
| 응답 종류 | 권장 시작점 |
|---|---|
| 공개 게시글·공지 | 짧은 max-age와 ETag |
| 로그인 사용자 프로필 | private와 ETag 또는 no-cache |
| 결제 결과·인증 정보 | no-store |
| 실시간 재고·잔액 | 짧은 캐시 또는 캐시하지 않음 |
실무에서 자주 하는 실수
- 모든 GET에 긴
max-age를 적용해 가격이나 권한 정보가 오래 남습니다. - 인증 응답을
public으로 표시해 CDN에서 사용자 데이터가 공유됩니다. - ETag를 무작위 UUID로 만들어 매번 달라지므로 304가 발생하지 않습니다.
- 304를 오류로 기록합니다. 304는 캐시 검증이 성공한 정상 흐름입니다.
- Redis와 HTTP 캐시를 혼동합니다. 전자는 서버 계산, 후자는 요청·응답 비용을 줄입니다.
핵심 요약
Cache-Control은 응답의 저장 위치와 재사용 기간을 지정합니다.no-cache는 저장 금지가 아니라 재검증이고,no-store가 저장 금지입니다.- ETag가 같으면 서버는 본문 없는 304를 반환할 수 있습니다.
- 사용자별 응답은
private, 민감 정보는no-store를 검토합니다. - ETag는 내용이 바뀔 때만 변경되는 값으로 만듭니다.
마무리
조회가 많고 변경이 적은 API부터 적용해 304 비율을 측정해 보세요.
관련 글: REST API 완벽 정리, Spring Boot API 응답 설계, CORS 동작 원리
공식 자료:
최종 확인: 2026년 7월 27일
'Computer Science > NetWork' 카테고리의 다른 글
| WebSocket vs SSE, 실시간 통신 방식 선택 기준 (0) | 2026.07.21 |
|---|---|
| 502 Bad Gateway가 떴다: Nginx가 범인인 척했지만 진범은 Spring Boot였다 (0) | 2026.07.19 |
| 오픈소스 공급망 공격, 2026년 개발자 대응 (0) | 2026.07.17 |
| HTTP/3 vs HTTP/2, QUIC 도입 기준 정리 (0) | 2026.07.15 |
| CORS 동작 원리와 Spring Boot 설정 방법 (0) | 2026.07.03 |