spring boot 가상 스레드 적용해보기
JDK 21부터 가상 스레드(virtual thread)가 정식 릴리즈 되었다. 또한 spring boot 3.2부터는 가상 스레드가 지원되어 spring boot 애플리케이션에서 가상 스레드를 시험해 볼 수 있게 되었다. 이번 포스팅에서는 spring boot 웹 애플리케이션에서 가상 스레드를 적용한 샘플코드를 작성해 보고 jmeter를 통해서 가상 스레드와 플랫폼 스레드의 처리량의 차이를 비교해 보고자 한다.
가상 스레드에 대한 내용은 다음 포스팅에서 정리하였다.
2023.11.24 - [자바] - java 21 처리량 향상을 위한 대안 - virtual thread 알아보자
2023.11.26 - [자바] - java21 - scoped value에 대해서 알아보자
사전 조건
virtual thread를 적용하기 위해서는 JDK 21을 설치해야 한다.
intellij IDE 혹은 SKDMAN을 통해서 손쉽게 JDK 21 버전을 설치할 수 있다.
프로젝트에는 JDK 21을 SDK 버전으로 설정해야 한다.
SDKMAN에 대해서는 다음 포스팅을 참고하기 바란다.
2023.09.02 - [분류 전체보기] - SDKMAN - 개발 도구 손쉽게 관리하기
샘플코드
virtual thread 적용을 위한 configuration
내장 톰캣에서 사용하는 스레드를 virtual thread로 적용하기 위해서는 다음과 같이 configuration을 생성한다.
package com.example.spring.virtualthread.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.support.TaskExecutorAdapter;
import java.util.concurrent.Executors;
@Configuration
@ConditionalOnProperty(
value = "application.use-virtual",
havingValue = "true")
public class ThreadConfig {
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
public AsyncTaskExecutor asyncTaskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler ->
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
}
}
@ConditionalOnProperty 어노테이션을 통해서 application.use-virtual=true 인 경우에 가상 스레드가 활성화된다.
application.use-virtual=false인 경우에는 전통적인 플랫폼 스레드가 사용된다.
application:
# false인 경우 플랫폼 스레드 사용
use-virtual: true
위와 같이 @Configuration 클래스 작성 필요 없이 다음과 같이 설정 만으로도 virtual thread를 활성화 할 수 있다.
spring.threads.virtual.enabled=true (가상 스레드 활성화)
spring.threads.virtual.enabled=false (가상 스레드 비활성화)
Service
단순히 blocking I/O를 수행하는 역할을 하는 코드로 5초간 sleep을 주었다.
package com.example.spring.virtualthread.service;
import org.springframework.stereotype.Service;
@Service
public class BlockingIOService {
public void blocking() throws InterruptedException {
Thread.sleep(5000);
}
}
controller
GET /thread 호출을 통해 가상 스레드 여부와 스레드 정보를 응답하는 단순한 API를 하나 만들었다.
package com.example.spring.virtualthread.controller;
import com.example.spring.virtualthread.service.BlockingIOService;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/thread")
@RequiredArgsConstructor
public class ThreadController {
private final BlockingIOService blockingIOService;
@GetMapping
public ResponseEntity<ThreadInfo> getThreadInfo() throws InterruptedException {
blockingIOService.blocking();
return ResponseEntity.ok(
new ThreadInfo(
Thread.currentThread().isVirtual(),
Thread.currentThread().toString())
);
}
public record ThreadInfo(boolean isVirtual, String threadName) {}
}
API 호출 테스트
application.use-virtual=true 인 경우에 /thread 호출은 다음과 같이 응답하여 가상 스레드가 사용됨을 확인할 수 있다.
{
"isVirtual": true,
"threadName": "VirtualThread[#57]/runnable@ForkJoinPool-1-worker-2"
}
application.use-virtual=false인 경우에 /thread 호출은 다음과 같이 응답하여 플랫폼 스레드가 사용됨을 확인할 수 있다.
{
"isVirtual": false,
"threadName": "Thread[#45,http-nio-8080-exec-2,5,main]"
}
처리량 테스트
jmeter를 통해서 가상 스레드와 플랫폼 스레드의 처리량을 비교해 보자.
맥 m1, m2 환경에서 jmeter를 설치하는 방법은 아래 포스팅에 정리하였다.
2023.12.02 - [ETC] - 맥 m1, m2에 jmeter 사용하기
jmeter 스레드 그룹 설정
1000 개의 스레드를 생성하여 각 스레드가 100초간 반복 호출하도록 설정하였다.
http 요청 설정
GET http://localhost:8080/thread API를 호출하도록 설정한다.
가상 스레드 적용 처리량
애플리케이션의 application.use-virtual=true로 설정 후 애플리케이션을 기동 후 jmeter 테스트를 수행한 결과는 다음과 같다.
가상 스레드의 경우에는 초당 185.6건의 처리량을 보였다.
가상 스레드의 경우 모든 응답 시간은 균일하게 5초가 소요되었다.
제약 없이 스레드를 생성하여 처리할 수 있기 때문에 blocking I/O 작업 동안 다른 스레드의 대기가 발생하지 않기 때문이다.
플랫폼 스레드 적용 처리량
플랫폼 스레드의 경우에는 별도의 thread pool 개수 설정을 하지 않았으므로 디폴트 값이 200개로 수행된다.
애플리케이션의 application.use-virtual=false로 설정 후 애플리케이션을 기동 후 jmeter 테스트를 수행한 결과는 다음과 같다.
플랫폼 스레드의 경우에는 초당 39.3건의 처리량을 보였다.
200개의 스레드로 제한되어 있기 때문에 blocking I/O 작업 동안 다른 스레드에서의 대기가 발생하기 때문이다.
baeldung 사이트의 글과 비교했을 때 가상 스레드와 플랫폼 스레드의 응답 시간 그래프 모양은 동일하다.
간단한 샘플 코드를 통해서 가상 스레드와 플랫폼 스레드 간의 처리량의 차이를 확인해 보았다. 빈번하게 blocking I/O가 발생하는 애플리케이션에 가상 스레드를 적용하면 확실하게 처리량의 향상을 얻을 수 있을 것으로 기대한다.
참고 링크