화면 성능 개선 전 학습 테스트
HTTP Cache, gzip,Servlet,Thread에 대한 학습 테스트를 진행 합니다.
학습 테스트
HTTP Cache
@Configuration
@EnableWebMvc
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);
}
모든 응답에 대해 처리 해야 했기 때문에 특정 컨트롤러에서 헤더에 정보를 추가하지 않고 설정 파일에서 캐시 값을 적용하도록 수정하였다.
server:
compression:
enabled: true
min-response-size: 10
스프링부트 설정 파일에 해당 설정 값을 작성하게 되면 아래와 같이 헤더 정보에 Transfer-Encoding이 chunked로 표시되는 것을 확인할 수 있다.
[Cache-Control:"no-cache, private", vary:"accept-encoding", Content-Type:"text/html;charset=UTF-8", Content-Language:"ko-KR", , Date:"Mon, 27 Jan 2025 14:44:58 GMT"]
@Bean
public FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean registration = new FilterRegistrationBean();
Filter etagHeaderFilter = new ShallowEtagHeaderFilter();
registration.setFilter(etagHeaderFilter);
registration.addUrlPatterns("/etag");
return registration;
}
etag
로 접근하는 요청에 E-Tag 값을 넣어 ETag가 무엇인지 테스트를 통해 확인한다.
[ETag:""005d25486ae7138209a9b6ceb6edf3b11"", Content-Type:"text/html;charset=UTF-8", Content-Language:"ko-KR", Date:"Mon, 27 Jan 2025 14:57:32 GMT", content-length:"293"]
@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) {
registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/js/**")
.addResourceLocations("classpath:/js/") // NOTE: 핸들러를 통해 들어온 정적 파일을 어디에 매칭할 것인지
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic());
}
정적 파일을 서빙할 때 캐싱이 적용 되어 있는 "resources/{version}/js/index.js" 에 캐싱이 적용되어 있고 max-age는 1년이라고 가정 해보자.
만약, 1년이 지나지 않은 시점에서 변경사항이 일어나서 배포해야 한다면 어떻게 할 것인가?
버저닝을 날짜로 사용한다면 배포 시점이 달라질 때 마다 캐시가 무효화 되고 새롭게 E-Tag를 저장하여 다시 캐싱을 적용할 수 있다.
리소스 요청 예시: /resources/20250128000572/js/index.js
[Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Last-Modified:"Mon, 27 Jan 2025 14:44:55 GMT", Cache-Control:"max-age=31536000, public", Accept-Ranges:"bytes", ETag:""0adf06cf637aff7c06810711225d7eec6"", Date:"Mon, 27 Jan 2025 15:04:42 GMT"], status code: 304 NOT_MODIFIED
Servlet
@Test
void testSharedCounter() throws Exception {
// 톰캣 서버 시작
final var tomcatStarter = TestHttpUtils.createTomcatStarter();
tomcatStarter.start();
// shared-counter 페이지를 4번 호출한다.
final var PATH = "/shared-counter";
TestHttpUtils.send(PATH);
TestHttpUtils.send(PATH);
TestHttpUtils.send(PATH);
final var response = TestHttpUtils.send(PATH);
// 톰캣 서버 종료
tomcatStarter.stop();
assertThat(response.statusCode()).isEqualTo(200);
// expected를 0이 아닌 올바른 값으로 바꿔보자.
// 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까?
// NOTE: Servlet의 인스턴스 변수를 공용으로 사용하고 있기 때문에 increase 할 때 프로세스가 실행될 때 최초 초기화 된 값에서 여러 스레드가 호출할 때 마다 공유하기 때문이다.
assertThat(Integer.parseInt(response.body())).isEqualTo(4);
}
4번 호출 한 서블릿의 카운터 변수는 결과적으로 "4"일 것이다. 그 이유는 쓰레드 간 해당 자원을 공유하고 있기 때문이다
void testLocalCounter() throws Exception {
// 톰캣 서버 시작
final var tomcatStarter = TestHttpUtils.createTomcatStarter();
tomcatStarter.start();
// local-counter 페이지를 3번 호출한다.
final var PATH = "/local-counter";
TestHttpUtils.send(PATH);
TestHttpUtils.send(PATH);
final var response = TestHttpUtils.send(PATH);
// 톰캣 서버 종료
tomcatStarter.stop();
assertThat(response.statusCode()).isEqualTo(200);
// expected를 0이 아닌 올바른 값으로 바꿔보자.
// 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까?
// Local scope에서 변수를 초기화 하고 increase 하기 때문에 해당 변수는 몇 번을 실행하든 호출이 일어났다면 1이 나오게된다.
assertThat(Integer.parseInt(response.body())).isEqualTo(1);
}
당연히 로컬 변수는 공유하지 않기 때문에 몇 번을 호출하더라도 1이 나올것이다.
@WebFilter("/*")
public class CharacterEncodingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
request.getServletContext().log("doFilter() 호출");
response.setCharacterEncoding("UTF-8");
chain.doFilter(request, response);
}
왜 인코딩을 따로 설정해야 하며, 어떻게 해야 하는지 알아볼 수 있는 코드이다. 해당 공식 문서를 읽어 보면 인코딩 방법을 명시 하지 않으면 기본적으로 ISO-8859를 사용한다고 한다.
그렇다면 특정 문자에 대해서는 디코딩이 이루어지지 않아 외계어를 볼 수 있기 때문에 세계 공용인 UTF-8로 인코딩 하여 반환 값을 검증했다.
Thread
/**
* 1. ThreadApp 클래스의 애플리케이션을 실행시켜 서버를 띄운다.
* 2. ThreadAppTest를 실행시킨다.
* 3. ThreadAppTest가 아닌, ThreadApp의 콘솔에서 SampleController가 생성한 http call count 로그를 확인한다.
* 4. application.yml에서 설정값을 변경해보면서 어떤 차이점이 있는지 분석해본다.
*/
public class ThreadAppTest {
private static final AtomicInteger count = new AtomicInteger(0);
@Test
void test() throws Exception {
final var NUMBER_OF_THREAD = 10;
var threads = new Thread[NUMBER_OF_THREAD]; // Thread 10개 생성
for (int i = 0; i < NUMBER_OF_THREAD; i++) {
// NOTE: thread 0 번 부터 순차적으로 /test URL을 호출
threads[i] = new Thread(() -> incrementIfOk(TestHttpUtils.send("/test")));
}
System.out.println("threads = " + threads.length);
for (final var thread : threads) {
thread.start();
Thread.sleep(50);
}
for (final var thread : threads) {
thread.join();
}
assertThat(count.intValue()).isEqualTo(2);
}
private static void incrementIfOk(final HttpResponse<String> response) {
if (response.statusCode() == 200) {
count.incrementAndGet();
}
}
//TODO: 동기화를 이용해서 쓰레드 간섭을 해결해보세요.
@Test
void synchronizedTest() throws InterruptedException {
var executorService = Executors.newFixedThreadPool(3);
var counter = new Counter();
IntStream.range(0, 1_000).forEach(count -> executorService.submit(counter::calculate));
executorService.awaitTermination(500, TimeUnit.MILLISECONDS);
assertThat(counter.getInstanceValue()).isEqualTo(1_000);
}
synchronized public void calculate() { // 동기화 사용
setInstanceValue(getInstanceValue() + 1);
}
@Test
void newFixedThreadPoolTest() {
// NOTE: 2개의 고정 쓰레드를 생성
final var executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
// NOTE: 고정 쓰레드에서 2개를 할당하고 나머지 1개는 큐에 저장
executor.submit(log("fixed thread pools"));
executor.submit(log("fixed thread pools"));
executor.submit(log("fixed thread pools"));
final int expectedPoolSize = 2;
final int expectedQueueSize = 1;
// NOTE: 결론적으로 고정 쓰레드를 생성한만큼 할당하고 남은 만큼은 큐에 담아둔다.
assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize());
assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size());
}
Last updated