[Spring Boot] Virtual Thread 적용

스프링 부트에서 가상 스레드를 지원하기 위한 작업

가상 스레드 사용 여부 프로퍼티 추가

  • 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 적용기

  • 주의 사항
    1. 간단한 blocking/synchronous 코드로 넘어가기
    2. 플랫폼 스레드를 가상 스레드로 바꾸는 것이 아니라 태스크(Task)를 가상 스레드로 바꾸기
    3. 동시성 제한을 위해 스레드 풀이 아닌 세마포어와 같은 기술을 사용하기
    4. 스레드 로컬에 무거운 객체를 캐시 하지 않기
  • 멀티 스레드가 사용되는 부분
    • 멀티 스레드 요청을 처리하는 웹 서버
    • @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

Virtual Thread 적용시 응답시간
Virtual Thread 적용시 API 처리량

  • Platform Thread

Platform 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

Virtual Thread 응답시간
Virtual Thread 처리량

 

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

Platform Thread 응답시간
Platform Thread 처리량

  • 가상 스레드 처리량 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