From f1a704429f34828eb0dd0ef1bc671cc98bbd787c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EB=AF=B8?= Date: Wed, 5 Aug 2026 16:27:54 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=9A=94=EC=B2=AD=EB=B3=84=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=97=90=20userId=20MDC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 유저 문의/버그 제보 시 특정 유저의 요청·에러 로그를 추적할 수 있도록, JWT에서 userId를 파싱하는 시점에 MDC에 담아 로그 전체에 남긴다. --- .../server/common/resolver/userId/UserIdResolver.java | 6 +++++- .../org/runnect/server/config/logging/MdcLoggingFilter.java | 1 + src/main/resources/logback-spring.xml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/runnect/server/common/resolver/userId/UserIdResolver.java b/src/main/java/org/runnect/server/common/resolver/userId/UserIdResolver.java index 55770c6..cedff32 100644 --- a/src/main/java/org/runnect/server/common/resolver/userId/UserIdResolver.java +++ b/src/main/java/org/runnect/server/common/resolver/userId/UserIdResolver.java @@ -4,6 +4,7 @@ import org.runnect.server.common.constant.TokenStatus; import org.runnect.server.common.constant.ErrorStatus; import lombok.RequiredArgsConstructor; +import org.slf4j.MDC; import org.runnect.server.common.module.check.TypeChecker; import org.runnect.server.user.exception.authException.InvalidAccessTokenException; import org.runnect.server.user.exception.authException.NullAccessTokenException; @@ -76,6 +77,7 @@ public Object resolveArgument(@NotNull MethodParameter parameter, ModelAndViewCo && request.getMethod().equals("GET") && VISITOR_POSSIBLE_URLS.contains(removeLastPathSegment(request.getRequestURI()))){ // 방문자모드 허용 api에 대한 요청이 맞는지 검증 + MDC.put("userId", String.valueOf(VISITOR_ID)); return VISITOR_ID; } @@ -92,7 +94,9 @@ public Object resolveArgument(@NotNull MethodParameter parameter, ModelAndViewCo final String tokenContents = jwtService.getJwtContents(accessToken); try { - return Long.parseLong(tokenContents); + Long userId = Long.parseLong(tokenContents); + MDC.put("userId", String.valueOf(userId)); + return userId; } catch (NumberFormatException e) { throw new NotFoundUserException(ErrorStatus.NOT_FOUND_USER_EXCEPTION, ErrorStatus.NOT_FOUND_USER_EXCEPTION.getMessage()); } diff --git a/src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java b/src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java index d7fe4a3..8508f85 100644 --- a/src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java +++ b/src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java @@ -36,6 +36,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha chain.doFilter(request, response); } finally { MDC.remove(TRACE_ID_KEY); + MDC.remove("userId"); } } } diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 81b0490..3a1dba5 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -3,7 +3,7 @@ + value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [userId=%X{userId}] %-5level [%thread] %logger{36} - %msg%n"/> From 2914ae5d4a86f7cb33d7bd9cd24dc8bfd5f07d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EB=AF=B8?= Date: Wed, 5 Aug 2026 16:38:53 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=ED=95=84=ED=84=B0=20=EB=A0=88?= =?UTF-8?q?=EB=B2=A8=EC=97=90=EC=84=9C=EB=8F=84=20userId=20MDC=20=EC=B5=9C?= =?UTF-8?q?=EC=84=A0=20=EB=85=B8=EB=A0=A5=20=EC=B1=84=EC=9A=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @UserId 파라미터가 없는 요청(토큰 재발급, 배너 등)이나 인자 리졸브 이전 단계에서 예외가 나는 경우 userId가 로그에 안 남는 문제를 MdcLoggingFilter에서 accessToken을 미리 파싱해두는 방식으로 보완. 실제 인증 검증/방문자 모드 처리는 UserIdResolver가 그대로 담당하고, 그 결과가 이 값을 덮어쓴다. 로컬 DB/Redis 띄우고 실제 JWT로 검증: - @UserId 있는 요청: 요청 진입 로그부터 userId 표시 - @UserId 없는 공개 엔드포인트: 토큰만 있으면 userId 표시 --- .../config/logging/MdcLoggingFilter.java | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java b/src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java index 8508f85..9d90126 100644 --- a/src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java +++ b/src/main/java/org/runnect/server/config/logging/MdcLoggingFilter.java @@ -1,6 +1,9 @@ package org.runnect.server.config.logging; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.runnect.server.common.constant.TokenStatus; +import org.runnect.server.config.jwt.JwtService; import org.slf4j.MDC; import org.springframework.stereotype.Component; @@ -17,13 +20,21 @@ * 요청마다 traceId를 발급해 MDC에 담아둔다. * 로그 패턴에 %X{traceId}를 포함시키면(logback-spring.xml), 여러 요청이 뒤섞인 로그에서도 * 같은 traceId로 특정 요청의 흐름만 추적할 수 있다. + * + * userId는 @UserId 파라미터가 없는 요청(토큰 재발급, 배너 등)에서도 로그에 남도록 + * 여기서 best-effort로 한 번 더 채운다. 실제 인증 검증/방문자 모드 처리는 UserIdResolver가 + * 맡고, 그 결과가 이후 이 값을 덮어쓴다. */ @Slf4j @Component +@RequiredArgsConstructor public class MdcLoggingFilter implements Filter { private static final String TRACE_ID_KEY = "traceId"; private static final int TRACE_ID_LENGTH = 8; + private static final String USER_ID_KEY = "userId"; + + private final JwtService jwtService; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) @@ -32,11 +43,26 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha try { MDC.put(TRACE_ID_KEY, traceId); HttpServletRequest httpRequest = (HttpServletRequest) request; + populateUserIdBestEffort(httpRequest); log.info("{} {}", httpRequest.getMethod(), httpRequest.getRequestURI()); chain.doFilter(request, response); } finally { MDC.remove(TRACE_ID_KEY); - MDC.remove("userId"); + MDC.remove(USER_ID_KEY); + } + } + + private void populateUserIdBestEffort(HttpServletRequest request) { + String accessToken = request.getHeader("accessToken"); + if (accessToken == null) { + return; + } + try { + if (jwtService.verifyToken(accessToken) == TokenStatus.TOKEN_VALID) { + MDC.put(USER_ID_KEY, jwtService.getJwtContents(accessToken)); + } + } catch (RuntimeException e) { + // 로깅 목적의 best-effort 파싱이므로 실패해도 요청 처리는 계속 진행한다. } } }