From 042d2745525d4842e59c64497a0042e7c92f77f4 Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Thu, 24 Sep 2026 15:14:27 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EC=B2=98=EB=A6=AC=EB=90=9C=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20WARN=20=EB=A1=9C=EA=B7=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EB=B0=8F=20MDC=EC=97=90=20=EC=9A=94=EC=B2=AD=20URI?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ssu/eatssu/global/log/MDCLoggingFilter.java | 4 +++- src/main/resources/application-dev.yml | 3 +++ src/main/resources/application-prod.yml | 3 +++ .../ssu/eatssu/global/log/MDCLoggingFilterTest.java | 10 ++++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/ssu/eatssu/global/log/MDCLoggingFilter.java b/src/main/java/ssu/eatssu/global/log/MDCLoggingFilter.java index e1d6ce9e..05107dc9 100644 --- a/src/main/java/ssu/eatssu/global/log/MDCLoggingFilter.java +++ b/src/main/java/ssu/eatssu/global/log/MDCLoggingFilter.java @@ -16,11 +16,13 @@ public class MDCLoggingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - String requestId = ((HttpServletRequest) request).getHeader("X-RequestID"); + HttpServletRequest httpRequest = (HttpServletRequest) request; + String requestId = httpRequest.getHeader("X-RequestID"); if (requestId == null) { requestId = UUID.randomUUID().toString().replace("-", ""); } MDC.put("requestId", requestId); + MDC.put("uri", httpRequest.getMethod() + " " + httpRequest.getRequestURI()); try { chain.doFilter(request, response); } finally { diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 5286c067..6fcdb99e 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -44,6 +44,9 @@ spring: max-file-size: 20MB max-request-size: 20MB + mvc: + log-resolved-exception: true + ## Auth jwt: secret: diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index e3bacb43..d2b074a9 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -45,6 +45,9 @@ spring: max-file-size: 20MB max-request-size: 20MB + mvc: + log-resolved-exception: true + ## Auth jwt: secret: diff --git a/src/test/java/ssu/eatssu/global/log/MDCLoggingFilterTest.java b/src/test/java/ssu/eatssu/global/log/MDCLoggingFilterTest.java index 46dd90a4..2407dde5 100644 --- a/src/test/java/ssu/eatssu/global/log/MDCLoggingFilterTest.java +++ b/src/test/java/ssu/eatssu/global/log/MDCLoggingFilterTest.java @@ -26,6 +26,16 @@ void clearMdc() { assertThat(MDC.get("requestId")).isNull(); } + @Test + void 요청_메서드와_URI를_전달하고_필터_종료_후_제거한다() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/v2/reviews"); + + new MDCLoggingFilter().doFilter(request, new MockHttpServletResponse(), (req, res) -> + assertThat(MDC.get("uri")).isEqualTo("POST /v2/reviews")); + + assertThat(MDC.get("uri")).isNull(); + } + @Test void 요청_ID가_없으면_새_ID를_생성한다() throws Exception { new MDCLoggingFilter().doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(), (req, res) -> From fa3370c4b27d751db1165a5357b99448b117811a Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Thu, 24 Sep 2026 15:18:16 +0900 Subject: [PATCH 2/6] =?UTF-8?q?refactor:=20GlobalExceptionHandler=20?= =?UTF-8?q?=EB=AF=B8=EC=82=AC=EC=9A=A9=20=ED=95=84=EB=93=9C=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatssu/global/handler/GlobalExceptionHandler.java | 10 ---------- .../admin/controller/ManageCategoryControllerTest.java | 6 +----- .../admin/controller/ManageFixMenuControllerTest.java | 6 +----- .../admin/controller/ManageInquiryControllerTest.java | 6 +----- .../admin/controller/ManageMealControllerTest.java | 6 +----- .../admin/controller/ManageReportControllerTest.java | 6 +----- .../admin/controller/ManageReviewControllerTest.java | 6 +----- .../menu/presentation/rest/MealControllerTest.java | 6 +----- .../presentation/PartnershipControllerTest.java | 6 +----- .../global/handler/GlobalExceptionHandlerTest.java | 3 +-- 10 files changed, 9 insertions(+), 52 deletions(-) diff --git a/src/main/java/ssu/eatssu/global/handler/GlobalExceptionHandler.java b/src/main/java/ssu/eatssu/global/handler/GlobalExceptionHandler.java index 16613daa..901cc7f6 100644 --- a/src/main/java/ssu/eatssu/global/handler/GlobalExceptionHandler.java +++ b/src/main/java/ssu/eatssu/global/handler/GlobalExceptionHandler.java @@ -1,9 +1,6 @@ package ssu.eatssu.global.handler; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.beans.TypeMismatchException; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; @@ -25,7 +22,6 @@ import org.springframework.web.multipart.support.MissingServletRequestPartException; import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import ssu.eatssu.domain.slack.service.SlackErrorNotifier; import ssu.eatssu.global.handler.response.BaseException; import ssu.eatssu.global.handler.response.BaseResponse; import ssu.eatssu.global.handler.response.BaseResponseStatus; @@ -33,14 +29,8 @@ /** * 전역 Controller 예외처리 */ -@Slf4j @RestControllerAdvice -@RequiredArgsConstructor public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { - private final SlackErrorNotifier slackErrorNotifier; - @Value("${server.env:unknown}") - private String serverEnv; - /** * BaseException 처리 */ diff --git a/src/test/java/ssu/eatssu/domain/admin/controller/ManageCategoryControllerTest.java b/src/test/java/ssu/eatssu/domain/admin/controller/ManageCategoryControllerTest.java index b6507161..dc589c0f 100644 --- a/src/test/java/ssu/eatssu/domain/admin/controller/ManageCategoryControllerTest.java +++ b/src/test/java/ssu/eatssu/domain/admin/controller/ManageCategoryControllerTest.java @@ -11,7 +11,6 @@ import ssu.eatssu.domain.admin.dto.request.RegisterCategoryRequest; import ssu.eatssu.domain.admin.service.ManageCategoryService; import ssu.eatssu.domain.restaurant.entity.Restaurant; -import ssu.eatssu.domain.slack.service.SlackErrorNotifier; import ssu.eatssu.global.handler.GlobalExceptionHandler; import ssu.eatssu.global.handler.response.BaseException; import ssu.eatssu.global.handler.response.BaseResponseStatus; @@ -29,15 +28,12 @@ class ManageCategoryControllerTest { @Mock private ManageCategoryService manageCategoryService; - @Mock - private SlackErrorNotifier slackErrorNotifier; - private MockMvc mockMvc; @BeforeEach void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(new ManageCategoryController(manageCategoryService)) - .setControllerAdvice(new GlobalExceptionHandler(slackErrorNotifier)) + .setControllerAdvice(new GlobalExceptionHandler()) .build(); } diff --git a/src/test/java/ssu/eatssu/domain/admin/controller/ManageFixMenuControllerTest.java b/src/test/java/ssu/eatssu/domain/admin/controller/ManageFixMenuControllerTest.java index 139c89c3..9efa3b41 100644 --- a/src/test/java/ssu/eatssu/domain/admin/controller/ManageFixMenuControllerTest.java +++ b/src/test/java/ssu/eatssu/domain/admin/controller/ManageFixMenuControllerTest.java @@ -13,7 +13,6 @@ import ssu.eatssu.domain.admin.dto.response.MenuBoards; import ssu.eatssu.domain.admin.service.ManageFixMenuService; import ssu.eatssu.domain.restaurant.entity.Restaurant; -import ssu.eatssu.domain.slack.service.SlackErrorNotifier; import ssu.eatssu.global.handler.GlobalExceptionHandler; import ssu.eatssu.global.handler.response.BaseException; import ssu.eatssu.global.handler.response.BaseResponseStatus; @@ -35,15 +34,12 @@ class ManageFixMenuControllerTest { @Mock private ManageFixMenuService manageFixMenuService; - @Mock - private SlackErrorNotifier slackErrorNotifier; - private MockMvc mockMvc; @BeforeEach void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(new ManageFixMenuController(manageFixMenuService)) - .setControllerAdvice(new GlobalExceptionHandler(slackErrorNotifier)) + .setControllerAdvice(new GlobalExceptionHandler()) .build(); } diff --git a/src/test/java/ssu/eatssu/domain/admin/controller/ManageInquiryControllerTest.java b/src/test/java/ssu/eatssu/domain/admin/controller/ManageInquiryControllerTest.java index 97866b3c..48641cfe 100644 --- a/src/test/java/ssu/eatssu/domain/admin/controller/ManageInquiryControllerTest.java +++ b/src/test/java/ssu/eatssu/domain/admin/controller/ManageInquiryControllerTest.java @@ -15,7 +15,6 @@ import ssu.eatssu.domain.admin.dto.response.PageWrapper; import ssu.eatssu.domain.admin.service.ManageInquiryService; import ssu.eatssu.domain.inquiry.entity.InquiryStatus; -import ssu.eatssu.domain.slack.service.SlackErrorNotifier; import ssu.eatssu.global.handler.GlobalExceptionHandler; import ssu.eatssu.global.handler.response.BaseException; import ssu.eatssu.global.handler.response.BaseResponseStatus; @@ -39,15 +38,12 @@ class ManageInquiryControllerTest { @Mock private ManageInquiryService manageInquiryService; - @Mock - private SlackErrorNotifier slackErrorNotifier; - private MockMvc mockMvc; @BeforeEach void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(new ManageInquiryController(manageInquiryService)) - .setControllerAdvice(new GlobalExceptionHandler(slackErrorNotifier)) + .setControllerAdvice(new GlobalExceptionHandler()) .setCustomArgumentResolvers(new PageableHandlerMethodArgumentResolver()) .build(); } diff --git a/src/test/java/ssu/eatssu/domain/admin/controller/ManageMealControllerTest.java b/src/test/java/ssu/eatssu/domain/admin/controller/ManageMealControllerTest.java index 020645b2..99ef0f4f 100644 --- a/src/test/java/ssu/eatssu/domain/admin/controller/ManageMealControllerTest.java +++ b/src/test/java/ssu/eatssu/domain/admin/controller/ManageMealControllerTest.java @@ -14,7 +14,6 @@ import ssu.eatssu.domain.admin.service.ManageMealService; import ssu.eatssu.domain.menu.entity.constants.TimePart; import ssu.eatssu.domain.restaurant.entity.Restaurant; -import ssu.eatssu.domain.slack.service.SlackErrorNotifier; import ssu.eatssu.global.handler.GlobalExceptionHandler; import ssu.eatssu.global.handler.response.BaseException; import ssu.eatssu.global.handler.response.BaseResponseStatus; @@ -41,15 +40,12 @@ class ManageMealControllerTest { @Mock private ManageMealService manageMealService; - @Mock - private SlackErrorNotifier slackErrorNotifier; - private MockMvc mockMvc; @BeforeEach void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(new ManageMealController(manageMealService)) - .setControllerAdvice(new GlobalExceptionHandler(slackErrorNotifier)) + .setControllerAdvice(new GlobalExceptionHandler()) .build(); } diff --git a/src/test/java/ssu/eatssu/domain/admin/controller/ManageReportControllerTest.java b/src/test/java/ssu/eatssu/domain/admin/controller/ManageReportControllerTest.java index b59e5b2d..e715cdad 100644 --- a/src/test/java/ssu/eatssu/domain/admin/controller/ManageReportControllerTest.java +++ b/src/test/java/ssu/eatssu/domain/admin/controller/ManageReportControllerTest.java @@ -12,7 +12,6 @@ import ssu.eatssu.domain.admin.dto.response.PageWrapper; import ssu.eatssu.domain.admin.dto.response.ReportLine; import ssu.eatssu.domain.admin.service.ManageReportService; -import ssu.eatssu.domain.slack.service.SlackErrorNotifier; import ssu.eatssu.global.handler.GlobalExceptionHandler; import ssu.eatssu.global.handler.response.BaseException; import ssu.eatssu.global.handler.response.BaseResponseStatus; @@ -35,15 +34,12 @@ class ManageReportControllerTest { @Mock private ManageReportService manageReportService; - @Mock - private SlackErrorNotifier slackErrorNotifier; - private MockMvc mockMvc; @BeforeEach void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(new ManageReportController(manageReportService)) - .setControllerAdvice(new GlobalExceptionHandler(slackErrorNotifier)) + .setControllerAdvice(new GlobalExceptionHandler()) .setCustomArgumentResolvers(new PageableHandlerMethodArgumentResolver()) .build(); } diff --git a/src/test/java/ssu/eatssu/domain/admin/controller/ManageReviewControllerTest.java b/src/test/java/ssu/eatssu/domain/admin/controller/ManageReviewControllerTest.java index be5ced6e..633cb0c9 100644 --- a/src/test/java/ssu/eatssu/domain/admin/controller/ManageReviewControllerTest.java +++ b/src/test/java/ssu/eatssu/domain/admin/controller/ManageReviewControllerTest.java @@ -8,7 +8,6 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import ssu.eatssu.domain.admin.service.ManageReviewService; -import ssu.eatssu.domain.slack.service.SlackErrorNotifier; import ssu.eatssu.global.handler.GlobalExceptionHandler; import ssu.eatssu.global.handler.response.BaseException; import ssu.eatssu.global.handler.response.BaseResponseStatus; @@ -26,15 +25,12 @@ class ManageReviewControllerTest { @Mock private ManageReviewService manageReviewService; - @Mock - private SlackErrorNotifier slackErrorNotifier; - private MockMvc mockMvc; @BeforeEach void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(new ManageReviewController(manageReviewService)) - .setControllerAdvice(new GlobalExceptionHandler(slackErrorNotifier)) + .setControllerAdvice(new GlobalExceptionHandler()) .build(); } diff --git a/src/test/java/ssu/eatssu/domain/menu/presentation/rest/MealControllerTest.java b/src/test/java/ssu/eatssu/domain/menu/presentation/rest/MealControllerTest.java index 0e5c1571..5827fb87 100644 --- a/src/test/java/ssu/eatssu/domain/menu/presentation/rest/MealControllerTest.java +++ b/src/test/java/ssu/eatssu/domain/menu/presentation/rest/MealControllerTest.java @@ -13,7 +13,6 @@ import ssu.eatssu.domain.menu.presentation.dto.response.MenusInMealResponse; import ssu.eatssu.domain.menu.service.MealService; import ssu.eatssu.domain.restaurant.entity.Restaurant; -import ssu.eatssu.domain.slack.service.SlackErrorNotifier; import ssu.eatssu.global.handler.GlobalExceptionHandler; import java.util.List; @@ -36,15 +35,12 @@ class MealControllerTest { @Mock private MealService mealService; - @Mock - private SlackErrorNotifier slackErrorNotifier; - private MockMvc mockMvc; @BeforeEach void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(new MealController(mealService)) - .setControllerAdvice(new GlobalExceptionHandler(slackErrorNotifier)) + .setControllerAdvice(new GlobalExceptionHandler()) .build(); } diff --git a/src/test/java/ssu/eatssu/domain/partnership/presentation/PartnershipControllerTest.java b/src/test/java/ssu/eatssu/domain/partnership/presentation/PartnershipControllerTest.java index 96dc44e1..ea46dcfc 100644 --- a/src/test/java/ssu/eatssu/domain/partnership/presentation/PartnershipControllerTest.java +++ b/src/test/java/ssu/eatssu/domain/partnership/presentation/PartnershipControllerTest.java @@ -15,7 +15,6 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import ssu.eatssu.domain.auth.security.CustomUserDetails; import ssu.eatssu.domain.partnership.service.PartnershipService; -import ssu.eatssu.domain.slack.service.SlackErrorNotifier; import ssu.eatssu.domain.user.entity.Role; import ssu.eatssu.global.handler.GlobalExceptionHandler; import ssu.eatssu.global.handler.response.BaseException; @@ -39,9 +38,6 @@ class PartnershipControllerTest { @Mock private PartnershipService partnershipService; - @Mock - private SlackErrorNotifier slackErrorNotifier; - private MockMvc mockMvc; @BeforeEach @@ -49,7 +45,7 @@ void setUp() { ObjectMapper objectMapper = new ObjectMapper().registerModule(new ParameterNamesModule()) .registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule()); mockMvc = MockMvcBuilders.standaloneSetup(new PartnershipController(partnershipService)) - .setControllerAdvice(new GlobalExceptionHandler(slackErrorNotifier)) + .setControllerAdvice(new GlobalExceptionHandler()) .setCustomArgumentResolvers(new AuthenticationPrincipalArgumentResolver()) .setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper)) .build(); diff --git a/src/test/java/ssu/eatssu/global/handler/GlobalExceptionHandlerTest.java b/src/test/java/ssu/eatssu/global/handler/GlobalExceptionHandlerTest.java index 3ba4de8d..2f1f0c5a 100644 --- a/src/test/java/ssu/eatssu/global/handler/GlobalExceptionHandlerTest.java +++ b/src/test/java/ssu/eatssu/global/handler/GlobalExceptionHandlerTest.java @@ -20,7 +20,6 @@ import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import org.springframework.web.multipart.support.MissingServletRequestPartException; import org.springframework.web.servlet.NoHandlerFoundException; -import ssu.eatssu.domain.slack.service.SlackErrorNotifier; import ssu.eatssu.global.handler.response.BaseException; import ssu.eatssu.global.handler.response.BaseResponse; import ssu.eatssu.global.handler.response.BaseResponseStatus; @@ -32,7 +31,7 @@ class GlobalExceptionHandlerTest { - private final GlobalExceptionHandler handler = new GlobalExceptionHandler(mock(SlackErrorNotifier.class)); + private final GlobalExceptionHandler handler = new GlobalExceptionHandler(); private final HttpHeaders headers = new HttpHeaders(); private final org.springframework.web.context.request.WebRequest request = mock(org.springframework.web.context.request.WebRequest.class); From ffd2fe14963cc9c87c83dbfb0c0a073cc741494d Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Thu, 24 Sep 2026 15:28:50 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20Slack=20=EB=A9=94=EC=8B=9C=EC=A7=80?= =?UTF-8?q?=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EC=A0=84=EC=86=A1=20=EB=B0=8F?= =?UTF-8?q?=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EC=8A=A4=EB=A0=88=EB=93=9C=20?= =?UTF-8?q?MDC=20=EC=A0=84=ED=8C=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/slack/service/SlackService.java | 2 ++ .../ssu/eatssu/global/config/AsyncConfig.java | 22 +++++++++++++ .../eatssu/global/config/AsyncConfigTest.java | 33 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/src/main/java/ssu/eatssu/domain/slack/service/SlackService.java b/src/main/java/ssu/eatssu/domain/slack/service/SlackService.java index 9e82b206..691f29c1 100644 --- a/src/main/java/ssu/eatssu/domain/slack/service/SlackService.java +++ b/src/main/java/ssu/eatssu/domain/slack/service/SlackService.java @@ -5,6 +5,7 @@ import com.slack.api.methods.request.chat.ChatPostMessageRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import ssu.eatssu.domain.slack.entity.SlackChannel; @@ -17,6 +18,7 @@ public class SlackService { private final MethodsClient methodsClient; + @Async public void sendSlackMessage(String message, SlackChannel channel) { String channelAddress = channel.getKrName(); diff --git a/src/main/java/ssu/eatssu/global/config/AsyncConfig.java b/src/main/java/ssu/eatssu/global/config/AsyncConfig.java index ca9284c6..5f9671df 100644 --- a/src/main/java/ssu/eatssu/global/config/AsyncConfig.java +++ b/src/main/java/ssu/eatssu/global/config/AsyncConfig.java @@ -1,9 +1,31 @@ package ssu.eatssu.global.config; +import org.slf4j.MDC; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskDecorator; import org.springframework.scheduling.annotation.EnableAsync; +import java.util.Map; + @Configuration @EnableAsync public class AsyncConfig { + + @Bean + public TaskDecorator mdcTaskDecorator() { + return runnable -> { + Map context = MDC.getCopyOfContextMap(); + return () -> { + if (context != null) { + MDC.setContextMap(context); + } + try { + runnable.run(); + } finally { + MDC.clear(); + } + }; + }; + } } diff --git a/src/test/java/ssu/eatssu/global/config/AsyncConfigTest.java b/src/test/java/ssu/eatssu/global/config/AsyncConfigTest.java index 2663eb05..c7ef2551 100644 --- a/src/test/java/ssu/eatssu/global/config/AsyncConfigTest.java +++ b/src/test/java/ssu/eatssu/global/config/AsyncConfigTest.java @@ -1,16 +1,49 @@ package ssu.eatssu.global.config; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.slf4j.MDC; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Future; import static org.assertj.core.api.Assertions.assertThat; class AsyncConfigTest { + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) + .withUserConfiguration(AsyncConfig.class); + + @AfterEach + void clearMdc() { + MDC.clear(); + } + @Test void 비동기_설정이_활성화되어_있다() { assertThat(AsyncConfig.class.isAnnotationPresent(Configuration.class)).isTrue(); assertThat(AsyncConfig.class.isAnnotationPresent(EnableAsync.class)).isTrue(); } + + @Test + void 비동기_스레드로_MDC를_전파하고_작업_종료_후_제거한다() { + contextRunner.run(context -> { + ThreadPoolTaskExecutor executor = context.getBean(ThreadPoolTaskExecutor.class); + executor.setCorePoolSize(1); + MDC.put("requestId", "request-1"); + + Future propagated = executor.submit(() -> MDC.get("requestId")); + MDC.clear(); + Future cleared = executor.submit(() -> MDC.get("requestId")); + + assertThat(propagated.get()).isEqualTo("request-1"); + assertThat(cleared.get()).isNull(); + }); + } } From c39537baae5861c39d94b7148da7d932696a8a24 Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Thu, 24 Sep 2026 15:28:57 +0900 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20JSON=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=EC=97=90=20=EB=A7=9E=EC=B6=B0=20Slack=20Graf?= =?UTF-8?q?ana=20=EB=A1=9C=EA=B7=B8=20=EB=A7=81=ED=81=AC=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ssu/eatssu/domain/slack/entity/SlackMessageFormat.java | 2 +- .../eatssu/domain/slack/entity/SlackMessageFormatTest.java | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/ssu/eatssu/domain/slack/entity/SlackMessageFormat.java b/src/main/java/ssu/eatssu/domain/slack/entity/SlackMessageFormat.java index 81abc5b1..bd09af6f 100644 --- a/src/main/java/ssu/eatssu/domain/slack/entity/SlackMessageFormat.java +++ b/src/main/java/ssu/eatssu/domain/slack/entity/SlackMessageFormat.java @@ -149,7 +149,7 @@ private static String buildGrafanaLogLink() { } String application = "eatssu-" + serverEnv; - String logQl = "{application=\"" + application + "\"} |= \"reqId=" + requestId + "\""; + String logQl = "{application=\"" + application + "\"} |= \"" + requestId + "\""; long now = System.currentTimeMillis(); Map query = new LinkedHashMap<>(); diff --git a/src/test/java/ssu/eatssu/domain/slack/entity/SlackMessageFormatTest.java b/src/test/java/ssu/eatssu/domain/slack/entity/SlackMessageFormatTest.java index 4a14f9fb..4d832206 100644 --- a/src/test/java/ssu/eatssu/domain/slack/entity/SlackMessageFormatTest.java +++ b/src/test/java/ssu/eatssu/domain/slack/entity/SlackMessageFormatTest.java @@ -27,6 +27,8 @@ import ssu.eatssu.global.handler.response.BaseException; import ssu.eatssu.global.handler.response.BaseResponseStatus; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.Date; import static org.assertj.core.api.Assertions.assertThat; @@ -118,6 +120,9 @@ void sendServerErrorIncludesGrafanaLogLinkWhenConfiguredAndRequestIdPresent() { // then assertThat(message).contains("https://example.grafana.net/explore?schemaVersion=1&panes=", "Grafana에서 로그 보기"); + assertThat(URLDecoder.decode(message, StandardCharsets.UTF_8)) + .contains("|= \\\"test-request-id\\\"") + .doesNotContain("reqId="); } @Test From 2e060be0d6a20f77e2a1d5b01c96f8c7d2bf82c4 Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Thu, 24 Sep 2026 15:36:26 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=EC=9D=91=EB=8B=B5=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=EC=97=90=20X-Request-ID=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9A=94=EC=B2=AD=20ID=20=ED=98=95=EC=8B=9D=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatssu/global/log/MDCLoggingFilter.java | 16 +++++++-- .../eatssu/global/config/AsyncConfigTest.java | 5 ++- .../global/log/MDCLoggingFilterTest.java | 36 +++++++++++++++++-- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/main/java/ssu/eatssu/global/log/MDCLoggingFilter.java b/src/main/java/ssu/eatssu/global/log/MDCLoggingFilter.java index 05107dc9..88fe8057 100644 --- a/src/main/java/ssu/eatssu/global/log/MDCLoggingFilter.java +++ b/src/main/java/ssu/eatssu/global/log/MDCLoggingFilter.java @@ -1,7 +1,12 @@ package ssu.eatssu.global.log; -import jakarta.servlet.*; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.slf4j.MDC; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -9,20 +14,25 @@ import java.io.IOException; import java.util.UUID; +import java.util.regex.Pattern; @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class MDCLoggingFilter implements Filter { + private static final String REQUEST_ID_HEADER = "X-Request-ID"; + private static final Pattern VALID_REQUEST_ID = Pattern.compile("[A-Za-z0-9-]{1,64}"); + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; - String requestId = httpRequest.getHeader("X-RequestID"); - if (requestId == null) { + String requestId = httpRequest.getHeader(REQUEST_ID_HEADER); + if (requestId == null || !VALID_REQUEST_ID.matcher(requestId).matches()) { requestId = UUID.randomUUID().toString().replace("-", ""); } MDC.put("requestId", requestId); MDC.put("uri", httpRequest.getMethod() + " " + httpRequest.getRequestURI()); + ((HttpServletResponse) response).setHeader(REQUEST_ID_HEADER, requestId); try { chain.doFilter(request, response); } finally { diff --git a/src/test/java/ssu/eatssu/global/config/AsyncConfigTest.java b/src/test/java/ssu/eatssu/global/config/AsyncConfigTest.java index c7ef2551..b96ecf5c 100644 --- a/src/test/java/ssu/eatssu/global/config/AsyncConfigTest.java +++ b/src/test/java/ssu/eatssu/global/config/AsyncConfigTest.java @@ -32,16 +32,19 @@ void clearMdc() { } @Test - void 비동기_스레드로_MDC를_전파하고_작업_종료_후_제거한다() { + void propagatesMdcToAsyncThreadAndClearsAfterTask() { contextRunner.run(context -> { + // given ThreadPoolTaskExecutor executor = context.getBean(ThreadPoolTaskExecutor.class); executor.setCorePoolSize(1); MDC.put("requestId", "request-1"); + // when Future propagated = executor.submit(() -> MDC.get("requestId")); MDC.clear(); Future cleared = executor.submit(() -> MDC.get("requestId")); + // then assertThat(propagated.get()).isEqualTo("request-1"); assertThat(cleared.get()).isNull(); }); diff --git a/src/test/java/ssu/eatssu/global/log/MDCLoggingFilterTest.java b/src/test/java/ssu/eatssu/global/log/MDCLoggingFilterTest.java index 2407dde5..0523cb0a 100644 --- a/src/test/java/ssu/eatssu/global/log/MDCLoggingFilterTest.java +++ b/src/test/java/ssu/eatssu/global/log/MDCLoggingFilterTest.java @@ -18,7 +18,7 @@ void clearMdc() { @Test void 요청_ID를_전달하고_필터_종료_후_제거한다() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("X-RequestID", "request-1"); + request.addHeader("X-Request-ID", "request-1"); new MDCLoggingFilter().doFilter(request, new MockHttpServletResponse(), (req, res) -> assertThat(MDC.get("requestId")).isEqualTo("request-1")); @@ -27,12 +27,15 @@ void clearMdc() { } @Test - void 요청_메서드와_URI를_전달하고_필터_종료_후_제거한다() throws Exception { + void putsMethodAndUriIntoMdcAndClearsAfterFilter() throws Exception { + // given MockHttpServletRequest request = new MockHttpServletRequest("POST", "/v2/reviews"); + // when new MDCLoggingFilter().doFilter(request, new MockHttpServletResponse(), (req, res) -> assertThat(MDC.get("uri")).isEqualTo("POST /v2/reviews")); + // then assertThat(MDC.get("uri")).isNull(); } @@ -41,4 +44,33 @@ void clearMdc() { new MDCLoggingFilter().doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(), (req, res) -> assertThat(MDC.get("requestId")).hasSize(32)); } + + @Test + void returnsRequestIdInResponseHeader() throws Exception { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("X-Request-ID", "request-1"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // when + new MDCLoggingFilter().doFilter(request, response, (req, res) -> {}); + + // then + assertThat(response.getHeader("X-Request-ID")).isEqualTo("request-1"); + } + + @Test + void replacesInvalidRequestIdWithGeneratedOne() throws Exception { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("X-Request-ID", "bad id!" + "a".repeat(64)); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // when + new MDCLoggingFilter().doFilter(request, response, (req, res) -> + assertThat(MDC.get("requestId")).matches("[0-9a-f]{32}")); + + // then + assertThat(response.getHeader("X-Request-ID")).matches("[0-9a-f]{32}"); + } } From ea075dac3532c70292ca9ff415d8b57e9c926d15 Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Thu, 24 Sep 2026 15:41:30 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=EC=BD=98=EC=86=94=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=ED=83=80=EC=9E=84=EC=8A=A4=ED=83=AC=ED=94=84?= =?UTF-8?q?=EB=A5=BC=20KST=EB=A1=9C=20=ED=91=9C=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/logback-spring.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 5534a23b..d91e0132 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -7,7 +7,7 @@ + value="%d{yyyy-MM-dd HH:mm:ss.SSS, Asia/Seoul} %clr([%level]) [%thread] [reqId=%X{requestId}] %cyan(%logger{36}) - %msg%n"/>