우테코의 HTTP 수업 내용과 실제 스프링에 적용하는 과정을 정리했다.
no-cache(강제 재검증) vs no-store(캐싱을 하지않음)
cache-control에 no-cache 로 설정하면 캐시를 사용하지만 항상 서버(프록시서버, 리버스 프록시서버, CDN, 서버)에 재검증 요청을 보내게된다.
이런 강제 재검증을 해야하는 경우 max-age=0, must-revalidate 를 사용하는 경우도 있다. HTTP1.1 이전 버전에서는 no-cache를 제대로 처리를 못했기 때문에 사용했던 방법이다.
no-store 는 아예 캐싱하지 않겠다는 설정이다. 브라우저에서 백 포워드 캐시를 활용하지 못한다.(페이지 뒤로가기, 앞으로가기)
no-store 보다는 no-cache 사용을 추천하며 no-cache와 private을 같이 사용하면 클라이언트에서는 항상 최신 버전을 유지할 수 있다.
private vs public
cache-control에 private 으로 설정하게 되면 오로지 웹 브라우저에서만 캐싱할 수 있음을 나타낸다. 중간 서버에서는 캐싱하지 않는다.
사용시 주의할 점은 서버의 응답에 authorization 헤더가 있으면 private Cache에 저장되지 않는다.
public는 중간 서버에 캐싱할 수 있음을 나타낸다.
캐시의 유효기간(max-age)
최대 설정할 수 있는 기간은 1년이다.
캐시 유효기간을 표시할때 Expires를 쓰는 경우가 있지만 HTTP1.1 이전에 쓰던 방식이다.
s-maxage
중간 서버에서만 적용되는 max-age 값을 설정하기 위해 s-maxage 값을 사용할 수 있다.
만약 s-maxage=31536000, max-age=0 과 같이 설정하면 CDN에서는 1년동안 캐시되지만 브라우저에서는 매번 재검증 요청을 보내도록 설정할 수 있다.
이때 s-maxage가 유효하다면 클라이언트에서는 항상 CDN 에 재검증을 요청할 것이다.
서버에서 배포할 일이 생긴다면 CDN invalidation을 수행해서 캐시를 지운뒤에 새로운 버전을 CDN에 캐싱한다.
CDN을 사용한다면 s-maxage를 사용해서 서버의 부하를 줄일수 있다.(재검증 요청을 CDN에서 받는다) 다만 서버에서 배포할 일이 생긴다면 직접 CDN invalidation을 수행해서 CDN에 최신 버전에 대한 캐싱이 이뤄지도록 해야한다.
재검증(Conditional Request)
캐시의 유효기간이 만료됐다면 클라이언트는 재검증 요청을 보낸뒤 기존 캐시를 재사용할지 결정한다. 이 때 두가지 방법으로 재검증을 할 수 있다.
If-Modified-Since vs ETag/If-None-Match
If-Modified-Since는 Last-Modifed를 비교해서 유효성 검증을 한다. 이때 Last-Modifed는 초 단위로 헤더에 저장되기 때문에 간발의 차이로 서버에서 최신 데이터를 받아오지 못하는 경우가 생길 수 있다. Etag 헤더는 해시 값을 저장하므로 나노초와 같이 더 미세한 간격까지 처리할 수 있다.
ETag/If-None-Match는 시간이 아닌 해시값으로 유효성 검증을 하기 때문에 클라이언트는 항상 최신 데이터를 빠르게 받아올 수 있다.
Last-Modified 와 ETag 를 같이 쓰는 경우가 있다. 이 경우 재검증을 할때 당연히 ETag를 사용하며 Last-Modified는 캐싱 외에 크롤러에게 마지막 수정 시간을 알려주어 크롤링 빈도를 조정할 수 있다.
만약 응답에 Cache-Control, Expires 헤더가 들어있지 않다면 브라우저는 휴리스틱을 사용하여 자체적으로 캐시 유효기간을 계산한다.
휴리스틱 캐싱을 사용하지 않는 이유는 서버입장에서 휴리스틱 캐싱이 이뤄진 브라우저의 해당 캐시를 제거할 방법이 없다. 즉 클라이언트가 서버에서 최신 데이터를 받아올 수 없게 된다.
즉 효과적으로 캐시를 사용하기 위해서는 Cache-Control를 사용해서 휴리스틱 캐싱이 발생하지 않도록 한다.
캐시 무효화란 브라우저가 캐시에서 이전 파일을 검색하지 않고 서버에 새 파일을 요청하도록 하는 행위를 말한다.
js, css 같은 정적 파일의 캐시 유효기간을 1년으로 설정한다. 캐시는 URL별로 관리되기 때문에 URL이 바뀌지 않는 이상 브라우저는 서버에 요청을 보내지 않는다.
만약 js, css 파일이 수정되면 서버에서 URL에 버저닝을 더해준다.
클라이언트에서는 Main resources(html)에 대해 항상 재검증 요청을 보낸다. html 파일에 태그돼 있는 새로운 버전의 정적 파일을 캐싱하게 된다.
Main resources는 no-cache, private 으로 관리해서 항상 서버의 최신 버전을 바라볼 수 있도록 설정하고 js, css 같은 정적파일들은 캐시 유효기간을 최대한 길게 설정함으로써 캐시를 효율적으로 관리할 수 있다.
CDN을 사용할 경우(public) s-maxage를 이용해서 기존에 서버가 받던 재검증 요청을 CDN이 받도록 설정할 수 있다. 다만 새로운 버전이 배포될 경우 직접 CDN invalidation을 해줘야한다.
Cache-Control 설정하는 법
컨트롤러에서 캐시 제어
- ResponseEntity
사용
@GetMapping("/home/{name}")
@ResponseBody
public ResponseEntity<String> hello(@PathVariable String name) {
CacheControl cacheControl = CacheControl.noCache()
return ResponseEntity.ok()
.cacheControl(cacheControl)
.body("name: " + name);
}
- HttpServletResponse
사용(View name 응답)
@GetMapping(value = "/home/{name}")
public String home(@PathVariable String name, final HttpServletResponse response) {
String cacheControl = CacheControl.noCache().getHeaderValue();
response.addHeader("Cache-Control", cacheControl);
return "home";
}
정적 리소스(js, css)에 대한 캐시 제어
- ResourceHandler
Spring Boot에서는 기본적으로 정적 리소스 요청을 처리할 수 있는 ResourceHttpRequestHandler
를 제공한다.
이 핸들러는 클래스 경로에 있는 /static, /public, /resources, /META-INF/resources 디렉토리에서 정적 콘텐츠를 제공한다.
- https://github.com/spring-projects/spring-framework/blob/main/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java
- https://www.baeldung.com/spring-mvc-static-resources
WebMvcConfigurer
의 addResourceHandlers
메서드 재정의
URL 패턴을 구성하고 파일의 특정 위치에 매핑하기만 하면 된다.
둘 이상의 위치에서 리소스를 찾으려면 addResourceLocations
메서드를 통해서 여러 위치를 포함시킬 수 있다.
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
}
인터셉터에서 캐시 제어
WebContentInterceptor
를 사용
@Override
public void addInterceptors(InterceptorRegistry registry) {
WebContentInterceptor interceptor = new WebContentInterceptor();
interceptor.addCacheMapping(CacheControl.noCache().cachePrivate(), "/login/*");
registry.addInterceptor(interceptor);
}
https://www.baeldung.com/spring-mvc-cache-headers
Etag 설정하는 법
ShallowEtagHeaderFilter
사용
@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
final FilterRegistrationBean<ShallowEtagHeaderFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new ShallowEtagHeaderFilter());
registration.addUrlPatterns("resources/static/*");
return registration;
}
ShallowEtagHeaderFilter는 DigestUtils
를 사용해서 eTag 값을 생성하는데 이때 MD5 해싱 알고리즘을 사용한다. protected String generateETagHeaderValue(InputStream inputStream, boolean isWeak) throws IOException {
// length of W/ + " + 0 + 32bits md5 hash + "
StringBuilder builder = new StringBuilder(37);
if (isWeak) {
builder.append("W/");
}
builder.append("\"0");
DigestUtils.appendMd5DigestAsHex(inputStream, builder);
builder.append('"');
return builder.toString();
}
@GetMapping("/etag")
public ResponseEntity etag() {
return ResponseEntity.ok()
.eTag("eTag")
.build();
}
https://www.baeldung.com/etags-for-rest-with-spring
응답 압축하기
HTTP 응답을 압축하면 웹 사이트의 성능을 높일 수 있다.
스프링 부트 설정을 통해 gzip과 같은 HTTP 압축 알고리즘을 적용시킬 수 있다.
HTTP response Compression은 Jetty, Tomcat, Reacor Netty, Undertow 에서 지원된다.
설정 방법
// application.properties
server:
compression:
enabled: true
응답 길이와 content-type 에 대해서 기본으로 설정된 값이 있다.
응답 길이는 2048 바이트 이상일것, content-type은 text/html, text/xml, text/plain, text/css, text/javascript, application/javascript, application/json, application/xml 의 경우에 압축이 이루어진다.
각각의 설정은 아래의 방법으로 설정할 수 있다.
- server: compression: min-response-size: {size}
- server: compression: mime-types: {content-type}
공부해볼것: 구버전의 js, css 파일들은 어떻게 관리될까?
참고: 구구강의
우테코 크루 클레이, 블링, 필즈와의 대화
https://toss.tech/article/smart-web-service-cache
https://paulcalvano.com/2018-03-14-http-heuristic-caching-missing-cache-control-and-expires-headers-explained/