From 370e96d8514a5b6b7caf89da2a0e0b2cf92e37af Mon Sep 17 00:00:00 2001 From: gihhyeon Date: Sat, 12 Sep 2026 11:56:13 +0900 Subject: [PATCH 1/5] =?UTF-8?q?refactor:=20@Async=20=ED=92=80=EC=9D=84=20?= =?UTF-8?q?=EC=84=B1=EA=B2=A9=EB=B3=84=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EA=B3=A0=20=EA=B1=B0=EB=B6=80=EC=A0=95=EC=B1=85=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 외부 I/O와 내부 DB 작업을 별도 스레드 풀로 분리해 상호 간섭을 막는다. - Discord는 externalExecutor, 내부 작업(분석·조회수·응모)은 generalExecutor로 격리 - 풀 포화 시 예외가 요청으로 전파되지 않도록 거부정책 지정 - Hikari 기본 커넥션 풀(10)을 웹 요청과 나눠 쓰도록 general을 max 4로 제한 - 종료 시 진행 중 작업을 기다리도록 graceful shutdown 설정 --- .../event/RequestReviewEventListener.java | 2 +- .../analytics/AnalyticsEventListener.java | 2 +- .../common/config/AsyncConfiguration.java | 47 +++++++++++++++---- .../infra/discord/DiscordWebhookSender.java | 2 +- .../place/service/ViewCountService.java | 2 +- .../common/config/AsyncConfigurationTest.java | 37 +++++++++++++++ 6 files changed, 78 insertions(+), 14 deletions(-) create mode 100644 src/test/java/com/matzip/common/config/AsyncConfigurationTest.java diff --git a/src/main/java/com/matzip/admin/event/RequestReviewEventListener.java b/src/main/java/com/matzip/admin/event/RequestReviewEventListener.java index e93d9fe..359d271 100644 --- a/src/main/java/com/matzip/admin/event/RequestReviewEventListener.java +++ b/src/main/java/com/matzip/admin/event/RequestReviewEventListener.java @@ -28,7 +28,7 @@ public RequestReviewEventListener(LotteryEventService lotteryEventService, Place this.analyticsRecorder = analyticsRecorder; } - @Async + @Async("generalExecutor") @TransactionalEventListener(condition = "#event.reviewStatus().name() == 'APPROVED'") public void enterCurrentEventOnApproval(RequestReviewEvent event) { Place place = placeRepository.findById(event.placeId()) diff --git a/src/main/java/com/matzip/common/analytics/AnalyticsEventListener.java b/src/main/java/com/matzip/common/analytics/AnalyticsEventListener.java index c5c3664..9b1537e 100644 --- a/src/main/java/com/matzip/common/analytics/AnalyticsEventListener.java +++ b/src/main/java/com/matzip/common/analytics/AnalyticsEventListener.java @@ -24,7 +24,7 @@ public AnalyticsEventListener(EventLogRepository eventLogRepository) { this.eventLogRepository = eventLogRepository; } - @Async + @Async("generalExecutor") @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) public void on(AnalyticsEvent event) { try { diff --git a/src/main/java/com/matzip/common/config/AsyncConfiguration.java b/src/main/java/com/matzip/common/config/AsyncConfiguration.java index 9a78aee..dab18b1 100644 --- a/src/main/java/com/matzip/common/config/AsyncConfiguration.java +++ b/src/main/java/com/matzip/common/config/AsyncConfiguration.java @@ -1,30 +1,57 @@ package com.matzip.common.config; -import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +@Slf4j @EnableAsync @Configuration public class AsyncConfiguration implements AsyncConfigurer { + private static final int SHUTDOWN_AWAIT_SECONDS = 15; + @Override public Executor getAsyncExecutor() { - return asyncTaskExecutor(); + return generalExecutor(); + } + + @Bean + public ThreadPoolTaskExecutor generalExecutor() { + // Hikari 기본 커넥션 풀(10)을 웹 요청과 나눠 쓰도록 max 4로 제한 + return buildExecutor("async-", 3, 4, 200, loggingDiscardPolicy()); } @Bean - public TaskExecutor asyncTaskExecutor() { - return new ThreadPoolTaskExecutorBuilder() - .corePoolSize(10) - .maxPoolSize(50) - .queueCapacity(100) - .threadNamePrefix("Async-") - .build(); + public ThreadPoolTaskExecutor externalExecutor() { + return buildExecutor("ext-", 1, 2, 30, new ThreadPoolExecutor.DiscardPolicy()); + } + + private ThreadPoolTaskExecutor buildExecutor(String threadNamePrefix, int corePoolSize, int maxPoolSize, + int queueCapacity, RejectedExecutionHandler rejectedHandler) { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(corePoolSize); + executor.setMaxPoolSize(maxPoolSize); + executor.setQueueCapacity(queueCapacity); + executor.setThreadNamePrefix(threadNamePrefix); + executor.setRejectedExecutionHandler(rejectedHandler); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(SHUTDOWN_AWAIT_SECONDS); + executor.initialize(); + return executor; + } + + // ViewCountService가 REQUIRES_NEW라 CallerRunsPolicy는 커넥션 데드락 위험이 있어 log & discard 사용 + private RejectedExecutionHandler loggingDiscardPolicy() { + return (rejected, executor) -> + log.warn("[비동기 작업 유실] general 풀 포화로 작업을 버립니다. activeThreads={}, queueSize={}", + executor.getActiveCount(), executor.getQueue().size()); } } diff --git a/src/main/java/com/matzip/common/infra/discord/DiscordWebhookSender.java b/src/main/java/com/matzip/common/infra/discord/DiscordWebhookSender.java index 0fe8536..a1c9559 100644 --- a/src/main/java/com/matzip/common/infra/discord/DiscordWebhookSender.java +++ b/src/main/java/com/matzip/common/infra/discord/DiscordWebhookSender.java @@ -21,7 +21,7 @@ public DiscordWebhookSender(WebClient.Builder webClientBuilder, this.discordWebhookProperties = discordWebhookProperties; } - @Async + @Async("externalExecutor") public void sendAsync(String content) { String webhookUrl = discordWebhookProperties.getWebhookUrl(); if (!StringUtils.hasText(webhookUrl)) { diff --git a/src/main/java/com/matzip/place/service/ViewCountService.java b/src/main/java/com/matzip/place/service/ViewCountService.java index 295d963..07f73bf 100644 --- a/src/main/java/com/matzip/place/service/ViewCountService.java +++ b/src/main/java/com/matzip/place/service/ViewCountService.java @@ -18,7 +18,7 @@ public class ViewCountService { private final PlaceRepository placeRepository; private final DailyViewCountRepository dailyViewCountRepository; - @Async + @Async("generalExecutor") @Transactional(propagation = Propagation.REQUIRES_NEW) public void incrementAllCounts(Long placeId) { placeRepository.incrementViewCount(placeId); diff --git a/src/test/java/com/matzip/common/config/AsyncConfigurationTest.java b/src/test/java/com/matzip/common/config/AsyncConfigurationTest.java new file mode 100644 index 0000000..fd9890b --- /dev/null +++ b/src/test/java/com/matzip/common/config/AsyncConfigurationTest.java @@ -0,0 +1,37 @@ +package com.matzip.common.config; + +import com.matzip.AbstractMatzipApplicationTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import static org.assertj.core.api.Assertions.assertThat; + +class AsyncConfigurationTest extends AbstractMatzipApplicationTest { + + @Autowired + @Qualifier("generalExecutor") + private ThreadPoolTaskExecutor generalExecutor; + + @Autowired + @Qualifier("externalExecutor") + private ThreadPoolTaskExecutor externalExecutor; + + @Test + void 내부작업_풀과_외부IO_풀이_분리되어_등록된다() { + assertThat(generalExecutor).isNotSameAs(externalExecutor); + } + + @Test + void generalExecutor는_Hikari_커넥션을_고려한_크기로_구성된다() { + assertThat(generalExecutor.getCorePoolSize()).isEqualTo(3); + assertThat(generalExecutor.getMaxPoolSize()).isEqualTo(4); + } + + @Test + void externalExecutor는_저빈도_외부IO용_작은_크기로_구성된다() { + assertThat(externalExecutor.getCorePoolSize()).isEqualTo(1); + assertThat(externalExecutor.getMaxPoolSize()).isEqualTo(2); + } +} From 82e12d74ac3b5cc8370a40790e446d9dac7cc914 Mon Sep 17 00:00:00 2001 From: gihhyeon Date: Sat, 12 Sep 2026 12:27:16 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20async=20=EC=8A=A4=EB=A0=88=EB=93=9C?= =?UTF-8?q?=EB=A1=9C=20MDC(traceId)=20=EC=A0=84=ED=8C=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @Async는 작업을 다른 풀 스레드로 넘겨 ThreadLocal인 MDC가 끊긴다. 부모 스레드의 MDC를 복사·복원해 async 로그에도 traceId가 남게 한다. - MdcTaskDecorator를 general·external 풀에 적용 - 풀 스레드 재사용 시 이전 요청 컨텍스트가 남지 않도록 실행 후 MDC 정리 --- .../common/config/AsyncConfiguration.java | 7 ++ .../common/logging/MdcTaskDecorator.java | 26 +++++++ .../common/logging/MdcTaskDecoratorTest.java | 68 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 src/main/java/com/matzip/common/logging/MdcTaskDecorator.java create mode 100644 src/test/java/com/matzip/common/logging/MdcTaskDecoratorTest.java diff --git a/src/main/java/com/matzip/common/config/AsyncConfiguration.java b/src/main/java/com/matzip/common/config/AsyncConfiguration.java index dab18b1..6249196 100644 --- a/src/main/java/com/matzip/common/config/AsyncConfiguration.java +++ b/src/main/java/com/matzip/common/config/AsyncConfiguration.java @@ -1,8 +1,10 @@ package com.matzip.common.config; +import com.matzip.common.logging.MdcTaskDecorator; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskDecorator; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @@ -42,12 +44,17 @@ private ThreadPoolTaskExecutor buildExecutor(String threadNamePrefix, int corePo executor.setQueueCapacity(queueCapacity); executor.setThreadNamePrefix(threadNamePrefix); executor.setRejectedExecutionHandler(rejectedHandler); + executor.setTaskDecorator(mdcTaskDecorator()); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(SHUTDOWN_AWAIT_SECONDS); executor.initialize(); return executor; } + private TaskDecorator mdcTaskDecorator() { + return new MdcTaskDecorator(); + } + // ViewCountService가 REQUIRES_NEW라 CallerRunsPolicy는 커넥션 데드락 위험이 있어 log & discard 사용 private RejectedExecutionHandler loggingDiscardPolicy() { return (rejected, executor) -> diff --git a/src/main/java/com/matzip/common/logging/MdcTaskDecorator.java b/src/main/java/com/matzip/common/logging/MdcTaskDecorator.java new file mode 100644 index 0000000..ba84850 --- /dev/null +++ b/src/main/java/com/matzip/common/logging/MdcTaskDecorator.java @@ -0,0 +1,26 @@ +package com.matzip.common.logging; + +import org.slf4j.MDC; +import org.springframework.core.task.TaskDecorator; + +import java.util.Map; + +public class MdcTaskDecorator implements TaskDecorator { + + @Override + public Runnable decorate(Runnable runnable) { + Map parentContext = MDC.getCopyOfContextMap(); + return () -> { + if (parentContext != null) { + MDC.setContextMap(parentContext); + } else { + MDC.clear(); + } + try { + runnable.run(); + } finally { + MDC.clear(); // 풀 스레드 재사용 시 이전 요청 컨텍스트가 남지 않도록 정리 + } + }; + } +} diff --git a/src/test/java/com/matzip/common/logging/MdcTaskDecoratorTest.java b/src/test/java/com/matzip/common/logging/MdcTaskDecoratorTest.java new file mode 100644 index 0000000..e6537d4 --- /dev/null +++ b/src/test/java/com/matzip/common/logging/MdcTaskDecoratorTest.java @@ -0,0 +1,68 @@ +package com.matzip.common.logging; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.slf4j.MDC; + +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; + +class MdcTaskDecoratorTest { + + private static final String TRACE_ID = "traceId"; + + private final MdcTaskDecorator decorator = new MdcTaskDecorator(); + + @AfterEach + void clearMdc() { + MDC.clear(); + } + + @Test + void 부모_스레드의_MDC를_실행_스레드로_복사한다() throws Exception { + MDC.put(TRACE_ID, "abc123"); // 제출(부모) 스레드에서 데코레이트 + AtomicReference seenDuringRun = new AtomicReference<>(); + Runnable decorated = decorator.decorate(() -> seenDuringRun.set(MDC.get(TRACE_ID))); + + // 다른 스레드에서 실행 → ThreadLocal이라 데코레이터 없이는 전파되지 않음 + runAndJoin(new Thread(decorated)); + + assertThat(seenDuringRun.get()).isEqualTo("abc123"); + } + + @Test + void 실행_후_스레드의_MDC를_정리한다() throws Exception { + MDC.put(TRACE_ID, "abc123"); + Runnable decorated = decorator.decorate(() -> {}); + + // 실행이 끝난 뒤 워커 스레드에 값이 남으면 재사용 시 다음 작업을 오염시킨다 + AtomicReference afterRun = new AtomicReference<>("SENTINEL"); + runAndJoin(new Thread(() -> { + decorated.run(); + afterRun.set(MDC.get(TRACE_ID)); + })); + + assertThat(afterRun.get()).isNull(); + } + + @Test + void 부모_MDC가_없으면_워커에_남은_잔여값을_지운다() throws Exception { + MDC.clear(); // 부모(제출) 스레드에 컨텍스트 없음 + AtomicReference seenDuringRun = new AtomicReference<>("SENTINEL"); + Runnable decorated = decorator.decorate(() -> seenDuringRun.set(MDC.get(TRACE_ID))); + + // 재사용된 워커 스레드에 이전 작업의 값이 남아있는 상황을 모사 + runAndJoin(new Thread(() -> { + MDC.put(TRACE_ID, "STALE"); + decorated.run(); + })); + + assertThat(seenDuringRun.get()).isNull(); + } + + private void runAndJoin(Thread worker) throws InterruptedException { + worker.start(); + worker.join(); + } +} From 52106631e0672a8b26e5ec1bb48dc709b08dbabb Mon Sep 17 00:00:00 2001 From: gihhyeon Date: Mon, 14 Sep 2026 20:37:03 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20async=20=ED=92=80=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=84=20=EC=83=81=EC=88=98=EB=A1=9C=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/matzip/admin/event/RequestReviewEventListener.java | 3 ++- .../com/matzip/common/analytics/AnalyticsEventListener.java | 3 ++- .../java/com/matzip/common/config/AsyncConfiguration.java | 6 ++++-- .../matzip/common/infra/discord/DiscordWebhookSender.java | 3 ++- .../java/com/matzip/place/service/ViewCountService.java | 3 ++- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/matzip/admin/event/RequestReviewEventListener.java b/src/main/java/com/matzip/admin/event/RequestReviewEventListener.java index 359d271..7f2af74 100644 --- a/src/main/java/com/matzip/admin/event/RequestReviewEventListener.java +++ b/src/main/java/com/matzip/admin/event/RequestReviewEventListener.java @@ -3,6 +3,7 @@ import com.matzip.common.analytics.AnalyticsRecorder; import com.matzip.common.analytics.domain.EventType; import com.matzip.common.analytics.domain.TargetType; +import com.matzip.common.config.AsyncConfiguration; import com.matzip.common.exception.BusinessException; import com.matzip.common.exception.code.ErrorCode; import com.matzip.lottery.service.LotteryEventService; @@ -28,7 +29,7 @@ public RequestReviewEventListener(LotteryEventService lotteryEventService, Place this.analyticsRecorder = analyticsRecorder; } - @Async("generalExecutor") + @Async(AsyncConfiguration.GENERAL_EXECUTOR) @TransactionalEventListener(condition = "#event.reviewStatus().name() == 'APPROVED'") public void enterCurrentEventOnApproval(RequestReviewEvent event) { Place place = placeRepository.findById(event.placeId()) diff --git a/src/main/java/com/matzip/common/analytics/AnalyticsEventListener.java b/src/main/java/com/matzip/common/analytics/AnalyticsEventListener.java index 9b1537e..e65dc67 100644 --- a/src/main/java/com/matzip/common/analytics/AnalyticsEventListener.java +++ b/src/main/java/com/matzip/common/analytics/AnalyticsEventListener.java @@ -2,6 +2,7 @@ import com.matzip.common.analytics.domain.entity.EventLog; import com.matzip.common.analytics.repository.EventLogRepository; +import com.matzip.common.config.AsyncConfiguration; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; @@ -24,7 +25,7 @@ public AnalyticsEventListener(EventLogRepository eventLogRepository) { this.eventLogRepository = eventLogRepository; } - @Async("generalExecutor") + @Async(AsyncConfiguration.GENERAL_EXECUTOR) @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) public void on(AnalyticsEvent event) { try { diff --git a/src/main/java/com/matzip/common/config/AsyncConfiguration.java b/src/main/java/com/matzip/common/config/AsyncConfiguration.java index 6249196..05914a8 100644 --- a/src/main/java/com/matzip/common/config/AsyncConfiguration.java +++ b/src/main/java/com/matzip/common/config/AsyncConfiguration.java @@ -18,6 +18,8 @@ @Configuration public class AsyncConfiguration implements AsyncConfigurer { + public static final String GENERAL_EXECUTOR = "generalExecutor"; + public static final String EXTERNAL_EXECUTOR = "externalExecutor"; private static final int SHUTDOWN_AWAIT_SECONDS = 15; @Override @@ -25,13 +27,13 @@ public Executor getAsyncExecutor() { return generalExecutor(); } - @Bean + @Bean(GENERAL_EXECUTOR) public ThreadPoolTaskExecutor generalExecutor() { // Hikari 기본 커넥션 풀(10)을 웹 요청과 나눠 쓰도록 max 4로 제한 return buildExecutor("async-", 3, 4, 200, loggingDiscardPolicy()); } - @Bean + @Bean(EXTERNAL_EXECUTOR) public ThreadPoolTaskExecutor externalExecutor() { return buildExecutor("ext-", 1, 2, 30, new ThreadPoolExecutor.DiscardPolicy()); } diff --git a/src/main/java/com/matzip/common/infra/discord/DiscordWebhookSender.java b/src/main/java/com/matzip/common/infra/discord/DiscordWebhookSender.java index a1c9559..9c50fa4 100644 --- a/src/main/java/com/matzip/common/infra/discord/DiscordWebhookSender.java +++ b/src/main/java/com/matzip/common/infra/discord/DiscordWebhookSender.java @@ -1,5 +1,6 @@ package com.matzip.common.infra.discord; +import com.matzip.common.config.AsyncConfiguration; import com.matzip.common.config.DiscordWebhookProperties; import java.util.Map; import lombok.extern.slf4j.Slf4j; @@ -21,7 +22,7 @@ public DiscordWebhookSender(WebClient.Builder webClientBuilder, this.discordWebhookProperties = discordWebhookProperties; } - @Async("externalExecutor") + @Async(AsyncConfiguration.EXTERNAL_EXECUTOR) public void sendAsync(String content) { String webhookUrl = discordWebhookProperties.getWebhookUrl(); if (!StringUtils.hasText(webhookUrl)) { diff --git a/src/main/java/com/matzip/place/service/ViewCountService.java b/src/main/java/com/matzip/place/service/ViewCountService.java index 07f73bf..a3b1d78 100644 --- a/src/main/java/com/matzip/place/service/ViewCountService.java +++ b/src/main/java/com/matzip/place/service/ViewCountService.java @@ -1,5 +1,6 @@ package com.matzip.place.service; +import com.matzip.common.config.AsyncConfiguration; import com.matzip.place.domain.DailyViewCount; import com.matzip.place.repository.DailyViewCountRepository; import com.matzip.place.repository.PlaceRepository; @@ -18,7 +19,7 @@ public class ViewCountService { private final PlaceRepository placeRepository; private final DailyViewCountRepository dailyViewCountRepository; - @Async("generalExecutor") + @Async(AsyncConfiguration.GENERAL_EXECUTOR) @Transactional(propagation = Propagation.REQUIRES_NEW) public void incrementAllCounts(Long placeId) { placeRepository.incrementViewCount(placeId); From 800ee09afe01354f41be7aee78b38bcf4abc5ae8 Mon Sep 17 00:00:00 2001 From: gihhyeon Date: Mon, 14 Sep 2026 21:01:37 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20external=20=ED=92=80=EB=8F=84?= =?UTF-8?q?=20=EC=9C=A0=EC=8B=A4=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=EB=A5=BC?= =?UTF-8?q?=20=EB=82=A8=EA=B8=B0=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../matzip/common/config/AsyncConfiguration.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/matzip/common/config/AsyncConfiguration.java b/src/main/java/com/matzip/common/config/AsyncConfiguration.java index 05914a8..df50f08 100644 --- a/src/main/java/com/matzip/common/config/AsyncConfiguration.java +++ b/src/main/java/com/matzip/common/config/AsyncConfiguration.java @@ -10,10 +10,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ThreadPoolExecutor; - -@Slf4j +import java.util.concurrent.RejectedExecutionHandler;@Slf4j @EnableAsync @Configuration public class AsyncConfiguration implements AsyncConfigurer { @@ -30,12 +27,12 @@ public Executor getAsyncExecutor() { @Bean(GENERAL_EXECUTOR) public ThreadPoolTaskExecutor generalExecutor() { // Hikari 기본 커넥션 풀(10)을 웹 요청과 나눠 쓰도록 max 4로 제한 - return buildExecutor("async-", 3, 4, 200, loggingDiscardPolicy()); + return buildExecutor("async-", 3, 4, 200, loggingDiscardPolicy(GENERAL_EXECUTOR)); } @Bean(EXTERNAL_EXECUTOR) public ThreadPoolTaskExecutor externalExecutor() { - return buildExecutor("ext-", 1, 2, 30, new ThreadPoolExecutor.DiscardPolicy()); + return buildExecutor("ext-", 1, 2, 30, loggingDiscardPolicy(EXTERNAL_EXECUTOR)); } private ThreadPoolTaskExecutor buildExecutor(String threadNamePrefix, int corePoolSize, int maxPoolSize, @@ -58,9 +55,9 @@ private TaskDecorator mdcTaskDecorator() { } // ViewCountService가 REQUIRES_NEW라 CallerRunsPolicy는 커넥션 데드락 위험이 있어 log & discard 사용 - private RejectedExecutionHandler loggingDiscardPolicy() { + private RejectedExecutionHandler loggingDiscardPolicy(String poolName) { return (rejected, executor) -> - log.warn("[비동기 작업 유실] general 풀 포화로 작업을 버립니다. activeThreads={}, queueSize={}", - executor.getActiveCount(), executor.getQueue().size()); + log.warn("[비동기 작업 유실] {}} 풀 포화로 작업을 버립니다. activeThreads={}, queueSize={}", + poolName, executor.getActiveCount(), executor.getQueue().size()); } } From c8f0d7f0ab355421c6991e5e85b0f9eb3a8dbb59 Mon Sep 17 00:00:00 2001 From: gihhyeon Date: Mon, 14 Sep 2026 21:02:07 +0900 Subject: [PATCH 5/5] =?UTF-8?q?style:=20public,=20private=20=EA=B0=80?= =?UTF-8?q?=EB=8F=85=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EA=B3=B5?= =?UTF-8?q?=EB=B0=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/matzip/common/config/AsyncConfiguration.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/matzip/common/config/AsyncConfiguration.java b/src/main/java/com/matzip/common/config/AsyncConfiguration.java index df50f08..58b920e 100644 --- a/src/main/java/com/matzip/common/config/AsyncConfiguration.java +++ b/src/main/java/com/matzip/common/config/AsyncConfiguration.java @@ -17,6 +17,7 @@ public class AsyncConfiguration implements AsyncConfigurer { public static final String GENERAL_EXECUTOR = "generalExecutor"; public static final String EXTERNAL_EXECUTOR = "externalExecutor"; + private static final int SHUTDOWN_AWAIT_SECONDS = 15; @Override