From 510fa37fadfc86c258ef7e1c4ad158dae46568af Mon Sep 17 00:00:00 2001 From: jihukimme Date: Thu, 12 Feb 2026 20:00:47 +0900 Subject: [PATCH 1/2] refactor: replace ThreadPoolTaskExecutor with SimpleAsyncTaskExecutor using virtual threads --- .../icebang/global/config/asnyc/AsyncConfig.java | 14 ++++++-------- .../src/main/resources/application.yml | 3 +++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/global/config/asnyc/AsyncConfig.java b/apps/user-service/src/main/java/site/icebang/global/config/asnyc/AsyncConfig.java index 03fa1496..48506ab7 100644 --- a/apps/user-service/src/main/java/site/icebang/global/config/asnyc/AsyncConfig.java +++ b/apps/user-service/src/main/java/site/icebang/global/config/asnyc/AsyncConfig.java @@ -1,14 +1,15 @@ package site.icebang.global.config.asnyc; import java.lang.reflect.Method; +import java.util.concurrent.Executor; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.support.ContextPropagatingTaskDecorator; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import lombok.extern.slf4j.Slf4j; @@ -18,14 +19,11 @@ public class AsyncConfig implements AsyncConfigurer { @Bean("traceExecutor") - public ThreadPoolTaskExecutor traceExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(10); - executor.setMaxPoolSize(50); - executor.setQueueCapacity(100); - executor.setTaskDecorator(new ContextPropagatingTaskDecorator()); // 필수 + public Executor traceExecutor() { + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(); + executor.setVirtualThreads(true); executor.setThreadNamePrefix("trace-"); - executor.initialize(); + executor.setTaskDecorator(new ContextPropagatingTaskDecorator()); // 컨텍스트(MDC 등) 전파 설정 return executor; } diff --git a/apps/user-service/src/main/resources/application.yml b/apps/user-service/src/main/resources/application.yml index f6302bc7..70a99661 100644 --- a/apps/user-service/src/main/resources/application.yml +++ b/apps/user-service/src/main/resources/application.yml @@ -1,6 +1,9 @@ spring: application: name: mvp + threads: + virtual: + enabled: true profiles: active: develop test: From 92032d176ecd4bdceabf5edfa60bae558df43154 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Fri, 13 Feb 2026 18:20:20 +0900 Subject: [PATCH 2/2] refactor: add SemaphoreTaskDecorator for controlling async task concurrency --- .../config/{asnyc => async}/AsyncConfig.java | 17 +++++++- .../config/async/SemaphoreTaskDecorator.java | 41 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) rename apps/user-service/src/main/java/site/icebang/global/config/{asnyc => async}/AsyncConfig.java (70%) create mode 100644 apps/user-service/src/main/java/site/icebang/global/config/async/SemaphoreTaskDecorator.java diff --git a/apps/user-service/src/main/java/site/icebang/global/config/asnyc/AsyncConfig.java b/apps/user-service/src/main/java/site/icebang/global/config/async/AsyncConfig.java similarity index 70% rename from apps/user-service/src/main/java/site/icebang/global/config/asnyc/AsyncConfig.java rename to apps/user-service/src/main/java/site/icebang/global/config/async/AsyncConfig.java index 48506ab7..ac059905 100644 --- a/apps/user-service/src/main/java/site/icebang/global/config/asnyc/AsyncConfig.java +++ b/apps/user-service/src/main/java/site/icebang/global/config/async/AsyncConfig.java @@ -1,4 +1,4 @@ -package site.icebang.global.config.asnyc; +package site.icebang.global.config.async; import java.lang.reflect.Method; import java.util.concurrent.Executor; @@ -7,23 +7,36 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.support.ContextPropagatingTaskDecorator; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.annotation.EnableAsync; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @Configuration @EnableAsync +@RequiredArgsConstructor public class AsyncConfig implements AsyncConfigurer { + private final SemaphoreTaskDecorator semaphoreTaskDecorator; + @Bean("traceExecutor") public Executor traceExecutor() { SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(); executor.setVirtualThreads(true); executor.setThreadNamePrefix("trace-"); - executor.setTaskDecorator(new ContextPropagatingTaskDecorator()); // 컨텍스트(MDC 등) 전파 설정 + + // MDC 전파 데코레이터 생성 + TaskDecorator contextDecorator = new ContextPropagatingTaskDecorator(); + + // 두 데코레이터의 조합: + // Context 설정(MDC 복사) 후 Semaphore 제어가 적용되도록 구성 + executor.setTaskDecorator( + runnable -> contextDecorator.decorate(semaphoreTaskDecorator.decorate(runnable))); + return executor; } diff --git a/apps/user-service/src/main/java/site/icebang/global/config/async/SemaphoreTaskDecorator.java b/apps/user-service/src/main/java/site/icebang/global/config/async/SemaphoreTaskDecorator.java new file mode 100644 index 00000000..e6364bb3 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/global/config/async/SemaphoreTaskDecorator.java @@ -0,0 +1,41 @@ +package site.icebang.global.config.async; + +import java.util.concurrent.Semaphore; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.task.TaskDecorator; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class SemaphoreTaskDecorator implements TaskDecorator { + + @Value("${spring.datasource.hikari.maximum-pool-size:10}") + private int maximumPoolSize; + + private Semaphore semaphore; + + @PostConstruct + public void init() { + this.semaphore = new Semaphore(maximumPoolSize); + log.info("SemaphoreTaskDecorator 초기화: 동시 실행 제한 수(maximumPoolSize) = {}", maximumPoolSize); + } + + @Override + public Runnable decorate(Runnable runnable) { + return () -> { + try { + semaphore.acquire(); + runnable.run(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("비동기 작업 실행 대기 중 인터럽트 발생", e); + } finally { + semaphore.release(); + } + }; + } +}