HTTP Caching은 웹 애플리케이션의 성능을 향상시키기 위해 사용하는 기술이다. 웹 캐시는 레이턴시와 네트워크 트래픽을 줄여줌으로서 리소스를 보여줄 때 필요한 시간을 단축시켜준다. HTTP Caching 기술을 이용하면 웹 사이트가 더 빠르게 반응하도록 제작할 수 있다. 스프링 MVC에서 사용가능한 HTTP Caching 옵션들을 정리해보자.
CacheControl은 Cache-Control 헤더와 관련있다. 서버와 브라우저 사이의 캐시 정책을 의미하며, 여러 지시자들을 이용하여 옵션을 설정할 수 있다.
공식 문서에 따르면 WebContentInterceptor, WebContentGenerator, Controller, Static Resource에서 인수로 사용된다고 한다. 문서에 작성된 코드를 살펴보면 CacheControl 객체를 이용하여 Cache-Control 헤더의 값을 조작하고 있는 것을 확인할 수 있다.
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
WebConfigurer를 이용하여 정적 리소스 경로를 설정했을 때 Cache-Control을 적용할 수 있다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// **/*.js의 요청에 대해 Cache 설정
// 요청이 캐싱 이후 60초 이내일 경우 해당 캐시는 유효하다 판단하여 캐싱 데이터를 사용
CacheControl cacheControl = CacheControl
.maxAge(60, TimeUnit.SECONDS)
.mustRevalidate();
registry.addResourceHandler("**/*.js")
.addResourceLocations("classpath:/static/")
.setCacheControl(cacheControl)
;
}
}
공식 문서에 따르면, Controller에서 명시적으로 HTTP Caching 지원을 추가할 수 있다고 한다. lastModified
와 ETag
는 요청 헤더에 담긴 값과 비교해야하는 추가적인 계산이 필요하기 때문에 추천하는 방법이라고 한다. (정확하게 해석한건가...? 🤔)
문서에 작성된 코드를 살펴보면 ResponseEntity에 Cache-Control과 ETag 설정을 추가하는 것을 확인할 수 있다. 만약 변경된 내용이 없다면 바디가 비어있는 304(Not-modified) 응답을 보낼 것이고, 변경된 내용이 있다면 Etag와 Cache-Control 헤더가 추가되어 전달된다고 한다.
@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {
Book book = findBook(id);
String version = book.getVersion();
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book);
}
또한 Controller에서 검증을 수행할 수 있다고 한다.
@RequestMapping
public String myHandleMethod(WebRequest request, Model model) {
long eTag = ...
if (request.checkNotModified(eTag)) {
return null;
}
model.addAttribute(...);
return "myViewName";
}
추가적으로 변경되지 않은 상황에 대해서 GET과 HEAD 요청에 대해서는 304 응답을, 그 외 POST, PUT, DELETE 요청에 대해서는 412(Precondition-failed) 응답으로 처리하면 된다고 한다.
공식 문서에서는 정적 자원은 Cache-Control과 별도의 헤더 옵션을 통해서 다루어야 한다고 한 문장으로 정의되어있다. 한 문장으로 정리되어있는 것으로 보았을 때, 개인적으로는 HTTP Caching에서 주요 목적이 정적인 자원들을 처리하기 위함이라는 것을 강조한 듯한 느낌이 들었다. 😤
정적 자원에 대한 설명과 마찬가지로 공식 문서에서는 간략하게 설명되어있다. 해석해보면 ETag 값을 추가하기 위해서 ShallowEtagHeaderFilter를 이용하면 된다는 이야기가 적혀있다. 이 필터를 사용하면 특정 URL 패턴에 대해서 ETag를 설정할 수 있다.
@Bean
public FilterRegistrationBean shallowEtagHeaderFilter() {
FilterRegistrationBean frb = new FilterRegistrationBean();
frb.setFilter(new ShallowEtagHeaderFilter());
frb.addUrlPatterns("MY_PATH/*");
return frb;
}
ETag가 무엇인지 감이 안오니 더 알아보자.
ETag(Entity Tag)는 웹 리소스의 특정 버전에 할당한 식별자다. lastModified
헤더보다 더 정교한 버전이라고 생각하면 된다고 한다. 동작 과정을 살펴보자.
ETag를 사용하면 향상된 퍼포먼스를 보여줄 수는 있지만 마냥 좋지는 않다고 한다. 생각해보면 여러 개의 서버를 운용하는데 같은 컨텐츠지만 ETag가 서로 다를 수 있기 때문이다.
ETag를 활용한 예제는 하단의 블로그에서 잘 정리가 되어있으니 참고하면 좋을 것 같다