gony-dev 님의 블로그

[Cache] Cache-Control Header 본문

Caching

[Cache] Cache-Control Header

minarinamu 2025. 7. 24. 16:37

Overview

일전에 다루었던 Redis는 캐싱 기능을 위해 사용하는 In-memory 데이터 저장소이다.

우리는 자주 사용되는 데이터를 캐싱하여, 이후 발생하는 요청 시에 데이터베이스에 직접 접근하지 않고 데이터를 제공할 수 있다.

Redis 같은 외부 라이브러리가 아닌 스프링 프로젝트 내부에서 헤더를 이용하여 캐싱을 하는 방법도 있다.

이에 Cache-Control Header에 대해 알아보자.


1. Cache-Control Header

개요

  1. 설명하기에 앞서 클라이언트-서버의 요청 플로우에 대해 알아보자.
    클라이언트는 서버와 HTTP를 통해 통신을 하여 데이터를 가져오고, 이를 사용자에게 제공한다. 이때 클라이언트는 네트워크를 거치는 시간, 서버는 요청을 처리하는데 시간이 걸린다.
  2. 만일 클라이언트가 요청한 데이터가 이전에 요청한 데이터와 같다면 어떨까?
    당연하게도 이는 서버의 불필요한 자원 낭비로 이어진다.
  3. 이때 Cache-Control Header를 사용하여 문제를 해결할 수 있다.
    그렇게 되면 클라이언트는 네트워크를 거치는 시간을 줄일 수 있고, 서버는 데이터베이스에 직접 접근해야하는 수고를 덜 수 있다.

클라이언트가 요청하면 위와 같이 응답 헤더에 Cache-Control이 반환된다!

헤더 종류

이제 헤더에 캐싱을 적용하기 위해 사용하는 헤더 종류에 대해 알아보자!

  1. Cache-Control Header
    • Cache-Control Header의 max-age 값은 자원이 캐싱될 경우, 유효한 시간을 초 단위로 나타낸다.
    • 이를 통해 클라이언트에 얼마나 길게 캐싱 데이터를 제공할 것인지 설정할 수 있다.
  2. Last-Modified Header
    • 캐시된 자원이 만료되더라도 값의 수정이 필요치 않아 이루어지지 않은 경우가 있다!
      이때는 새로 캐싱할 필요가 없고, Last-Modified Header를 통해 자원이 언제 마지막으로 수정되었는지 확인할 수 있다.
    • 만일 클라이언트의 Last-Modified Header의 값이 서버가 가진 자원의 값과 일치한다면,
      304(Not Modified) 상태코드와 함께 헤더만 요청을 전송한다.
    • 결국 캐시 메모리에 있는 헤더에 갱신이 되고, 클라이언트는 캐시 메모리의 데이터값을 계속 사용할 수 있다.
  3. ETag Header
    • Last-Modified Header는 시간의 형식으로 정보를 저장하지만, 1초 이하의 단위로는 캐시 관리를 하지 못한다.
    • ETag는 자원 고유의 식별자이며, 자원의 내용이 바뀌면 ETag도 변경이 된다.
    • 클라이언트가 요청을 보낼 때 캐시에 ETag 값이 있다면, 해당 값을 "If-None-Match Header"에 포함하여 전달한다.
      이때, 이 값이 서버가 가진 자원의 ETag의 값과 일치한다면 304 상태코드와 함께 헤더만 요청을 전송한다.

 


2. Cache-Control Header 동작 과정

Cache-Control Header의 동작 방식을 다음과 같다.
1. 서버가 응답에 Cache-Control 헤더를 설정하여 클라이언트에게 캐시 정책을 전달한다.
2. 클라이언트는 이 정책에 따라 캐시 유효성을 판단한다.
3. 이후에 서버에 들어오는 요청의 경우, 캐시가 유효하다면 캐시를 사용하며
캐시가 만료되었다면 재요청을 수행한다.

 

 


3. Spring Boot 적용 코드

Code

PostService
public Post getPostById(Long id) {

    return postRepository.findById(id).orElseThrow(RuntimeException::new);
}​

PostController
    @GetMapping("/{id}")
    public ResponseEntity<Post> getPostById(
            @PathVariable Long id,
            @RequestHeader(value = "If-Modified-Since", required = false) String ifModifiedSince,
            @RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) {

        Post post = postService.getPostById(id);

        // ETag 생성
        String eTag = "\"" + post.hashCode() + "\"";

        // updatedAt 기준 Last-Modified 시간
        Instant lastModified = post.getUpdatedAt().toInstant(ZoneOffset.UTC);

        boolean notModified = false;

        // ETag 비교
        if (ifNoneMatch != null && ifNoneMatch.equals(eTag)) {
            notModified = true;
        }

        // Last-Modified 비교 (If-Modified-Since 헤더가 있는 경우만)
        if (!notModified && ifModifiedSince != null) {
            try {
                ZonedDateTime clientDate = ZonedDateTime.parse(ifModifiedSince, DateTimeFormatter.RFC_1123_DATE_TIME);
                Instant clientInstant = clientDate.toInstant();
                if (clientInstant.equals(lastModified)) {
                    notModified = true;
                }
            } catch (DateTimeParseException e) {
                // 잘못된 날짜 포맷 → 무시 (304 처리하지 않고 전체 본문 내려줌)
            }
        }

        if (notModified) {
            return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
                    .eTag(eTag)
                    .lastModified(lastModified.toEpochMilli())
                    .build();
        }

        return ResponseEntity.ok()
                .cacheControl(CacheControl.maxAge(600, TimeUnit.SECONDS).cachePublic())
                .eTag(eTag)
                .lastModified(lastModified.toEpochMilli())
                .body(post);
    }

 

Result

Postman을 통해 결과를 알아보자!

(사전에 post1과 2가 저장되어 있다고 가정한다.)

첫번째 요청 시 

두 번째 요청 시
두번째에는 "If-None-Match" 또는 "If-Modified-Since"를 넣어보자.
위의 결과처럼 304 상태 코드를 전해주고 캐싱되는 것을 확인할 수 있다!

 


결론

Cache-Control Header를 통해서 알아본 바는 다음과 같다.

1. max-age로 유효시간을 설정할 수 있다.

2. if-Modified-Since를 통해 수정 여부를 확인하고 갱신할 수 있다.

3. if-None-Match를 통해 원하는 자원을 가져왔는지 확인할 수 있다.

이렇게 캐싱을 통해 우리는 서버 사용량을 줄여 불필요한 자원 낭비를 하지 않을 수 있다!

'Caching' 카테고리의 다른 글

[Cache] Spring Boot Cache  (2) 2025.08.01
[Redis] Connection Mode-2  (0) 2024.12.08
[Redis] Connection Mode-1  (0) 2024.12.06
[Redis] Spring batch vs. Scheduler  (0) 2024.10.17
[Redis] Transaction  (1) 2024.10.15