1. HTTP Caching

  • 응답 값의 복사본을 재사용하여 리소스를 불러오는 속도를 향상시키기 위함이다.
  • 일반적으로 GET 응답만 캐싱하며, <key, value> 를 <URL, response> 구조 형태로 가진다.
  • 캐싱한 리소스의 위치는 브라우저의 private cache 에 존재할 수도 있고, 프록시 서버(proxy, reverse proxy, CDN) 의 Shared Cache 로 존재할 수도 있다.

 

2. HTTP Cache 동작 원리

  1. cache hit : 클라이언트가 요청한 데이터가 캐시되어 있는 경우, 캐시에 존재하는 데이터를 클라이언트에게 전달
  2. cache miss : 클라이언트가 요청한 데이터가 캐시되어 있지 않은 경우, 서버로 부터 데이터를 조회해 클라이언트에게 전달 (해당 데이터는 캐시에 저장)
  3. cache revalidation
    1. 캐시의 사본이 최신인지 서버에 검사를 요청.
    2. 구분

cache hit, cache miss, cache revalidate hit

 

 

 

3. HTTP Header : Cache-Control

  1. 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 를 설정하지 않으면 휴리스틱 캐싱이 적용되어 자체적으로 캐싱을 적용하고 재사용한다.
      • 휴리스틱 캐싱
        • 경험적으로 만료 일자를 저장하는 캐싱 방법으로, 클라이언트(브라우저)가 임의로 캐시를 조절한다.
        • 휴리스틱 캐싱은 서버에서 해당 캐시를 제거할 수 없어 클라이언트가 서버에서 최신 데이터를 받아올 수 있는 방법이 없다.
  2. Date : 응답이 생성된 시간을 의미
    • Date: Mon, 26 Dec 2022 22:22:22 GMT
  3. 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 상태 코드와 함께 최신 데이터를 전달 (해당 데이터는 캐시에 저장)

revalidate hit, revalidate miss

 

 

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

  1. localhost:8080/resource-versioning 요청
    • /resources/[version-number]/js/index.js 응답 헤더 : Cache-Control : max-age=31536000 확인
  2. /resources/[version-number]/js/index.js 요청
    • 응답 헤더 304 Not Modified 응답 헤더 확인

/resource-versioning network
/resources/[version]/js/index.js network

 

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

'인프라 공방 > summary' 카테고리의 다른 글

nGrinder 로컬 테스트 구성을 위한 에러 기록  (0) 2024.03.29
nGrinder tutorial  (0) 2024.03.19
pinpoint simple summary  (0) 2024.03.08