스프링 부트에서 가상 스레드를 지원하기 위한 작업
가상 스레드 사용 여부 프로퍼티 추가
- spring-boot-autoconfigure 모듈의 spring-configuration-metadata.json에 가상 스레드를 지원하기 위한 프로퍼티가 추가되었다.
{
"name" : "spring.threads.virtual.enabled",
"type" : "java.lang.Boolean",
"description": "Whether to use virtual threads.",
"defaultValue": false
},
- 따라서 해당 프로퍼티 값을 true로 설정하면 스프링 부트에서 가상 스레드를 사용할 수 있다.
spring.threads.virtual.enabled=true
스레드 모델 Enum과 Condition 어노테이션 추가
- 가상 스레드를 지원함에 따라 스레드 모델은 가상 스레드와 플랫폼 스레드로 나뉘게 되었다.
- 따라서 둘을 구분하기 위한 Enum 클래스가 추가되었다.
public enum Threading {
PLATFORM {
@Override
public boolean isActive(Environment environment) {
return !VIRTUAL.isActive(environment);
}
},
VIRTUAL {
@Override
public boolean isActive(Environment environment) {
return environment.getProperty("spring.threads.virtual.enabled", boolean.class, false)
&& JavaVersion.getJavaVersion().isEqualOrNewerThan(JavaVersion.TWENTY_ONE);
}
};
public abstract boolean isActive(Environment environment);
}
- 또한, 스레드 모델 조건에 따라 빈을 다르게 생성하기 위한 Condition 어노테이션과 구현체도 추가되었다.
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnThreadingCondition.class)
public @interface ConditionalOnThreading {
Threading value();
}
가상 스레드 사용 여부에 따른 빈 등록
- 멀티 스레드를 사용하는 부분을 가상 스레드로 교체
- 스프링의 톰캣 자동 구성 클래스의 일부
@AutoConfiguration
@ConditionalOnNotWarDeployment
@ConditionalOnWebApplication
@EnableConfigurationProperties(ServerProperties.class)
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })
public static class TomcatWebServerFactoryCustomizerConfiguration {
...
**// 가상 스레드 여부에 따라 가상 스레드 기반의 웹서버를 빈으로 등록
@Bean
@ConditionalOnThreading(Threading.VIRTUAL)
TomcatVirtualThreadsWebServerFactoryCustomizer tomcatVirtualThreadsProtocolHandlerCustomizer() {
return new TomcatVirtualThreadsWebServerFactoryCustomizer();
}**
}
...
}
- 톰캣에서 가상 스레드를 사용하는 VirtualThreadExecutor을 추가
- 기존에는 스레드 풀에 미리 스레드를 생성해 두고 재사용하는 ThreadPoolExecutor을 사용
public class VirtualThreadExecutor implements Executor {
private final JreCompat jreCompat = JreCompat.getInstance();
private Object threadBuilder;
public VirtualThreadExecutor(String namePrefix) {
threadBuilder = jreCompat.createVirtualThreadBuilder(namePrefix);
}
@Override
public void execute(Runnable command) {
jreCompat.threadBuilderStart(threadBuilder, command);
}
}
Spring Boot Virtual Thread 적용기
- 주의 사항
- 간단한 blocking/synchronous 코드로 넘어가기
- 플랫폼 스레드를 가상 스레드로 바꾸는 것이 아니라 태스크(Task)를 가상 스레드로 바꾸기
- 동시성 제한을 위해 스레드 풀이 아닌 세마포어와 같은 기술을 사용하기
- 스레드 로컬에 무거운 객체를 캐시 하지 않기
- 멀티 스레드가 사용되는 부분
- 멀티 스레드 요청을 처리하는 웹 서버
- @Async 사용을 위한 TaskExecutor
- @Schedule 사용을 위한 TaskScheduler
- Redis, Kafka, RabbitMQ 등
전체 Virtual Thread 적용
- Spring Boot 3.2 이상
spring:
threads:
virtual:
enabled: true
- Spring Boot 3.x
// Web Request 를 처리하는 Tomcat 이 Virtual Thread를 사용하여 유입된 요청을 처리하도록 한다.
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer()
{
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
// Async Task에 Virtual Thread 사용
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
public AsyncTaskExecutor asyncTaskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
✅ 문제점: Thread Blocking 이 발생하지 않는 경우 Platform Thread 가 더 처리량이 높다. (Virtual Thread Scheduling을 위한 오버헤드의 영향)
⇒ 따라서 전체에 Virtual Thread를 적용하지 말고, 개별 작업에 가상 스레드를 할당하는 형태로 한다.
[1000 개의 데이터 GET 요청]
- Virtual Thread


- Platform Thread


=> 플랫폼 스레드 처리량 8.9/sec, 가상 스레드 처리량 7.91/sec
비동기 처리에만 Virtual Thread 적용
- AsyncConfig.java
- bean 이름 지정
- VirtualThreadTaskExecutor 사용
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "virtualExecutor")
public Executor threadPoolTaskExecutor(){
VirtualThreadTaskExecutor taskExecutor = new VirtualThreadTaskExecutor();
return taskExecutor;
}
}
- ServiceImpl.java
- 비동기 처리할 메서드에 @Async("virtualExecutor") 적용
- N개의 요청이 왔을 때 각각에 대하여 비동기 처리
@Override
@Async("virtualExecutor")
@KafkaListener(topics = "${kafka.topic}", groupId = "${spring.kafka.consumer.group-id}")
public void registerFood(FoodRegisterReqList foodRegisterReqList) {
Optional<Refrigerator> refrigerator = refrigeratorRepository.findById(
foodRegisterReqList.refrigeratorId());
log.debug("registerFood method start : {} ", Thread.currentThread().toString());
if (refrigerator.isEmpty()) {
log.error("registerFood method refrigerator: {}이 없습니다.",
foodRegisterReqList.refrigeratorId());
throw new BaseExceptionHandler(ErrorCode.NOT_FOUND_REFRIGERATOR_EXCEPTION);
}
for (FoodRegisterReq foodRegisterReq : foodRegisterReqList.foodList()) {
asyncRegisterFood(refrigerator.get(), foodRegisterReq);
}
log.debug("registerFood method success : {} ", Thread.currentThread().toString());
}
[1000개의 POST 요청]
- Virtual Thread


- Platform Thread(max pool : 10, core pool: 3, queue capacity: 100,000)


- 가상 스레드 처리량 20.2/sec, 플랫폼 스레드 처리량 10.9/sec
⇒ 가상 스레드의 처리량이 2배 더 높고, 응답시간도 대체적으로 낮음
스케줄러에 Virtual Thread 적용
- 모든 스케줄러에 Virtual Thread 적용
@Configuration
@EnableScheduling
public class SchedulingConfig {
@Bean
public TaskScheduler taskScheduler() {
return new ConcurrentTaskScheduler(
Executors.newScheduledThreadPool(0, Thread.ofVirtual().factory())
);
}
}
- 스케줄러 설정 + 비동기 실행
@Component
@Slf4j
public class testScheduler {
@Async("virtualExecutor")
@Scheduled(fixedRate = 1000)
public void myScheduledTask() {
log.info("{} ", Thread.currentThread().toString());
}
}'BackEnd > SpringBoot' 카테고리의 다른 글
| [SpringBoot] ElasticSearch 연동 (0) | 2024.05.29 |
|---|