✌️
Studylog
See More
Studylog
Studylog
  • INTRO
  • AWS
    • AWS101
      • Virtualization and the AWS structures
      • AWS account and free-tier
      • AWS IAM
      • AWS EC2
        • EC2 basic
        • ENI and EIP
        • Elastic Block Storage
        • Auto Scaling
        • Elastic Load Balancer
  • IaC
    • Terraform
      • License Change
      • Tutorial
      • Module
      • Versioning
  • Airflow
    • Airflow-Ninja
      • Introduction and Goal
      • Tutorial: Settings
      • Tutorial: Module Import, Alert
  • Docker
    • Production with Docker
      • Customizing root directory
  • Network
    • Network-Handbook
      • Introduction and Goal
      • OSI7Layer
      • DNS
      • SSL/TLS
  • Comupter Architecture
    • Basic
      • Introduction and Goal
      • Chapter 1. The Internal Language of Computers
      • Chapter 2. Combinatorial Logic
      • Chapter 3. The Essentials of Memory and Disk Sequential Logic
    • Hands-on
      • Introduction and Goal
      • theory
        • Chapter 1. Logic Gates
        • Chapter 2. ALU
      • project
        • Project 1. Elementary logic gates implement
        • Project 2. Boolean Arithmetic
  • Java
    • Readable Code
      • 학습 목적
      • 추상화
      • 논리적 사고 흐름
      • 객체지향 이론
      • 객체지향 코드 적용하기
      • 코드 다듬기
      • 읽기 좋은 코드를 도와줄 조언들
  • Spring Boot
    • Practical Testing
      • 테스트 사전 지식
      • 스프링 레이어드 아키텍처 테스트하기
        • Persistence Layer
        • Business Layer
    • 스프링 핵심 원리 - 기본편
      • 객체 지향 설계와 스프링
      • 스프링 핵심 원리 이해
        • 예제 만들기
        • 객체 지향 원리 적용
        • 스프링 컨테이너와 스프링 빈
  • Python
    • Effective Python
      • Introduction and Goal
      • Closure: Lazy Evaluation And Eager Evaluation
      • Python public attributes are better getter-setter
      • No refactoring attribute, we can use property decorator
      • You can do it, but it means you don't have to
  • Playgrounds
    • Java Playground
      • 학습 목적
      • 숫자 야구 게임
        • 학습 테스트
        • 문자열 계산기
        • 숫자 야구 게임 구현
        • 숫자 야구 게임 다시 구현하기
      • 자동차 경주
        • 문자열 덧셈 계산기
        • 자동차 경주 미션 구현
      • 좌표 계산기
        • 연료 주입
        • 좌표 계산기 미션 구현
    • Infra Playground
      • VPC: 망분리 그리고 테스트
      • 컨테이너 사전 지식
      • 화면 성능 개선 전 학습 테스트
      • SSM: Session Manager
      • SQL, 이 정도는 알아야지 😎
      • Subway-Map
        • 망 구성하기
        • 서버 구성하기
        • 화면 성능 개선하기
      • Conference Platform
        • 망 구성하기
        • 서버 구성하기
        • 화면 성능 개선하기
  • Tools
    • SOPS
    • Bruno
    • 🖥️FCK-NAT
    • 🧊Pulumi
Powered by GitBook
On this page
  • 학습 테스트
  • HTTP Cache
  • Servlet
  • Thread
  1. Playgrounds
  2. Infra Playground

화면 성능 개선 전 학습 테스트

HTTP Cache, gzip,Servlet,Thread에 대한 학습 테스트를 진행 합니다.

Previous컨테이너 사전 지식NextSSM: Session Manager

Last updated 4 months ago

학습 테스트

HTTP Cache


Cache-Control 관리하기

  • HTTP 응답 헤더에 Cache-Control 이 없으면 웹 브라우저가 휴리스틱 캐싱에 따른 암시적 캐싱을 한다.

  • 의도하지 않은 캐싱을 막기 위해 모든 응답의 헤더에 Cache-Control: no-cache를 명시한다.

  • 또한, 쿠키나 사용자 개인 정보 유출을 막기 위해 private도 추가한다.

휴리스틱 캐싱에 대해 자세히 알아보려면 에 분석 사례 확인하기

@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);
    }

모든 응답에 대해 처리 해야 했기 때문에 특정 컨트롤러에서 헤더에 정보를 추가하지 않고 설정 파일에서 캐시 값을 적용하도록 수정하였다.

HTTP 압축 알고리즘 적용 with gzip

  • 스프링 부트 설정을 통해 gzip과 같은 HTTP 압축 알고리즘을 적용 시킬 수 있다.

  • 스프링에서 데이터를 압축할 때 기본 2KB부터 적용 되기 때문에 작은 크기의 파일은 압축하지 않는다. 작은 크기의 파일도 압축할 수 있도록 기본 설정을 10으로 변경한다.

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"]

E-Tag 와 If-None-Match 를 활용한 HTTP 조건부 요청 캐싱

  • 필터를 활용하여 "/etag" 경로도 ETag를 적용해보자

@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"]

캐시 무효화

컨텐츠가 변경될 때 URL을 변경하여 응답을 장기간 캐시하게 만드는 기술이다.

정적 리소스를 max-age: 1년으로 설정한 후 JS나 CSS 리소스에 변경사항이 생기면 캐시가 제거되도록 할 때 사용하는 방식이다.

@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이 나올것이다.

서블릿의 필터를 활용하여 인코딩 값 검증하기

  • 서블릿이 생성 되고 HTTP로 넘어온 데이터를 파싱하고 서비스 로직을 호출하고 프로세스를 종료하는 과정들을 살펴 보며 인코딩 된 값을 어떻게 처리하는지 확인한다.

@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();
        }
    }
server:
  tomcat:
    accept-count: 100
    max-connections: 2
    threads:
      max: 10
  • accept-count: max-connections가 초과 했을 때 ThreadQueue 에 저장될 최대 개수

  • max-connections: 서버가 동시에 처리할 수 있는 연결의 개수 제한

  • threads.max: 워커 스레드의 최대 스레드 수

Thread 동기화 및 Thread-Safe

  1. 동기화 되지 않아 예상 개수보다 적게 생성 되는 경쟁 상태를 동기화로 풀어주기

  2. syncronized 예약어를 사용하지 않고 Thread-Safe한 코드 생성하기

//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);
}
//TODO: synchronized 예약어를 사용하지 않고 Thread safe하게 구성해보세요
@Test
void synchronizedTest2() throws InterruptedException {
    var executorService = Executors.newFixedThreadPool(3);
    var servlet = new TestServlet();

    IntStream.range(0, 1_000).forEach(count -> executorService.submit(() -> servlet.join(new User("cu"))));


    executorService.awaitTermination(1_000, TimeUnit.MILLISECONDS);
    assertThat(servlet.getUsers().size()).isEqualTo(1);
}

private static final class TestServlet {
    private static final Long LATENCY = 200L;

    private List<User> users = new ArrayList<>();

    public void join(User user) {
        lock.lock(); // Mutex Lock 사용
        if (!users.contains(user)) {
            sleep();
            users.add(user);
        }
        lock.unlock();
    }

    private static void sleep() {
        if (condition()) {
            try {
                Thread.sleep(LATENCY);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

Thread Pool에 따른 Queue 적재 방식

  1. newFixedThreadPool 이 Queue에 저장하는 쓰레드 개수 검증하기

  2. newCachedThreadPool 이 Queue에 저장하는 쓰레드 개수 검증하기

@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());
}
@Test
void newCachedThreadPoolTest() {
    // NOTE: 캐시 쓰레드 풀을 생성
    final var executor = (ThreadPoolExecutor) Executors.newCachedThreadPool();

    // NOTE: 캐시 쓰레드는 이미 사용중인 쓰레드가 있다면 새롭게 생성, 사용하지 않는다면(60초 동안) 리소스 제거
    executor.submit(log("cached thread pools"));
    executor.submit(log("cached thread pools"));
    executor.submit(log("cached thread pools"));

    final int expectedPoolSize = 3;
    final int expectedQueueSize = 0; //TODO: 알맞은 값으로 변경해보세요.

    // NOTE: 결론적으로 3개의 쓰레드가 새롭게 생성되서 할당되고 큐에 담기지 않음
    assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize());
    assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size());
}

깃허브 학습 테스트 자료
학습 테스트 풀이 자료
해당 블로그
Spring 공식문서
공식문서 참고 자료