1. 개요

이 사용방법(예제)에서는 HTTP 캐싱에 대해 알아 봅니다. 또한 클라이언트와 Spring MVC 애플리케이션간에이 메커니즘을 구현하는 다양한 방법을 살펴볼 것입니다.

2. HTTP 캐싱 소개

브라우저에서 웹 페이지를 열면 일반적으로 웹 서버에서 많은 리소스를 다운로드합니다.

예를 들어,이 예에서 브라우저는 하나의 / login  페이지에 대해 세 개의 리소스를 다운로드해야 합니다. 브라우저가 모든 웹 페이지에 대해 여러 HTTP 요청을 만드는 것이 일반적입니다. 이제 이러한 페이지를 매우 자주 요청하면 네트워크 트래픽이 많이 발생하고 이러한 페이지를 제공하는 데 시간이 더 오래 걸립니다 .

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

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

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

다음 섹션에서는 Spring MVC 컨트롤러의 응답에이 헤더를 추가합니다. 나중에 만료 시간을 기준으로 캐시 된 리소스의 유효성을 검사하는 Spring API도 볼 수 있습니다.

3. 컨트롤러 응답의 캐시 제어

3.1. ResponseEntity 사용 

이를 수행하는 가장 간단한 방법은 Spring에서 제공 하는 CacheControl  빌더 클래스 사용하는 것입니다 .

@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);
}

그러면 응답에 Cache-Control 헤더 가 추가됩니다 .

@Test
void whenHome_thenReturnCacheHeader() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.get("/hello/baeldung"))
      .andDo(MockMvcResultHandlers.print())
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.header()
        .string("Cache-Control","max-age=60, must-revalidate, no-transform"));
}

3.2. HttpServletResponse 사용 

종종 컨트롤러는 핸들러 메서드에서 뷰 이름을 반환해야합니다. 그러나  ResponseEntity 클래스는 뷰 이름을 반환하고 동시에 요청 본문을 처리하는 것을 허용하지 않습니다 .

또는 이러한 컨트롤러의 경우 HttpServletResponse에서  직접 Cache-Control 헤더를 설정할 수  있습니다.

@GetMapping(value = "/home/{name}")
public String home(@PathVariable String name, final HttpServletResponse response) {
    response.addHeader("Cache-Control", "max-age=60, must-revalidate, no-transform");
    return "home";
}

이것은 또한 마지막 섹션과 유사한 HTTP 응답에 Cache-Control 헤더를 추가합니다 .

@Test
void whenHome_thenReturnCacheHeader() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.get("/home/baeldung"))
      .andDo(MockMvcResultHandlers.print())
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.header()
        .string("Cache-Control","max-age=60, must-revalidate, no-transform"))
      .andExpect(MockMvcResultMatchers.view().name("home"));
}

4. 정적 리소스에 대한 캐시 제어

일반적으로 Spring MVC 애플리케이션 HTML, CSS 및 JS 파일과 같은 많은 정적 리소스를 제공 합니다. 이러한 파일은 많은 네트워크 대역폭을 사용하므로 브라우저가 파일을 캐시하는 것이 중요합니다. 응답 Cache-Control 헤더를 사용하여 다시 활성화합니다 .

Spring을 사용하면 리소스 매핑에서이 캐싱 동작을 제어 할 수 있습니다.

@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/resources/**").addResourceLocations("/resources/")
      .setCacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS)
        .noTransform()
        .mustRevalidate());
}

이렇게하면 / resources 아래에 정의 된 모든 리소스 가 응답 Cache-Control 헤더 와 함께 반환됩니다 . 

5. 인터셉터의 캐시 제어

Spring MVC 애플리케이션에서 인터셉터를 사용 하여 모든 요청에 ​​대해 사전 및 사후 처리를 수행 할 수 있습니다. 이것은 애플리케이션의 캐싱 동작을 제어 할 수있는 또 다른 자리 표시 자입니다.

이제 사용자 정의 인터셉터를 구현하는 대신 Spring에서 제공 하는 WebContentInterceptor를  사용합니다 .

@Override
public void addInterceptors(InterceptorRegistry registry) {
    WebContentInterceptor interceptor = new WebContentInterceptor();
    interceptor.addCacheMapping(CacheControl.maxAge(60, TimeUnit.SECONDS)
      .noTransform()
      .mustRevalidate(), "/login/*");
    registry.addInterceptor(interceptor);
}

여기에서 WebContentInterceptor를  등록  하고 마지막 몇 섹션과 유사한 Cache-Control 헤더를 추가했습니다 . 특히 다른 URL 패턴에 대해 다른 Cache-Control 헤더를 추가 할 수 있습니다 .

위의 예에서 / login으로 시작하는 모든 요청에 ​​대해 다음 헤더를 추가합니다.

@Test
void whenInterceptor_thenReturnCacheHeader() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.get("/login/baeldung"))
      .andDo(MockMvcResultHandlers.print())
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.header()
        .string("Cache-Control","max-age=60, must-revalidate, no-transform"));
}

6. Spring MVC에서 캐시 유효성 검사

지금까지 응답에 Cache-Control 헤더 를 포함하는 다양한 방법에 대해 설명했습니다 . 이는 max-age 와 같은 구성 속성을 기반으로 리소스를 캐시 할 클라이언트 또는 브라우저를 나타냅니다 .

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

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

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

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

는  LASTMODIFIED  헤더는 초 정밀도로 시간 간격을 저장할 수 있습니다. 이는 더 짧은 만료가 필요한 경우 제한 사항이 될 수 있습니다. 따라서 대신 Etag 를 사용하는 것이 좋습니다 . Etag  헤더는 해시 값을 저장 하므로  나노초와 같이 더 미세한 간격까지 고유 한 해시를 생성 할 수 있습니다.

즉, LastModified 를 사용하는 것이 어떤 모습인지 확인해 보겠습니다 .

Spring은 요청에 만료 헤더가 있는지 여부를 확인하는 몇 가지 유틸리티 메서드를 제공합니다.

@GetMapping(value = "/productInfo/{name}")
public ResponseEntity<String> validate(@PathVariable String name, WebRequest request) {
 
    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().body("Hello " + name);
}

Spring은 마지막 요청 이후 자원이 수정되었는지 확인하기 위해 checkNotModified ()  메소드를 제공합니다  .

@Test
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("/productInfo/baeldung").headers(headers))
      .andDo(MockMvcResultHandlers.print())
      .andExpect(MockMvcResultMatchers.status().is(304));
}

7. 결론

이 기사에서는 Spring MVC에서 Cache-Control 응답 헤더를 사용하여 HTTP 캐싱에 대해 배웠습니다 . ResponseEntity  클래스를 사용하거나 정적 리소스에 대한 리소스 매핑을 통해 컨트롤러의 응답에 헤더를 추가 할 수 있습니다 .

Spring 인터셉터를 사용하여 특정 URL 패턴에 대해이 헤더를 추가 할 수도 있습니다.

항상 그렇듯이 코드는 GitHub에서 사용할 수 있습니다 .