From 91ceca5e25edc6083cc727d73c7a673fff98fe16 Mon Sep 17 00:00:00 2001 From: starvingorange Date: Wed, 18 Feb 2026 20:15:51 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:friend=20=EB=AA=A9=EB=A1=9D=20api=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopon/user/presentation/FriendController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/loopon/user/presentation/FriendController.java b/src/main/java/com/loopon/user/presentation/FriendController.java index 95dac3f6..0f097bab 100644 --- a/src/main/java/com/loopon/user/presentation/FriendController.java +++ b/src/main/java/com/loopon/user/presentation/FriendController.java @@ -29,7 +29,7 @@ public class FriendController implements FriendApiDocs { //내 친구 목록 조회 API @Override @GetMapping - public ResponseEntity>> getMyFriend(@AuthenticationPrincipal PrincipalDetails principalDetails, @PageableDefault(sort = "nickname", direction = Sort.Direction.ASC) Pageable pageable) { + public ResponseEntity>> getMyFriend(@AuthenticationPrincipal PrincipalDetails principalDetails, @PageableDefault(sort = "updatedAt", direction = Sort.Direction.DESC) Pageable pageable) { Long me = principalDetails.getUserId(); SliceResponse res = friendService.getMyFriends(me, pageable); return ResponseEntity.ok(CommonResponse.onSuccess(res)); From 184f5977a85b89f10b79b7a49045c616ce6488b5 Mon Sep 17 00:00:00 2001 From: Seungwon-Choi Date: Thu, 19 Feb 2026 02:54:38 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refactor:=20LogAspect=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/loopon/global/log/LogAspect.java | 132 +++++------------- 1 file changed, 34 insertions(+), 98 deletions(-) diff --git a/src/main/java/com/loopon/global/log/LogAspect.java b/src/main/java/com/loopon/global/log/LogAspect.java index aa1a5de3..2d6330f7 100644 --- a/src/main/java/com/loopon/global/log/LogAspect.java +++ b/src/main/java/com/loopon/global/log/LogAspect.java @@ -5,134 +5,70 @@ import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; -import org.aspectj.lang.reflect.CodeSignature; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; -import org.springframework.util.StopWatch; -import java.util.HashMap; -import java.util.Map; +import java.util.Arrays; @Component @Aspect @Slf4j public class LogAspect { - private static final String[] SENSITIVE_KEYWORDS = { - "password", "pw", "secret", - "token", "access", "refresh", - "auth", "cred", - "key", "pin", "card", - "ssn", "social" - }; - - @Pointcut("execution(* com.loopon..*Controller.*(..))") - public void controllerMethods() { + @Pointcut("execution(* com.loopon.*Controller.*(..))") + public void controller() { } @Pointcut("execution(* com.loopon..*Service.*(..))") - public void serviceMethods() { + public void service() { } - @Around("controllerMethods() || serviceMethods()") + @Around("controller() || service()") public Object logExecution(ProceedingJoinPoint joinPoint) throws Throwable { - StopWatch stopWatch = new StopWatch(); - MethodInfo methodInfo = null; + long startTime = System.currentTimeMillis(); + Object result = null; try { - methodInfo = extractMethodInfo(joinPoint); - - stopWatch.start(); - Object result = joinPoint.proceed(); - stopWatch.stop(); - - logSuccess(methodInfo, result, stopWatch); + result = joinPoint.proceed(); return result; - - } catch (Throwable e) { - if (stopWatch.isRunning()) { - stopWatch.stop(); - } - - logFailure(methodInfo, e, stopWatch); - throw e; + } finally { + long totalTime = System.currentTimeMillis() - startTime; + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + + log.info("[{}] {}.{} | Args: {} | Ret: {} | {}ms", + getLayer(signature), // 1. 레이어 (API / SVC) + className(signature), // 2. 클래스명 + methodName(signature), // 3. 메서드명 + formatArgs(joinPoint.getArgs()), // 4. 파라미터 (단순화) + formatResult(result), // 5. 결과값 (길이 제한) + totalTime // 6. 소요시간 + ); } } - private void logSuccess(MethodInfo methodInfo, Object result, StopWatch stopWatch) { - log.info("{} | {}({}) -> {} [{}ms]", - methodInfo.className(), - methodInfo.methodName(), - methodInfo.params(), - formatResult(result), - stopWatch.getTotalTimeMillis()); - } - - private void logFailure(MethodInfo info, Throwable e, StopWatch stopWatch) { - String className = (info != null) ? info.className() : "UnknownClass"; - String methodName = (info != null) ? info.methodName() : "UnknownMethod"; - Map params = (info != null) ? info.params() : Map.of(); - - log.error("{} | {}({}) -> [EXCEPTION] {}: {} [{}ms]", - className, - methodName, - params, - e.getClass().getSimpleName(), - e.getMessage(), - stopWatch.getTotalTimeMillis() - ); - } - - private MethodInfo extractMethodInfo(ProceedingJoinPoint joinPoint) { - MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + private String getLayer(MethodSignature signature) { String className = signature.getDeclaringType().getSimpleName(); - String methodName = signature.getName(); - Map params = extractParams(joinPoint); - - return new MethodInfo(className, methodName, params); + if (className.endsWith("Controller")) return "API"; + if (className.endsWith("Service")) return "SVC"; + return "ETC"; } - private Map extractParams(ProceedingJoinPoint joinPoint) { - CodeSignature signature = (CodeSignature) joinPoint.getSignature(); - String[] paramNames = signature.getParameterNames(); - Object[] args = joinPoint.getArgs(); - Map params = new HashMap<>(); - - if (paramNames == null) return params; - - for (int i = 0; i < paramNames.length; i++) { - String key = paramNames[i]; - Object value = args[i]; - if (isSensitive(key)) { - params.put(key, "****"); - } else { - params.put(key, value); - } - } - return params; + private String className(MethodSignature signature) { + return signature.getDeclaringType().getSimpleName(); } - private boolean isSensitive(String key) { - if (key == null) return false; - - String lowerKey = key.toLowerCase(); + private String methodName(MethodSignature signature) { + return signature.getName(); + } - for (String keyword : SENSITIVE_KEYWORDS) { - if (lowerKey.contains(keyword)) { - return true; - } - } - return false; + private String formatArgs(Object[] args) { + if (args == null || args.length == 0) return "[]"; + return Arrays.toString(args); } private String formatResult(Object result) { if (result == null) return "void"; - String resultStr = result.toString(); - if (resultStr.length() > 100) { - return resultStr.substring(0, 100) + "..."; - } - return resultStr; + String s = result.toString(); + return s.length() > 100 ? s.substring(0, 100) + "..." : s; } - - private record MethodInfo(String className, String methodName, Map params) {} } From f54d39f74ad176b309e6f95719f6c5fa02aa83d0 Mon Sep 17 00:00:00 2001 From: Seungwon-Choi Date: Thu, 19 Feb 2026 02:54:48 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20MdcLoggingFilter=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopon/global/log/MdcLoggingFilter.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/loopon/global/log/MdcLoggingFilter.java b/src/main/java/com/loopon/global/log/MdcLoggingFilter.java index c9d3aed4..42ca8be1 100644 --- a/src/main/java/com/loopon/global/log/MdcLoggingFilter.java +++ b/src/main/java/com/loopon/global/log/MdcLoggingFilter.java @@ -1,34 +1,38 @@ package com.loopon.global.log; -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 lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.UUID; +@Slf4j @Component @Order(Ordered.HIGHEST_PRECEDENCE) -public class MdcLoggingFilter implements Filter { +public class MdcLoggingFilter extends OncePerRequestFilter { + private static final String REQUEST_ID = "request_id"; @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - HttpServletRequest httpServletRequest = (HttpServletRequest) request; - String requestId = UUID.randomUUID().toString().substring(0, 8); + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String requestId = request.getHeader("X-Request-ID"); + if (requestId == null || requestId.isEmpty()) { + requestId = UUID.randomUUID().toString().substring(0, 8); + } MDC.put(REQUEST_ID, requestId); + response.setHeader("X-Request-ID", requestId); try { - chain.doFilter(request, response); + filterChain.doFilter(request, response); } finally { MDC.clear(); } From a9edb35b137f9d22edccffb35a57aacb7c13e51c Mon Sep 17 00:00:00 2001 From: Seungwon-Choi Date: Thu, 19 Feb 2026 02:54:57 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20GlobalExceptionAdvice=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionAdvice.java | 57 ++++++++----------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/loopon/global/exception/GlobalExceptionAdvice.java b/src/main/java/com/loopon/global/exception/GlobalExceptionAdvice.java index 8372690a..302feb42 100644 --- a/src/main/java/com/loopon/global/exception/GlobalExceptionAdvice.java +++ b/src/main/java/com/loopon/global/exception/GlobalExceptionAdvice.java @@ -2,7 +2,7 @@ import com.loopon.global.domain.ErrorCode; import com.loopon.global.domain.dto.CommonResponse; -import com.loopon.global.domain.dto.CommonResponse.ValidationErrorDetail; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -25,36 +25,32 @@ public class GlobalExceptionAdvice { @ExceptionHandler(BusinessException.class) - public ResponseEntity> handleBusinessException(BusinessException ex) { - log.warn("Business Exception: [{}] {}", ex.getErrorCode().getCode(), ex.getMessage()); + public ResponseEntity> handleBusinessException(BusinessException ex, HttpServletRequest request) { + log.warn("[BUS] {} {} | {}: {}", request.getMethod(), request.getRequestURI(), ex.getErrorCode().getCode(), ex.getMessage()); return ResponseEntity .status(ex.getErrorCode().getStatus()) .body(CommonResponse.onFailure(ex.getErrorCode())); } - @ExceptionHandler({ - BindException.class, - MethodArgumentNotValidException.class - }) - public ResponseEntity>> handleValidationException(BindException ex) { - log.warn("Validation Error: {}", ex.getBindingResult().getFieldError() != null + @ExceptionHandler({BindException.class, MethodArgumentNotValidException.class}) + public ResponseEntity>> handleValidationException(BindException ex, HttpServletRequest request) { + String errorMessage = ex.getBindingResult().getFieldError() != null ? ex.getBindingResult().getFieldError().getDefaultMessage() - : "Unknown Validation Error"); + : "Unknown Validation Error"; + + log.warn("[VAL] {} {} | Msg: {}", request.getMethod(), request.getRequestURI(), errorMessage); return ResponseEntity .status(ErrorCode.INVALID_INPUT_VALUE.getStatus()) - .body(CommonResponse.onFailure( - ErrorCode.INVALID_INPUT_VALUE, - ex.getBindingResult() - )); + .body(CommonResponse.onFailure(ErrorCode.INVALID_INPUT_VALUE, ex.getBindingResult())); } @ExceptionHandler(ConstraintViolationException.class) - public ResponseEntity>> handleConstraintViolationException(ConstraintViolationException ex) { - log.warn("Constraint Violation: {}", ex.getMessage()); + public ResponseEntity>> handleConstraintViolationException(ConstraintViolationException ex, HttpServletRequest request) { + log.warn("[VAL] {} {} | Msg: {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); - List details = ex.getConstraintViolations().stream() - .map(violation -> new ValidationErrorDetail( + List details = ex.getConstraintViolations().stream() + .map(violation -> new CommonResponse.ValidationErrorDetail( violation.getPropertyPath().toString(), violation.getMessage() )) @@ -72,8 +68,8 @@ public ResponseEntity>> handleConstra HttpRequestMethodNotSupportedException.class, IllegalArgumentException.class }) - public ResponseEntity> handleBadRequest(Exception ex) { - log.warn("Client Error: [{}] {}", ex.getClass().getSimpleName(), ex.getMessage()); + public ResponseEntity> handleBadRequest(Exception ex, HttpServletRequest request) { + log.warn("[BAD] {} {} | {}: {}", request.getMethod(), request.getRequestURI(), ex.getClass().getSimpleName(), ex.getMessage()); ErrorCode errorCode = switch (ex) { case NoHandlerFoundException ignored -> ErrorCode.NOT_FOUND; @@ -88,32 +84,25 @@ public ResponseEntity> handleBadRequest(Exception ex) { } @ExceptionHandler(AuthenticationException.class) - public ResponseEntity> handleAuthenticationException(AuthenticationException ex) { - log.warn("Authentication Exception: {}", ex.getMessage()); + public ResponseEntity> handleAuthenticationException(AuthenticationException ex, HttpServletRequest request) { + log.warn("[AUTH] {} {} | {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); return ResponseEntity .status(ErrorCode.UNAUTHORIZED.getStatus()) .body(CommonResponse.onFailure(ErrorCode.UNAUTHORIZED)); } @ExceptionHandler(AccessDeniedException.class) - public ResponseEntity> handleAccessDeniedException(AccessDeniedException ex) { - log.warn("Access Denied Exception: {}", ex.getMessage()); + public ResponseEntity> handleAccessDeniedException(AccessDeniedException ex, HttpServletRequest request) { + log.warn("[DENY] {} {} | {}", request.getMethod(), request.getRequestURI(), ex.getMessage()); return ResponseEntity .status(ErrorCode.FORBIDDEN.getStatus()) .body(CommonResponse.onFailure(ErrorCode.FORBIDDEN)); } - @ExceptionHandler(AuthorizationException.class) - public ResponseEntity> handleAuthorizationException(AuthorizationException ex) { - log.warn("Authorization Custom Exception: {}", ex.getMessage()); - return ResponseEntity - .status(ex.getErrorCode().getStatus()) - .body(CommonResponse.onFailure(ex.getErrorCode())); - } - @ExceptionHandler(Exception.class) - public ResponseEntity> handleException(Exception ex) { - log.error("Unhandled Exception: ", ex); + public ResponseEntity> handleException(Exception ex, HttpServletRequest request) { + log.error("[ERR] {} {} | Exception: ", request.getMethod(), request.getRequestURI(), ex); + return ResponseEntity .status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus()) .body(CommonResponse.onFailure(ErrorCode.INTERNAL_SERVER_ERROR)); From 021337b093c00a8be1549f59918b46143d8fdcc2 Mon Sep 17 00:00:00 2001 From: Seungwon-Choi Date: Thu, 19 Feb 2026 03:00:43 +0900 Subject: [PATCH 5/5] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/loopon/global/log/LogAspectTest.java | 119 +++--------------- .../global/log/MdcLoggingFilterTest.java | 92 +++++--------- 2 files changed, 49 insertions(+), 162 deletions(-) diff --git a/src/test/java/com/loopon/global/log/LogAspectTest.java b/src/test/java/com/loopon/global/log/LogAspectTest.java index 91888995..98d2771e 100644 --- a/src/test/java/com/loopon/global/log/LogAspectTest.java +++ b/src/test/java/com/loopon/global/log/LogAspectTest.java @@ -8,17 +8,11 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -@ExtendWith({MockitoExtension.class, OutputCaptureExtension.class}) +@ExtendWith(MockitoExtension.class) class LogAspectTest { @InjectMocks @@ -28,106 +22,27 @@ class LogAspectTest { private ProceedingJoinPoint joinPoint; @Mock - private MethodSignature methodSignature; + private MethodSignature signature; @Test - @DisplayName("성공: 정상 수행 시 파라미터, 결과값, 소요시간이 로그에 남아야 한다") - void 정상_수행_로그_기록(CapturedOutput output) throws Throwable { - // Given - setupJoinPoint(UserCommandService.class, "signUp", - new String[]{"email", "nickname"}, - new Object[]{"test@test.com", "tester"}); - - given(joinPoint.proceed()).willReturn("SuccessResult"); - - // When - logAspect.logExecution(joinPoint); - - // Then - verify(joinPoint, times(1)).proceed(); - - assertThat(output.getOut()) - .contains("UserCommandService") - .contains("signUp") - .contains("email=test@test.com") - .contains("SuccessResult") - .containsPattern("\\[\\d+ms\\]"); - } - - @Test - @DisplayName("마스킹: 민감한 키워드(password, token 등)는 ****로 가려져야 한다") - void 민감_정보_마스킹_처리(CapturedOutput output) throws Throwable { - // Given - setupJoinPoint(AuthService.class, "login", - new String[]{"email", "password", "refreshToken"}, - new Object[]{"user@test.com", "secret1234", "eyJh..."}); - - given(joinPoint.proceed()).willReturn("LoginSuccess"); - - // When - logAspect.logExecution(joinPoint); - - // Then - assertThat(output.getOut()) - .contains("email=user@test.com") - .doesNotContain("secret1234") - .doesNotContain("eyJh...") - .contains("password=****") - .contains("refreshToken=****"); - } - - @Test - @DisplayName("예외: 예외 발생 시 에러 로그가 남고 예외가 다시 던져져야 한다") - void 예외_발생_시_에러_로그_기록(CapturedOutput output) throws Throwable { - // Given - setupJoinPoint(UserQueryService.class, "getUser", - new String[]{"userId"}, - new Object[]{1L}); - - given(joinPoint.proceed()).willThrow(new IllegalArgumentException("Invalid User ID")); - - // When & Then - assertThatThrownBy(() -> logAspect.logExecution(joinPoint)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Invalid User ID"); - - assertThat(output.getOut()) - .contains("[EXCEPTION]") - .contains("IllegalArgumentException") - .contains("Invalid User ID") - .contains("userId=1"); - } - - @Test - @DisplayName("말줄임: 결과값이 100자를 넘으면 잘라서(...) 로깅해야 한다") - void 긴_결과값_말줄임_처리(CapturedOutput output) throws Throwable { - // Given - setupJoinPoint(BoardService.class, "getContent", new String[]{}, new Object[]{}); - - String longString = "A".repeat(150); + @DisplayName("결과값이 100자를 넘으면 잘라야 한다") + void truncateResult() throws Throwable { + // given + String longString = "A".repeat(200); given(joinPoint.proceed()).willReturn(longString); - // When - logAspect.logExecution(joinPoint); - - // Then - assertThat(output.getOut()) - .contains(longString.substring(0, 100) + "...") - .doesNotContain(longString); - } - - private void setupJoinPoint(Class targetClass, String methodName, String[] paramNames, Object[] args) { - doReturn(targetClass).when(methodSignature).getDeclaringType(); + given(joinPoint.getSignature()).willReturn(signature); + given(signature.getDeclaringType()).willReturn(TestController.class); + given(signature.getName()).willReturn("testMethod"); + given(joinPoint.getArgs()).willReturn(new Object[]{}); - given(methodSignature.getName()).willReturn(methodName); - given(methodSignature.getParameterNames()).willReturn(paramNames); + // when + Object result = logAspect.logExecution(joinPoint); - given(joinPoint.getSignature()).willReturn(methodSignature); - given(joinPoint.getArgs()).willReturn(args); + // then + assertEquals(longString, result); } - interface UserCommandService {} - interface AuthService {} - interface UserQueryService {} - interface BoardService {} + static class TestController { + } } diff --git a/src/test/java/com/loopon/global/log/MdcLoggingFilterTest.java b/src/test/java/com/loopon/global/log/MdcLoggingFilterTest.java index 906b8770..4244ed84 100644 --- a/src/test/java/com/loopon/global/log/MdcLoggingFilterTest.java +++ b/src/test/java/com/loopon/global/log/MdcLoggingFilterTest.java @@ -1,85 +1,57 @@ package com.loopon.global.log; import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.MDC; - -import java.io.IOException; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) class MdcLoggingFilterTest { - @InjectMocks - private MdcLoggingFilter mdcLoggingFilter; - - @Mock - private HttpServletRequest request; - - @Mock - private ServletResponse response; - - @Mock - private FilterChain filterChain; - - private static final String REQUEST_ID = "request_id"; - - @AfterEach - void tearDown() { - MDC.clear(); - } + private final MdcLoggingFilter filter = new MdcLoggingFilter(); @Test - @DisplayName("성공: Request ID가 생성되고, 체인 실행 후 MDC가 비워져야 한다") - void 정상_수행_요청ID_생성_및_MDC_정리() throws ServletException, IOException { - // Given - doAnswer(invocation -> { - String requestId = MDC.get(REQUEST_ID); - - assertNotNull(requestId, "Request ID가 생성되어야 합니다."); - assertEquals(8, requestId.length(), "UUID 앞 8자리여야 합니다."); - return null; - }).when(filterChain).doFilter(any(), any()); - - // When - mdcLoggingFilter.doFilter(request, response, filterChain); - - // Then - verify(filterChain, times(1)).doFilter(request, response); - - assertNull(MDC.get(REQUEST_ID), "필터 종료 후에는 MDC가 비워져야 합니다."); + @DisplayName("헤더에 ID가 없으면 새로 생성하고 MDC에 넣어야 한다") + void generateRequestId() throws Exception { + // given + MockHttpServletRequest req = new MockHttpServletRequest(); + MockHttpServletResponse res = new MockHttpServletResponse(); + FilterChain chain = mock(FilterChain.class); + + // when + filter.doFilter(req, res, chain); + + // then + String requestId = res.getHeader("X-Request-ID"); + assertNotNull(requestId); + verify(chain, times(1)).doFilter(req, res); + assertNull(MDC.get("request_id")); } @Test - @DisplayName("예외: 필터 체인 도중 예외가 발생해도 MDC는 반드시 비워져야 한다") - void 예외_발생_시_MDC_정리_보장() throws ServletException, IOException { - // Given - doThrow(new RuntimeException("Unexpected Error")) - .when(filterChain).doFilter(any(), any()); - - // When & Then - assertThrows(RuntimeException.class, () -> - mdcLoggingFilter.doFilter(request, response, filterChain) - ); - - assertNull(MDC.get(REQUEST_ID), "예외가 발생해도 MDC는 비워져야 합니다."); + @DisplayName("헤더에 ID가 있으면 그걸 유지해야 한다") + void keepRequestId() throws Exception { + // given + MockHttpServletRequest req = new MockHttpServletRequest(); + req.addHeader("X-Request-ID", "original-id"); + MockHttpServletResponse res = new MockHttpServletResponse(); + FilterChain chain = mock(FilterChain.class); + + // when + filter.doFilter(req, res, chain); + + // then + assertEquals("original-id", res.getHeader("X-Request-ID")); } }