Java 21 신규기능 virtual thread를 스프링에 적용해보자!

Jinseok Lee·2023년 12월 20일
1

사연

java21 버전에서 virtual thread라는 기능이 추가가 되었는데 기존의 플랫폼 쓰레드 방식과 다르게 jvm에서 독립적으로 non-blocking 하게 실행되어 리액티브 프로그래밍의 이점을 얻을 수 있다하여 spring boot에 적용하고 성능 테스트를 해보고 싶었다.

설정

먼저 나의 개발환경이다.

  • java 21
  • gradle 8.5
  • spring boot 3.2.0
spring:
  threads:
    virtual:
      enabled: true

application.yml에 vitual thread를 사용할 것이라고 명시해준다
이 설정을 해주면 내부적으로는 spring embedded tomcat이 기본적으로 thread-per-request로 기존의 OS의 쓰레드를 활용하는 플랫폼 쓰레드를 사용하는데 해당 설정을 해주면 os에 독립적인 jvm내에 virtual thread를 사용하도록 설정이 된다.

spring boot version이 3.2.x보다 낮으면 bean을 따로 등록해줘야한다고 한다. (관련문서)

@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
  return protocolHandler -> {
    protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
  };
}

테스트

controller

@RestController
@RequestMapping("/hello")
public class HelloController {

    @GetMapping
    public String hello() throws InterruptedException {
        Thread.sleep(700);
        return "hello";
    }
    
}

test code

class ApiApplicationTest {

    public ClientHttpRequestInterceptor clientHttpRequestInterceptor() {
        return (request, body, execution) -> {
            RetryTemplate retryTemplate = new RetryTemplate();
            retryTemplate.setRetryPolicy(new SimpleRetryPolicy(3));
            try {
                return retryTemplate.execute(context -> execution.execute(request, body));
            } catch (Throwable throwable) {
                throw new RuntimeException(throwable);
            }
        };
    }
    

    @Test
    void 동시요청_성능_테스트() throws InterruptedException {
        long beforeTime = System.currentTimeMillis();

        RestTemplate restTemplate = new RestTemplateBuilder()
                .setConnectTimeout(Duration.ofSeconds(10))
                .setReadTimeout(Duration.ofSeconds(10))
                .interceptors(clientHttpRequestInterceptor())
                .build();


        int numberOfThreads = 2000;
        ExecutorService service = Executors.newVirtualThreadPerTaskExecutor();
        CountDownLatch latch = new CountDownLatch(numberOfThreads);

        for (int i = 0; i < numberOfThreads; i++) {
            service.execute(() -> {
                try {
                    restTemplate.getForObject(
                            "http://localhost:8080/hello",
                            String.class
                    );
                } catch (Exception e) {
                    e.printStackTrace();
                }

                latch.countDown();
            });
        }
        latch.await();

        long afterTime = System.currentTimeMillis();
        long secDiffTime = (afterTime - beforeTime);
        System.out.println("소요시간 (ms) : " + secDiffTime);
    }
}

테스트 결과

버추얼 쓰레드를 사용하지 않았을때는 2,000개 동시 요청시 7,471ms가 걸렸고 버추얼 쓰레드를 사용해서 웹서버를 구동시키고 테스트를 했을때는 1,499ms 정도로 유의미한 수준의 성능 변화가 있었다.

그런데 여기에서 Thread.sleep()을 걸지 않았을때에는 큰 차이가 발생하지 않았다. 무엇인가 blocking이 있는 로직을 처리할때 성능 향상을 기대해 볼 수 있을 것으로 보인다.

정리

이미지 출처: https://findstar.pe.kr/2023/04/17/java-virtual-threads-1/

아직 버추얼 쓰레드의 패러다임에 대해서 명확하게 이해하기는 어렵지만, 기존의 플랫폼 쓰레드의 경우에는 OS의 쓰레드를 사용했고 그래서 관리하는데 비용이 많이 들었는데 버추얼 쓰레드는 그 기능을 jvm에서 독립적으로 가능하게해서 논블로킹으로 처리가 가능한 쓰레드를 개발한 것으로 보인다. 해당 기능은 Project Loom이라는 프로젝트로 부터 기인한 것이라고 하는데 관심 있는 분들은 찾아보면 도움이 될거 같다.

결론적으로 일단 블로킹이 발생하는 요청의 경우 버추얼 쓰레드로 동작이 되는 것이 성능 향상에 도움이 되는 것 같고 기존의 webflux와 같은 리액티브 프로그래밍의 경우 디버깅에 문제가 있는 것으로 알고 있는데 해당 기능은 java jvm에서 독립적으로 제공하는 기능이다 보니 메모리 덤프도 가능하고 여러가지로 java 진영에서 리액티브한 프로그래밍을 해야하는 경우 표준으로 자리잡을 것으로 보인다.

참고

https://findstar.pe.kr/2023/04/17/java-virtual-threads-1/
https://spring.io/blog/2022/10/11/embracing-virtual-threads

profile
전 위메프, 이직준비중

0개의 댓글