1. HTTP Caching
- 응답 값의 복사본을 재사용하여 리소스를 불러오는 속도를 향상시키기 위함이다.
- 일반적으로 GET 응답만 캐싱하며, <key, value> 를 <URL, response> 구조 형태로 가진다.
- 캐싱한 리소스의 위치는 브라우저의 private cache 에 존재할 수도 있고, 프록시 서버(proxy, reverse proxy, CDN) 의 Shared Cache 로 존재할 수도 있다.
2. HTTP Cache 동작 원리
- cache hit : 클라이언트가 요청한 데이터가 캐시되어 있는 경우, 캐시에 존재하는 데이터를 클라이언트에게 전달
- cache miss : 클라이언트가 요청한 데이터가 캐시되어 있지 않은 경우, 서버로 부터 데이터를 조회해 클라이언트에게 전달 (해당 데이터는 캐시에 저장)
- cache revalidation
- 캐시의 사본이 최신인지 서버에 검사를 요청.
- 구분

3. HTTP Header : Cache-Control
- Cache-Control : Date Header 로 부터 경과된 시간과 비교하여 만료여부 판단
- 캐시 값
- Cache-Control: max-age=604800 : Date Header 로 부터 1주일이 경과했는지 확인하여 만료여부 판단
- Cache-Control: no-cache : max-age=0 과 같다. 로컬 저장소에 저장하는 것을 막지는 않으며, 항상 서버에 최신 컨텐츠 여부를 확인한다.
- Cache-Control: no-store : 클라이언트 요청, 서버 응답에 간해 어떤 것도 저장해서는 안된다. 요청은 서버 측으로 전송되고 전체 응답은 매번 다운로드된다.
- Cache-Control: private : 요청한 사용자만 캐시할 수 있다는 의미 (개인정보 보호 의미X)
- Cache-Control Expires Header 를 설정하지 않으면 휴리스틱 캐싱이 적용되어 자체적으로 캐싱을 적용하고 재사용한다.
- 휴리스틱 캐싱
- 경험적으로 만료 일자를 저장하는 캐싱 방법으로, 클라이언트(브라우저)가 임의로 캐시를 조절한다.
- 휴리스틱 캐싱은 서버에서 해당 캐시를 제거할 수 없어 클라이언트가 서버에서 최신 데이터를 받아올 수 있는 방법이 없다.
- 휴리스틱 캐싱
- 캐시 값
- Date : 응답이 생성된 시간을 의미
- Date: Mon, 26 Dec 2022 22:22:22 GMT
- Expires : 응답이 만료되는 시간을 의미
- Expires: Sun, 23 Feb 2025 08:23:51 GMT
- Cache-Control 헤더가 존재하는 경우, Expires Header 는 무시된다.
4. HTTP 조건부 요청
- 브라우저가 서버에 업데이트 된 리소스가 있는지 보내는 요청
- 캐시는 서버에 주기적으로 revalidate 요청을 보낸다.
- revalidate hit(slow hit) : 캐시가 최신일 경우, 서버에서 304 Not Modified 상태 코드를 전달하며 캐시 데이터 전달
- revalidate miss : 캐시가 최신 데이터가 아닌 경우, 서버에서 200 OK 상태 코드와 함께 최신 데이터를 전달 (해당 데이터는 캐시에 저장)

5. HTTP Header : last-modified, etag(entity tag)
[1] Time-based
- Last-Modified : 응답 헤더에 Last-Modified 가 있으면 Time-based 조건이 활성화되는 헤더
- Last-Modified : Mon, 03 Jan 2011 17:45:57 GMT
- If-Modified-Since : 브라우저는 날짜를 저장해두고 해당 리소스를 요청할 때 헤더에 If-Modified-Since 를 추가한다.
- If-Modified-Since : If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
- 구분
- 리소스 날짜가 동일하면 304 Not modified 응답 코드를 전송한다. (캐시의 리소스가 변경되지 않은 경우)
- 리소스 날짜가 동일하지 않으면 200 OK 응답를 전송하며 리소스를 반환한다. (캐시의 리소스가 변경된 경우)
[2] Content-based
- ETag : 특정 버전의 리소스를 식별하는 식별자 헤더이다.
- ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
- ETag: W/"0815"
- If-None-Match : 응답 헤더에 포함되어 있으면 Content-based 조건이 활성화되는 헤더이다.
- 구분
- Etag 값이 동일하면 서버에서 304Not modified 응답 코드를 전송한다. (캐시의 리소스가 변경되지 않은 경우)
- Etag 값이 존재하지 않으면 200 OK 응답 코드를 전송하며 리소스를 반환한다. (캐시의 리소스가 변경된 경우)
- 구분
6. Spring 에서 Cache-Control 적용하기
- 모든 path 에 no-cache, private 를 설정하는 코드
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.WebContentInterceptor;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(final InterceptorRegistry registry) {
WebContentInterceptor interceptor = new WebContentInterceptor();
interceptor.addCacheMapping(CacheControl.noCache().cachePrivate(), "/**");
registry.addInterceptor(interceptor);
}
}

7. Spring 에서 압축 알고리즘 적용하기
- server.compression.enabled : Content-Encoding: gzip 을 사용함을 명시하는 프로퍼티
- server.compression.mime-types : 압축을 적용할 mime-type 을 설정하는 프로퍼티
- server.compression.min-response-size : 응답이 압축되기 전 최소 크기 설정
- defualt: 2048 bytes = 2KB
- 해당 크기보다 작은 압축은 되지 않음
server:
compression:
enabled: true
mime-types: text/html,text/css,application/javascript,application/json
min-response-size: 10

8. Spring 에서 Etag 적용하기
import javax.servlet.Filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean registration = new FilterRegistrationBean();
Filter etagHeaderFilter = new ShallowEtagHeaderFilter();
registration.setFilter(etagHeaderFilter);
registration.addUrlPatterns("/etag");
return registration;
}
}
9. Spring 에서 Cache busting
- localhost:8080/resource-versioning 요청
- /resources/[version-number]/js/index.js 응답 헤더 : Cache-Control : max-age=31536000 확인
- /resources/[version-number]/js/index.js 요청
- 응답 헤더 304 Not Modified 응답 헤더 확인


source code
import java.time.Duration;
import javax.servlet.Filter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.WebContentInterceptor;
import com.brainbackdoor.support.ResourceVersion;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
public static final String PREFIX_STATIC_RESOURCES = "/resources";
private final ResourceVersion version;
@Autowired
public WebMvcConfig(ResourceVersion version) {
this.version = version;
}
@Override
public void addInterceptors(final InterceptorRegistry registry) {
WebContentInterceptor interceptor = new WebContentInterceptor();
interceptor.addCacheMapping(CacheControl.noCache().cachePrivate(), "/*");
registry.addInterceptor(interceptor)
.excludePathPatterns(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**");
}
@Bean
public FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean registration = new FilterRegistrationBean();
Filter etagHeaderFilter = new ShallowEtagHeaderFilter();
registration.setFilter(etagHeaderFilter);
registration.addUrlPatterns("/etag", PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/*");
return registration;
}
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
CacheControl cacheControl = CacheControl.maxAge(Duration.ofDays(365)).cachePublic(); // 1시간 설정
registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**") // path prefix 설정
.addResourceLocations("classpath:/") // 정적 리소스 경로
.setCacheControl(cacheControl);
}
}
@Test
void testCacheBustingOfStaticResources() {
final var uri = String.format("%s/%s/js/index.js", PREFIX_STATIC_RESOURCES, version.getVersion());
// "/resource-versioning/[version]/js/index.js" 경로의 정적 파일에 ETag를 사용한 캐싱이 적용되었는지 확인한다.
final var response = webTestClient
.get()
.uri(uri)
.exchange()
.expectStatus().isOk()
.expectHeader().exists(HttpHeaders.ETAG)
.expectHeader().cacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic())
.expectBody(String.class).returnResult();
log.info("response header\n{}", response.getResponseHeaders());
log.info("response body\n{}", response.getResponseBody());
final var etag = response.getResponseHeaders().getETag();
// 캐싱되었다면 "/resource-versioning/[version]/js/index.js"로 다시 호출했을때 HTTP status는 304를 반환한다.
webTestClient.get()
.uri(uri)
.header(HttpHeaders.IF_NONE_MATCH, etag)
.exchange()
.expectStatus()
.isNotModified();
}
Reference
- 참고 강의
- HTTP Header: Cache-Control
- HTTP Header: last-modified, Etag(entity tag)
- spring cache control
- server compression (gzip)
'인프라 공방 > summary' 카테고리의 다른 글
nGrinder 로컬 테스트 구성을 위한 에러 기록 (0) | 2024.03.29 |
---|---|
nGrinder tutorial (0) | 2024.03.19 |
pinpoint simple summary (0) | 2024.03.08 |
1. HTTP Caching
- 응답 값의 복사본을 재사용하여 리소스를 불러오는 속도를 향상시키기 위함이다.
- 일반적으로 GET 응답만 캐싱하며, <key, value> 를 <URL, response> 구조 형태로 가진다.
- 캐싱한 리소스의 위치는 브라우저의 private cache 에 존재할 수도 있고, 프록시 서버(proxy, reverse proxy, CDN) 의 Shared Cache 로 존재할 수도 있다.
2. HTTP Cache 동작 원리
- cache hit : 클라이언트가 요청한 데이터가 캐시되어 있는 경우, 캐시에 존재하는 데이터를 클라이언트에게 전달
- cache miss : 클라이언트가 요청한 데이터가 캐시되어 있지 않은 경우, 서버로 부터 데이터를 조회해 클라이언트에게 전달 (해당 데이터는 캐시에 저장)
- cache revalidation
- 캐시의 사본이 최신인지 서버에 검사를 요청.
- 구분

3. HTTP Header : Cache-Control
- Cache-Control : Date Header 로 부터 경과된 시간과 비교하여 만료여부 판단
- 캐시 값
- Cache-Control: max-age=604800 : Date Header 로 부터 1주일이 경과했는지 확인하여 만료여부 판단
- Cache-Control: no-cache : max-age=0 과 같다. 로컬 저장소에 저장하는 것을 막지는 않으며, 항상 서버에 최신 컨텐츠 여부를 확인한다.
- Cache-Control: no-store : 클라이언트 요청, 서버 응답에 간해 어떤 것도 저장해서는 안된다. 요청은 서버 측으로 전송되고 전체 응답은 매번 다운로드된다.
- Cache-Control: private : 요청한 사용자만 캐시할 수 있다는 의미 (개인정보 보호 의미X)
- Cache-Control Expires Header 를 설정하지 않으면 휴리스틱 캐싱이 적용되어 자체적으로 캐싱을 적용하고 재사용한다.
- 휴리스틱 캐싱
- 경험적으로 만료 일자를 저장하는 캐싱 방법으로, 클라이언트(브라우저)가 임의로 캐시를 조절한다.
- 휴리스틱 캐싱은 서버에서 해당 캐시를 제거할 수 없어 클라이언트가 서버에서 최신 데이터를 받아올 수 있는 방법이 없다.
- 휴리스틱 캐싱
- 캐시 값
- Date : 응답이 생성된 시간을 의미
- Date: Mon, 26 Dec 2022 22:22:22 GMT
- Expires : 응답이 만료되는 시간을 의미
- Expires: Sun, 23 Feb 2025 08:23:51 GMT
- Cache-Control 헤더가 존재하는 경우, Expires Header 는 무시된다.
4. HTTP 조건부 요청
- 브라우저가 서버에 업데이트 된 리소스가 있는지 보내는 요청
- 캐시는 서버에 주기적으로 revalidate 요청을 보낸다.
- revalidate hit(slow hit) : 캐시가 최신일 경우, 서버에서 304 Not Modified 상태 코드를 전달하며 캐시 데이터 전달
- revalidate miss : 캐시가 최신 데이터가 아닌 경우, 서버에서 200 OK 상태 코드와 함께 최신 데이터를 전달 (해당 데이터는 캐시에 저장)

5. HTTP Header : last-modified, etag(entity tag)
[1] Time-based
- Last-Modified : 응답 헤더에 Last-Modified 가 있으면 Time-based 조건이 활성화되는 헤더
- Last-Modified : Mon, 03 Jan 2011 17:45:57 GMT
- If-Modified-Since : 브라우저는 날짜를 저장해두고 해당 리소스를 요청할 때 헤더에 If-Modified-Since 를 추가한다.
- If-Modified-Since : If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
- 구분
- 리소스 날짜가 동일하면 304 Not modified 응답 코드를 전송한다. (캐시의 리소스가 변경되지 않은 경우)
- 리소스 날짜가 동일하지 않으면 200 OK 응답를 전송하며 리소스를 반환한다. (캐시의 리소스가 변경된 경우)
[2] Content-based
- ETag : 특정 버전의 리소스를 식별하는 식별자 헤더이다.
- ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
- ETag: W/"0815"
- If-None-Match : 응답 헤더에 포함되어 있으면 Content-based 조건이 활성화되는 헤더이다.
- 구분
- Etag 값이 동일하면 서버에서 304Not modified 응답 코드를 전송한다. (캐시의 리소스가 변경되지 않은 경우)
- Etag 값이 존재하지 않으면 200 OK 응답 코드를 전송하며 리소스를 반환한다. (캐시의 리소스가 변경된 경우)
- 구분
6. Spring 에서 Cache-Control 적용하기
- 모든 path 에 no-cache, private 를 설정하는 코드
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.WebContentInterceptor;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(final InterceptorRegistry registry) {
WebContentInterceptor interceptor = new WebContentInterceptor();
interceptor.addCacheMapping(CacheControl.noCache().cachePrivate(), "/**");
registry.addInterceptor(interceptor);
}
}

7. Spring 에서 압축 알고리즘 적용하기
- server.compression.enabled : Content-Encoding: gzip 을 사용함을 명시하는 프로퍼티
- server.compression.mime-types : 압축을 적용할 mime-type 을 설정하는 프로퍼티
- server.compression.min-response-size : 응답이 압축되기 전 최소 크기 설정
- defualt: 2048 bytes = 2KB
- 해당 크기보다 작은 압축은 되지 않음
server:
compression:
enabled: true
mime-types: text/html,text/css,application/javascript,application/json
min-response-size: 10

8. Spring 에서 Etag 적용하기
import javax.servlet.Filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean registration = new FilterRegistrationBean();
Filter etagHeaderFilter = new ShallowEtagHeaderFilter();
registration.setFilter(etagHeaderFilter);
registration.addUrlPatterns("/etag");
return registration;
}
}
9. Spring 에서 Cache busting
- localhost:8080/resource-versioning 요청
- /resources/[version-number]/js/index.js 응답 헤더 : Cache-Control : max-age=31536000 확인
- /resources/[version-number]/js/index.js 요청
- 응답 헤더 304 Not Modified 응답 헤더 확인


source code
import java.time.Duration;
import javax.servlet.Filter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.WebContentInterceptor;
import com.brainbackdoor.support.ResourceVersion;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
public static final String PREFIX_STATIC_RESOURCES = "/resources";
private final ResourceVersion version;
@Autowired
public WebMvcConfig(ResourceVersion version) {
this.version = version;
}
@Override
public void addInterceptors(final InterceptorRegistry registry) {
WebContentInterceptor interceptor = new WebContentInterceptor();
interceptor.addCacheMapping(CacheControl.noCache().cachePrivate(), "/*");
registry.addInterceptor(interceptor)
.excludePathPatterns(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**");
}
@Bean
public FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean registration = new FilterRegistrationBean();
Filter etagHeaderFilter = new ShallowEtagHeaderFilter();
registration.setFilter(etagHeaderFilter);
registration.addUrlPatterns("/etag", PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/*");
return registration;
}
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
CacheControl cacheControl = CacheControl.maxAge(Duration.ofDays(365)).cachePublic(); // 1시간 설정
registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**") // path prefix 설정
.addResourceLocations("classpath:/") // 정적 리소스 경로
.setCacheControl(cacheControl);
}
}
@Test
void testCacheBustingOfStaticResources() {
final var uri = String.format("%s/%s/js/index.js", PREFIX_STATIC_RESOURCES, version.getVersion());
// "/resource-versioning/[version]/js/index.js" 경로의 정적 파일에 ETag를 사용한 캐싱이 적용되었는지 확인한다.
final var response = webTestClient
.get()
.uri(uri)
.exchange()
.expectStatus().isOk()
.expectHeader().exists(HttpHeaders.ETAG)
.expectHeader().cacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic())
.expectBody(String.class).returnResult();
log.info("response header\n{}", response.getResponseHeaders());
log.info("response body\n{}", response.getResponseBody());
final var etag = response.getResponseHeaders().getETag();
// 캐싱되었다면 "/resource-versioning/[version]/js/index.js"로 다시 호출했을때 HTTP status는 304를 반환한다.
webTestClient.get()
.uri(uri)
.header(HttpHeaders.IF_NONE_MATCH, etag)
.exchange()
.expectStatus()
.isNotModified();
}
Reference
- 참고 강의
- HTTP Header: Cache-Control
- HTTP Header: last-modified, Etag(entity tag)
- spring cache control
- server compression (gzip)
'인프라 공방 > summary' 카테고리의 다른 글
nGrinder 로컬 테스트 구성을 위한 에러 기록 (0) | 2024.03.29 |
---|---|
nGrinder tutorial (0) | 2024.03.19 |
pinpoint simple summary (0) | 2024.03.08 |