본문 바로가기

Computer Science/NetWork

HTTP 캐시 완벽 정리: Cache-Control·ETag·304 동작 원리와 Spring Boot 구현

추천캐릭터 2026. 7. 28. 08:00
728x90

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-MatchIf-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일

728x90