본문 바로가기
아직 카테고리 미정

HTTP Caching에 대해서 좀 이해해보자!!!

by simplify-len 2020. 11. 14.

이번에 HTTP Code 중 304 에러 코드에 대해서 이해하는 과정에서 WebCache에 대한 정보를 일부 활용했다.

304 에러 코드는 웹 캐시에 저장된 데이터가 같은 데이터를 Get하려고 한다면, 이를 웹 브라우저가 변경되지 않은 데이터라는 것을 인지한다.

그럼 WebCache에 대해서 한꺼풀씩 이해해봅시다.

HTTP Caching이 뭘까?

우리는 일반적으로 웹서버에 정적 파일을 다운로드 받습니다. 예를 들어, '/login' page 라고 했을 때, 로그인 페이지에는 css, js, html 등의 파일을 가져와야 합니다. 

 이때, http, css, js 등 여러 HTTP요청을 만드는 것이 일반적입니다. 그러나, 이제 이러한 페이지를 매우 자주 요청하면 네트워크 트래픽이 많이 발생하고 이러한 페이지를 제공하는 데 시간이 더 오래 걸릴것입니다.

네트워크 부하를 줄이기 위해 HTTP 프로토콜을 사용하면 브라우저가 이러한 페이지 일부를 캐시 할 수 있습니다. 활성화된 경우 브라우저는 로컬 캐시에 리소스 사본을 저장할 수 있습니다. 결과적으로, 브라우저는 네트워크를 통해 요청하는 대신 로컬 저장소에서 리소스 사본를 제공합니다.

어떻게 Cache 할 수 있는거지?

웹 서버 Response에 Cache-Control 헤더를 추가하여 특정 리소스를 캐시하도록 브라우저에게 지시할 수 있습니다.

리소스가 로컬 사본으로 캐시되기 때문에 브라우저에서 오래된 컨텐츠를 제공 할 위험이 있습니다. 따라서 웹 서버는 일반적으로 Cache-Control 헤더에 만료시간을 추가합니다.

@GetMapping("/hello/{name}")
@ResponseBody
public ResponseEntity<String> hello(@PathVariable String name) {
    CacheControl cacheControl = CacheControl.maxAge(60, TimeUnit.SECONDS)
      .noTransform()
      .mustRevalidate();
    return ResponseEntity.ok()
      .cacheControl(cacheControl)
      .body("Hello " + name);
}

CacheControl 빌더 클래스를 사용해 브라우저에게 "이거 캐시하고, 1분지나면 캐시를 재갱신해!"라고 요청합니다.

그러면 응답 코드에 "Cache-Control","max-age=60, must-revalidate, no-transform" 코드가 추가됩니다.

그럼 스프링 부트 코드에서 캐시 유효성을 검사해보자.

max-age 와 같은 구성 속성을 기반으로 리소스를 캐시 할 클라이언트 또는 브라우저을 활용해 캐시가 동작됨을 이해해봅시다.

일반적으로 각 리소스에 캐시 만료 시간을 추가하는 것이 좋습니다 . 결과적으로 브라우저는 캐시에서 만료 된 리소스를 제공하지 않을 수 있습니다.

브라우저는 항상 만료 여부를 확인해야하지만 매번 리소스를 다시 가져올 필요는 없습니다. 브라우저가 서버에서 '리소스가 변경되지 않았음'을 확인할 수있는 경우 캐시 된 버전을 계속 제공 할 수 있습니다. 이를 위해 HTTP는 두 가지 응답 헤더를 제공합니다.

  1. Etag – 서버에서 캐시 된 리소스가 변경되었는지 여부를 확인하기 위해 고유 한 해시 값을 저장하는 HTTP 응답 헤더 – 해당 If-None-Match 요청 헤더는 마지막 Etag 값을 전달해야합니다.
  2. LastModified – 리소스가 마지막으로 업데이트 된 시간 단위를 저장하는 HTTP 응답 헤더 – 해당 If-Unmodified-Since 요청 헤더는 마지막 수정 날짜를 포함해야합니다.

이러한 헤더 중 하나를 사용하여 만료 된 리소스를 다시 가져와야하는지 확인할 수 있습니다. 헤더의 유효성을 검사 한 후 서버는 리소스를 다시 보내거나 변경이 없음을 나타내는 304 HTTP 코드를 보낼 수 있습니다 . 후자의 경우 브라우저는 캐시 된 리소스를 계속 사용할 수 있습니다.

package jpabook.jpashop.controller;

import static org.springframework.http.HttpHeaders.IF_UNMODIFIED_SINCE;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@RunWith(SpringRunner.class)
@WebMvcTest(HomeController.class)
public class HomeControllerTest {

  @Autowired
  public MockMvc mockMvc;

  @Test
  public void whenValidate_thenReturnCacheHeader() throws Exception {
    HttpHeaders headers = new HttpHeaders();
    headers.add(IF_UNMODIFIED_SINCE, "Tue, 04 Feb 2020 19:57:25 GMT");
    this.mockMvc.perform(MockMvcRequestBuilders.get("/hello/aaa").headers(headers))
        .andDo(MockMvcResultHandlers.print())
        .andExpect(MockMvcResultMatchers.status().is(304));
  }
}
    @RequestMapping("/hello/{name}")
    public ResponseEntity<String> home(@PathVariable String name, WebRequest request)
        throws IOException {

//        Resource resource = new ClassPathResource("test.txt");

        ZoneId zoneId = ZoneId.of("GMT");
        long lastModifiedTimestamp = LocalDateTime.of(2020, 02, 4, 19, 57, 45)
            .atZone(zoneId).toInstant().toEpochMilli();

        if (request.checkNotModified(lastModifiedTimestamp)){
            return ResponseEntity.status(304).build();
        }

        return ResponseEntity.ok()
            .cacheControl(CacheControl.noCache())
//            .lastModified(resource.lastModified())
            .body("Hello" + name);
    }

 

하다보니까, max-age, noCache, etc.. CacheControl 클래스에 있는 것들이 궁금해졌다.

CacheControl 안에는

CacheControl 헤더는 캐시 동작과 관련된 프로토콜로서, 값으로 여러 항목이 존재한다는 것을 알고, 그 항목마다 의미와 목적이 다르다는 사실을 알았다. 여러 항목이 존재하는 경우에 콤마(,)각각의 항목을 구분한다.

max-age
max-age는 요청과 응답에서 모두 사용할 수 있다.
max-age 를 서버에서 사용하게 되면, 캐시서버에서 캐싱하고 있는 컨텐츠의 유효기간을 정의하는 것이다. 서버로 다시 확인하지 않고 클라이언트에게 제공할 수 있는 최대 시간이다.
Expires 헤더와 비슷하지만, 두 헤더가 모두 존재하는 경우 캐시서버는 Expires 헤더를 무시한다.

참고: HTTP 1.0 에서는 Cache-Control의 max-age 헤더가 무시된다. 따라서 캐시서버가 HTTP 1.0 또는 1.1 두 버젼 중 지원하는 버젼을 모를경우 Expires 와 Cache-Control의 max-age를 같이 사용하면 좋다. 

max-age 값이 클라이언트에게서 사용되는 경우에는 클라이언트가 캐싱된 이미지를 사용하는 기준을 정하게 되는 것이다.
예를 들어 캐시서버에 저장된 컨텐츠가 실제 서버에 있는 컨텐츠와 같다고 하더라도.. 클라이언트가 정의한 max-age 값보다 크다면,  캐시서버는 자신이 가지고 있는 컨텐츠를 직접 제공하지 못한다.
극단적으로 클라이언트가 max-age 값을 '0' 으로 정하게 되면, 캐시서버는 항상 실제서버로 요청을 전달하여 유효성을 매번 확인해야 한다.
캐시서버의 목적이 유효한 컨텐츠의 경우 서버로 요청을 전달하지 않게 하여 웹서버의 성능 향상을 도모하는것이 주인데 이러한 경우 효과를 기대할 수 없게 된다.

max-stale
max-stale 은 요청에서 사용되는 헤더 정보이다.
max-stale 은 만료된 컨텐츠라 하더라도 캐싱된 컨텐츠가 있다면 사용하겠다는 뜻이다. 클라이언트는 또한 만료된 컨텐츠의 재활용 가능 시간을 정할 수가 있는데 다음과 같은 형식으로 사용한다.

Cache-Control: max-stale=600

위와 같이 max-stale 의 값으로 600 을 명시한 경우, 만료된 컨텐츠는 이후 10분까지 재사용할 수 있다는 것을 의미한다.

min-fresh
min-fresh 도 요청에서 사용되는 정보이다.
클라이언트가 요청하는 컨텐츠가 현재도 유효해야 겠지만, 이후 수초 동안에도 변경되지 않았으면 하는 경우.. min-fresh 에 값을 정의하여 사용하면 된다.  다음 예를 보자.

Cache-Control: min-fresh=60

현재 요청하는 컨텐츠가 지금을 기준으로 이후 60초 동안 변경되지 않을 것이라면 캐싱된 컨텐츠를 직접 제공하라.. 라는 의미인것이다.

must-revalidate
must-revalidate 는 응답헤더에 사용되는 정보인데, 컨텐츠의 유효성을 최대한 보장하기 위한 것이다. 이후 요청에서 캐싱된 컨텐츠라 하더라도 반드시 웹서버로부터 유효성 체크를 받아야 한다는 뜻이다.

또한, 클라이언트가 max-stale 을 이용하면 유효하지 않은 컨텐츠라도 캐싱된 것을 이용할 수 있다. 이 경우 유효하지 않은 컨텐츠가 클라이언트에게 제공될 수 있는데 must-revalidate은 이것을 방지하기 위해 사용되는 정보이다.

Cache-Control: must-revalidate

응답헤더에 위 정보가 포함되게 되면, 캐시서버는 이후 동일한 컨텐츠에 max-stale 정보가 있다고 하더라도 이를 무시한다. 즉, 만료된 컨텐츠는 사용하지 않게 한다

no-cache
no-cache는 요청과 응답 모두 사용된다.
클라이언트의 요청에 no-cache 정보가 포함되어 있다면, 말 그대로 캐시서버에게 캐싱된 컨텐츠가 있다고 하더라도 실제 웹서버에게서 컨텐츠를 제공받고 싶다는 의미이다.
no-cache는 'max-age=0' 과 어찌 보면 동작면에서 비슷하지만 약간의 차이가 있긴 하다. no-cache의 경우는 컨텐츠 자체를 웹서버에게서 받겠다는 것인 반면, max-age=0 은 캐싱된 컨텐츠의 유효성을 웹서버에게서 확인받겠다는 뜻이다.
다시 말하면.... no-cache는 컨텐츠를 웹서버에게서 받아 오는 것이고, max-age=0 은 캐시서버가 가지고 있는 컨텐츠의 유효성을 웹서버에게서 체크해서 유효하다면 캐시서버로부터 컨텐츠를 받아 오는 것이다.

서버의 응답에 no-cache가 존재하는 경우 웹서버는 캐시서버에게 다음을 요구한다.
'다음번 요청에 유효성 확인을 하지 않고 캐싱된 컨텐츠를 사용하지 말아 달라'
이는 웹서버가 강제로 캐시서버에게 유효성 확인을 주문하고자 할때 사용하는 것이다.

Cache-Control: no-cache="Accept-Ranges"

위와 헤더 정보가 응답에 포함된 경우, Accept-Ranges 가 응답헤더에 포함되지 않은 경우에만 캐싱된 컨텐츠를 사용하고, 포함된 경우에는 유효성 체크를 해야 한다는 의미이다.

no-store
요청과 응답에 모두 사용될 수 있는 정보이며, 말그대로 캐시서버에게 컨텐츠를 저장하지 말라고 하는 것이다. 스토리지에 저장되지 않으면 캐시서버로써의 역할을 할 수 없음을 우리는 연결해서 생각해 볼 수 있다.

매우 민감한 정보(예를 들어 주민번호와 같은 개인정보)가 포함된 컨텐츠인 경우 보안을 이유로 다른 매체에 저장되는 것을 방지하고 싶을 수 있다. 이 경우 해당 컨텐츠에 대해 no-store 를 정의하게 되면 캐시서버는 자신의 스토리지에 저장하지 않게 된다.

no-transform
이것도 요청과 응답에 모두 사용될 수 있는 정보이다. no-transform은 웹서버가 제공한 컨텐츠를 어떤 형태로도 변형하면 안된다는 것을 요구할 때 사용한다.
예를 들어 이미지 파일의 경우 해상도등을 캐시서버의 저장공간의 효율성을 위해 해상도를 낮추어 저장할 수도 있는데.. 이러한 행위를 하지 말라는 뜻이다.

only-if-cached
요청에만 사용되는 정보이다. 말 그대로 캐시서버에게 만약 캐싱하고 있는 컨텐츠라면 응답해달라는 뜻이다.
그렇다면 캐싱하지 않은 컨텐츠라면.. .??
그렇다. 웹서버에게 가지 않고.. 그냥 에러 메시지를 반환하게 된다.
이때의 에러 메시지는 "504 Gateway Timeout" 이다 .

네트워크 환경이 너무 열악하여, 캐시서버까지는 빠른 반면, 실제 웹서버까지는 지연(Delay)이 너무 심한 경우 캐시서버가 캐싱하고 있는 컨텐츠에 한해서 제공받겠다는 의미이다.
사실 이런 상황이 없다고 봐도 되기에  사용되지 않는 정보로 봐도 될것 같다.

private
응답에 사용되는 정보이며, 특정 사용자만을 위한 헤더 정보라 보면 된다.
캐시서버가 캐싱하고 있는 컨텐츠를 최초 요청했던 사용자에게만 제공해야 하는 경우 사용된다.
이 경우 다른 사용자에게는 캐싱하고 있는 컨텐츠라 하더라도 절대 제공되지 않는다.

proxy-revalidate
응답에 사용되는 정보이다.
중간에 있는 프록시 서버(캐시서버)들은 자신이 캐싱하고 있는 컨텐츠라 할지라도 웹서버로부터 컨텐츠 유효성 체크를 받아야 한다는 것을 뜻한다.
must-revalidate 과 거의 비슷하지만, proxy-revalidate 은 클라이언트 자신의 캐싱 컨텐츠에 대해서는 유효성 체크 없이 그대로 사용할 수 있다는 차이가 있다.
must-revalidate 은 웹브라우저가 캐싱하고 있는 컨텐츠에 대해서도 직접 사용하지 못하고 유효성을 체크 받아야 하나 proxy-revalidate 은 프록시 서버(캐시서버)들만 유효성 체크를 받으면 된다는 뜻이다.

public
이것은 private 과 반대되는 의미이다.
private 은 특정 사용자에 한해서 캐싱된 컨턴츠를 이용할 수 있었다면, public은 그렇지 않다. 모든 사용자가 이용할 수 있다. 클라이언트가 인증정보를 제공한다고 하더라도 캐시서버는 private user 처럼 처리한다. 인증정보에 상관없이 캐싱된 컨텐츠를 제공한다는 뜻이다.

s-maxage 
이는 max-age와 사용 목적에서는 같다. 응답에서만 사용되는 정보이다.
max-age 와 다른 점은 다수의 사용자들에게 컨텐츠를 제공하는 형태의 공용 캐시서버에게만 유효 하다는 것이다.
웹브라우저와 같은 클라이언트의 로컬 캐시에는 적용되지 않는다.

댓글