diff --git a/apps/user-service/build.gradle b/apps/user-service/build.gradle index cf0b538b..afdfac89 100644 --- a/apps/user-service/build.gradle +++ b/apps/user-service/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.5.4' + id 'org.springframework.boot' version '3.5.8' id 'io.spring.dependency-management' version '1.1.7' id 'com.diffplug.spotless' version '7.2.1' id 'org.asciidoctor.jvm.convert' version '3.3.2' 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 57% 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 03fa1496..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,31 +1,42 @@ -package site.icebang.global.config.asnyc; +package site.icebang.global.config.async; 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.TaskDecorator; 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.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @Configuration @EnableAsync +@RequiredArgsConstructor public class AsyncConfig implements AsyncConfigurer { + private final SemaphoreTaskDecorator semaphoreTaskDecorator; + @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(); + + // 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..57f993d0 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/global/config/async/SemaphoreTaskDecorator.java @@ -0,0 +1,48 @@ +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() { + int safetyBuffer = 5; + int taskConcurrencyLimit = Math.max(1, maximumPoolSize - safetyBuffer); + + this.semaphore = new Semaphore(taskConcurrencyLimit); + log.info( + "SemaphoreTaskDecorator 초기화: DB 풀({}) - 여유분({}) = 동시 실행 제한 수({})", + maximumPoolSize, + safetyBuffer, + taskConcurrencyLimit); + } + + @Override + public Runnable decorate(Runnable runnable) { + return () -> { + try { + semaphore.acquire(); + runnable.run(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("비동기 작업 실행 대기 중 인터럽트 발생", e); + } finally { + semaphore.release(); + } + }; + } +} diff --git a/apps/user-service/src/main/resources/application-develop.yml b/apps/user-service/src/main/resources/application-develop.yml index 49b275e0..a366c277 100644 --- a/apps/user-service/src/main/resources/application-develop.yml +++ b/apps/user-service/src/main/resources/application-develop.yml @@ -19,8 +19,8 @@ spring: connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 - maximum-pool-size: 10 - minimum-idle: 5 + maximum-pool-size: 30 + minimum-idle: 10 pool-name: HikariCP-MyBatis quartz: diff --git a/apps/user-service/src/main/resources/application-production.yml b/apps/user-service/src/main/resources/application-production.yml index 9463232d..068e05c4 100644 --- a/apps/user-service/src/main/resources/application-production.yml +++ b/apps/user-service/src/main/resources/application-production.yml @@ -13,8 +13,8 @@ spring: connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 - maximum-pool-size: 10 - minimum-idle: 5 + maximum-pool-size: 30 + minimum-idle: 10 pool-name: HikariCP-MyBatis # Gmail 연동 설정 @@ -39,7 +39,7 @@ spring: mybatis: mapper-locations: classpath:mybatis/mapper/**/*.xml - type-aliases-package: site.icebang.dto + type-aliases-package: site.icebang.domain configuration: map-underscore-to-camel-case: true diff --git a/apps/user-service/src/main/resources/application-test-e2e.yml b/apps/user-service/src/main/resources/application-test-e2e.yml index 14c572b1..a84c4b40 100644 --- a/apps/user-service/src/main/resources/application-test-e2e.yml +++ b/apps/user-service/src/main/resources/application-test-e2e.yml @@ -21,7 +21,7 @@ spring: mybatis: mapper-locations: classpath:mybatis/mapper/**/*.xml - type-aliases-package: site.icebang.dto + type-aliases-package: site.icebang.domain configuration: map-underscore-to-camel-case: true diff --git a/apps/user-service/src/main/resources/application-test-integration.yml b/apps/user-service/src/main/resources/application-test-integration.yml index 0bc7cbcc..dafcae05 100644 --- a/apps/user-service/src/main/resources/application-test-integration.yml +++ b/apps/user-service/src/main/resources/application-test-integration.yml @@ -36,7 +36,7 @@ spring: mybatis: mapper-locations: classpath:mybatis/mapper/**/*.xml - type-aliases-package: site.icebang.dto + type-aliases-package: site.icebang.domain configuration: map-underscore-to-camel-case: true diff --git a/apps/user-service/src/main/resources/application-test-unit.yml b/apps/user-service/src/main/resources/application-test-unit.yml index 1487e336..356fff91 100644 --- a/apps/user-service/src/main/resources/application-test-unit.yml +++ b/apps/user-service/src/main/resources/application-test-unit.yml @@ -37,7 +37,7 @@ spring: mybatis: mapper-locations: classpath:mybatis/mapper/**/*.xml - type-aliases-package: site.icebang.dto + type-aliases-package: site.icebang.domain configuration: map-underscore-to-camel-case: true