Spring MVC의 캐시 헤더

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 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 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에서 사용할 수 있습니다.