From bd909d5317c670a16d9ef964750d45f4bebf8487 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Thu, 1 Jan 2026 02:11:54 +0900 Subject: [PATCH 01/16] =?UTF-8?q?feat:=20HTTP=20=EC=9A=94=EC=B2=AD/?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=A1=9C=EA=B9=85=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - traceId 기반 요청 추적 - 요청/응답 로깅 - CustomExceptionHandler와 중복 로깅 방지 - Actuator 엔드포인트 로깅 제외 --- .../common/filter/HttpLoggingFilter.java | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java diff --git a/src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java b/src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java new file mode 100644 index 00000000..4120bd82 --- /dev/null +++ b/src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java @@ -0,0 +1,118 @@ +package com.example.solidconnection.common.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +@Slf4j +@RequiredArgsConstructor +@Component +public class HttpLoggingFilter extends OncePerRequestFilter { + + private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher(); + + private static final List EXCLUDE_PATTERNS = List.of( + "/actuator/**" + ); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + + // 1) traceId 부여 + String traceId = generateTraceId(); + MDC.put("traceId", traceId); + + boolean excluded = isExcluded(request); + + // 2) 로깅 제외 대상이면 그냥 통과 (traceId는 유지: 추후 하위 레이어 로그에도 붙음) + if (excluded) { + try { + filterChain.doFilter(request, response); + } finally { + MDC.clear(); + } + return; + } + + printRequestUri(request); + + try { + filterChain.doFilter(request, response); + + Boolean alreadyExceptionLogging = (Boolean) request.getAttribute("exceptionHandlerLogged"); + if (alreadyExceptionLogging == null || !alreadyExceptionLogging) { + printResponse(request, response); + } + + } finally { + MDC.clear(); + } + } + + private boolean isExcluded(HttpServletRequest req) { + String path = req.getRequestURI(); + for (String p : EXCLUDE_PATTERNS) { + if (PATH_MATCHER.match(p, path)) { + return true; + } + } + return false; + } + + private String generateTraceId() { + return java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 16); + } + + private void printResponse( + HttpServletRequest request, + HttpServletResponse response + ) { + Long userId = (Long) request.getAttribute("user_id"); + String uri = buildDecodedRequestUri(request); + HttpStatus status = HttpStatus.valueOf(response.getStatus()); + + log.info("[RESPONSE] {} userId = {}, ({})", uri, userId, status); + } + + private void printRequestUri(HttpServletRequest request) { + String methodType = request.getMethod(); + String uri = buildDecodedRequestUri(request); + log.info("[REQUEST] {} {}", methodType, uri); + } + + private String decodeQuery(String rawQuery) { + if (rawQuery == null) { + return null; + } + try { + return URLDecoder.decode(rawQuery, StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + return rawQuery; + } + } + + private String buildDecodedRequestUri(HttpServletRequest request) { + String path = request.getRequestURI(); + String query = decodeQuery(request.getQueryString()); + return (query == null || query.isBlank()) ? path : path + "?" + query; + } +} From ff145a85cce6721fc267c209e66e00f92586a633 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Thu, 1 Jan 2026 02:33:48 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat:=20ExceptionHandler=EC=97=90=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EB=A1=9C=EA=B9=85=20=EB=B0=A9=EC=A7=80=20?= =?UTF-8?q?=ED=94=8C=EB=9E=98=EA=B7=B8=20=EB=B0=8F=20userId=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/CustomExceptionHandler.java | 59 +++++++++++++++---- .../common/filter/HttpLoggingFilter.java | 2 +- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java b/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java index 5700c304..6fb4ca3a 100644 --- a/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java +++ b/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java @@ -9,6 +9,7 @@ import com.example.solidconnection.common.response.ErrorResponse; import com.fasterxml.jackson.databind.exc.InvalidFormatException; import io.jsonwebtoken.JwtException; +import jakarta.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; import lombok.extern.slf4j.Slf4j; @@ -24,8 +25,13 @@ public class CustomExceptionHandler { @ExceptionHandler(CustomException.class) - protected ResponseEntity handleCustomException(CustomException ex) { - log.error("커스텀 예외 발생 : {}", ex.getMessage()); + protected ResponseEntity handleCustomException( + CustomException ex, + HttpServletRequest request + ) { + request.setAttribute("exceptionHandlerLogged", true); + Long userId = (Long) request.getAttribute("userId"); + log.error("커스텀 예외 발생 userId : {} msg: {}", userId, ex.getMessage()); ErrorResponse errorResponse = new ErrorResponse(ex); return ResponseEntity .status(ex.getCode()) @@ -33,9 +39,14 @@ protected ResponseEntity handleCustomException(CustomException ex } @ExceptionHandler(InvalidFormatException.class) - public ResponseEntity handleInvalidFormatException(InvalidFormatException ex) { + public ResponseEntity handleInvalidFormatException( + InvalidFormatException ex, + HttpServletRequest request + ) { + request.setAttribute("exceptionHandlerLogged", true); + Long userId = (Long) request.getAttribute("userId"); String errorMessage = ex.getValue() + " 은(는) 유효하지 않은 값입니다."; - log.error("JSON 파싱 예외 발생 : {}", errorMessage); + log.error("JSON 파싱 예외 발생 userId: {} msg: {}", userId, errorMessage); ErrorResponse errorResponse = new ErrorResponse(JSON_PARSING_FAILED, errorMessage); return ResponseEntity .status(JSON_PARSING_FAILED.getCode()) @@ -43,14 +54,20 @@ public ResponseEntity handleInvalidFormatException(InvalidFormatExceptio } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { + public ResponseEntity handleValidationExceptions( + MethodArgumentNotValidException ex, + HttpServletRequest request + ) { + request.setAttribute("exceptionHandlerLogged", true); + Long userId = (Long) request.getAttribute("userId"); + List errors = new ArrayList<>(); ex.getBindingResult() .getFieldErrors() .forEach(fieldError -> errors.add(fieldError.getDefaultMessage())); String errorMessage = errors.toString(); - log.error("입력값 검증 예외 발생 : {}", errorMessage); + log.error("입력값 검증 예외 발생 userId : {} msg: {}", userId, errorMessage); ErrorResponse errorResponse = new ErrorResponse(INVALID_INPUT, errorMessage); return ResponseEntity .status(HttpStatus.BAD_REQUEST) @@ -58,8 +75,14 @@ public ResponseEntity handleValidationExceptions(MethodArgumentNo } @ExceptionHandler(DataIntegrityViolationException.class) - public ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException ex) { - log.error("데이터 무결성 제약조건 위반 예외 발생 : {}", ex.getMessage()); + public ResponseEntity handleDataIntegrityViolationException( + DataIntegrityViolationException ex, + HttpServletRequest request + ) { + request.setAttribute("exceptionHandlerLogged", true); + Long userId = (Long) request.getAttribute("userId"); + + log.error("데이터 무결성 제약조건 위반 예외 발생 userId : {} msg : {}", userId, ex.getMessage()); ErrorResponse errorResponse = new ErrorResponse(DATA_INTEGRITY_VIOLATION, "데이터 무결성 제약조건 위반 예외 발생"); return ResponseEntity .status(DATA_INTEGRITY_VIOLATION.getCode()) @@ -67,9 +90,15 @@ public ResponseEntity handleDataIntegrityViolationException(DataIntegrit } @ExceptionHandler(JwtException.class) - public ResponseEntity handleJwtException(JwtException ex) { + public ResponseEntity handleJwtException( + JwtException ex, + HttpServletRequest request + ) { + request.setAttribute("exceptionHandlerLogged", true); + Long userId = (Long) request.getAttribute("userId"); + String errorMessage = ex.getMessage(); - log.error("JWT 예외 발생 : {}", errorMessage); + log.error("JWT 예외 발생 userId : {} msg : {}", userId, errorMessage); ErrorResponse errorResponse = new ErrorResponse(JWT_EXCEPTION, errorMessage); return ResponseEntity .status(HttpStatus.BAD_REQUEST) @@ -77,9 +106,15 @@ public ResponseEntity handleJwtException(JwtException ex) { } @ExceptionHandler(Exception.class) - public ResponseEntity handleOtherException(Exception ex) { + public ResponseEntity handleOtherException( + Exception ex, + HttpServletRequest request + ) { + request.setAttribute("exceptionHandlerLogged", true); + Long userId = (Long) request.getAttribute("userId"); + String errorMessage = ex.getMessage(); - log.error("서버 내부 예외 발생 : {}", errorMessage); + log.error("서버 내부 예외 발생 userId : {} , msg : {}", userId, errorMessage); ErrorResponse errorResponse = new ErrorResponse(NOT_DEFINED_ERROR, errorMessage); return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) diff --git a/src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java b/src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java index 4120bd82..0a0d1c53 100644 --- a/src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java +++ b/src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java @@ -86,7 +86,7 @@ private void printResponse( HttpServletRequest request, HttpServletResponse response ) { - Long userId = (Long) request.getAttribute("user_id"); + Long userId = (Long) request.getAttribute("userId"); String uri = buildDecodedRequestUri(request); HttpStatus status = HttpStatus.valueOf(response.getStatus()); From 88dee7481a838775bd497b4f614b1b06f6e9a880 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Thu, 1 Jan 2026 03:18:39 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20API=20=EC=88=98=ED=96=89=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EB=A1=9C=EA=B9=85=20=EC=9D=B8=ED=84=B0=EC=85=89?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ApiPerformanceInterceptor.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/common/interceptor/ApiPerformanceInterceptor.java diff --git a/src/main/java/com/example/solidconnection/common/interceptor/ApiPerformanceInterceptor.java b/src/main/java/com/example/solidconnection/common/interceptor/ApiPerformanceInterceptor.java new file mode 100644 index 00000000..f32a50a4 --- /dev/null +++ b/src/main/java/com/example/solidconnection/common/interceptor/ApiPerformanceInterceptor.java @@ -0,0 +1,84 @@ +package com.example.solidconnection.common.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.HandlerMapping; + +@Slf4j +@RequiredArgsConstructor +@Component +public class ApiPerformanceInterceptor implements HandlerInterceptor { + private static final String ROUTE_PATTERN_ATTRIBUTE = "routePattern"; + private static final String START_TIME_ATTRIBUTE = "startTime"; + private static final String REQUEST_URI_ATTRIBUTE = "requestUri"; + private static final int RESPONSE_TIME_THRESHOLD = 3_000; + private static final Logger API_PERF = LoggerFactory.getLogger("API_PERF"); + + @Override + public boolean preHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler + ) throws Exception { + + long startTime = System.currentTimeMillis(); + + request.setAttribute(START_TIME_ATTRIBUTE, startTime); + request.setAttribute(REQUEST_URI_ATTRIBUTE, request.getRequestURI()); + + Object bestMatchingPattern = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); + + if(bestMatchingPattern instanceof String s) { + request.setAttribute(ROUTE_PATTERN_ATTRIBUTE, s); + } + + return true; + } + + @Override + public void afterCompletion( + HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception ex + ) throws Exception { + Long startTime = (Long) request.getAttribute(START_TIME_ATTRIBUTE); + if(startTime == null) { + return; + } + + long responseTime = System.currentTimeMillis() - startTime; + + String uri = request.getRequestURI(); + String method = request.getMethod(); + int status = response.getStatus(); + + if (responseTime > RESPONSE_TIME_THRESHOLD) { + API_PERF.warn( + "type=API_Performance method_type={} uri={} response_time={} status={}", + method, uri, responseTime, status + ); + + log.warn("[API Performance] {} {} - {}ms [Status: {}]", + method, uri, responseTime, status + ); + + return; + } + + API_PERF.info( + "type=API_Performance method_type={} uri={} response_time={} status={}", + method, uri, responseTime, status + ); + + log.info("[API Performance]: {} {} - {}ms [Status: {}]", + method, uri, responseTime, status + ); + } +} From 69ccdd6ff86fa705547cb4df0e275c4d5b3dce8a Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Thu, 1 Jan 2026 16:42:47 +0900 Subject: [PATCH 04/16] =?UTF-8?q?feat:=20ApiPerf=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=EC=85=89=ED=84=B0,=20Logging=20=ED=95=84=ED=84=B0=20=EB=B9=88?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/config/web/WebMvcConfig.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java b/src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java index 56bb288e..b5600809 100644 --- a/src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java +++ b/src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java @@ -1,11 +1,17 @@ package com.example.solidconnection.common.config.web; +import com.example.solidconnection.common.filter.HttpLoggingFilter; +import com.example.solidconnection.common.interceptor.ApiPerformanceInterceptor; import com.example.solidconnection.common.resolver.AuthorizedUserResolver; import com.example.solidconnection.common.resolver.CustomPageableHandlerMethodArgumentResolver; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @@ -14,6 +20,8 @@ public class WebMvcConfig implements WebMvcConfigurer { private final AuthorizedUserResolver authorizedUserResolver; private final CustomPageableHandlerMethodArgumentResolver customPageableHandlerMethodArgumentResolver; + private final HttpLoggingFilter httpLoggingFilter; + private final ApiPerformanceInterceptor apiPerformanceInterceptor; @Override public void addArgumentResolvers(List resolvers) { @@ -22,4 +30,18 @@ public void addArgumentResolvers(List resolvers) customPageableHandlerMethodArgumentResolver )); } + + @Override + public void addInterceptors(InterceptorRegistry registry){ + registry.addInterceptor(apiPerformanceInterceptor) + .addPathPatterns("/**"); + } + + @Bean + public FilterRegistrationBean customHttpLoggingFilter() { + FilterRegistrationBean filterBean = new FilterRegistrationBean<>(); + filterBean.setFilter(httpLoggingFilter); + filterBean.setOrder(Ordered.HIGHEST_PRECEDENCE); + return filterBean; + } } From 222980c4ffcc171ff94cc725e9bef9ae3d755dea Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Thu, 1 Jan 2026 22:13:35 +0900 Subject: [PATCH 05/16] =?UTF-8?q?refactor:=20logback=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - info, warn, error, api_perf 로 로그 파일 분리해서 관리 --- src/main/resources/logback-spring.xml | 106 ++++++++++++++++++++------ 1 file changed, 84 insertions(+), 22 deletions(-) diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index e179be0f..a51603ff 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -2,34 +2,96 @@ - - + - - /var/log/spring/solid-connection-server.log + + + - - - /var/log/spring/solid-connection-server.%d{yyyy-MM-dd}.log - 30 - + + + ${LOG_PATH}/info/info.log + + ${LOG_PATH}/info/info.%d{yyyy-MM-dd}.log + 7 + + + ${FILE_PATTERN} + + + INFO + ACCEPT + DENY + + - - - timestamp=%d{yyyy-MM-dd'T'HH:mm:ss.SSS} level=%-5level thread=%thread logger=%logger{36} - message=%msg%n - - - + + + ${LOG_PATH}/warn/warn.log + + ${LOG_PATH}/warn/warn.%d{yyyy-MM-dd}.log + 7 + + + ${FILE_PATTERN} + + + WARN + ACCEPT + DENY + + - - - + + + ${LOG_PATH}/error/error.log + + ${LOG_PATH}/error/error.%d{yyyy-MM-dd}.log + 7 + + + ${FILE_PATTERN} + + + ERROR + ACCEPT + DENY + + - + + + ${LOG_PATH}/api-perf/api-perf.log + + ${LOG_PATH}/api-perf/api-perf.%d{yyyy-MM-dd}.log + 7 + + + ${FILE_PATTERN} + + + + + + + + + + + + + + + + + + + + - + - + \ No newline at end of file From d49b83b82ddca0e2b0b390f98057859e3d0af8c6 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Fri, 2 Jan 2026 00:25:29 +0900 Subject: [PATCH 06/16] =?UTF-8?q?feat:=20=EC=BF=BC=EB=A6=AC=20=EB=B3=84=20?= =?UTF-8?q?=EC=88=98=ED=96=89=EC=8B=9C=EA=B0=84=20=EB=A9=94=ED=8A=B8?= =?UTF-8?q?=EB=A6=AD=20=EB=AA=A8=EB=8B=88=ED=84=B0=EB=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 데이터소스 프록시 의존성 및 config 파일 추가 * feat: 데이터 소스 프록시가 metric을 찍을 수 있도록 listener 클래스 추가 * feat: 요청 시 method, uri 정보를 listener에서 활용하기 위해 RequestContext 및 관련 interceptor 추가 --- build.gradle | 4 ++ .../datasource/DataSourceProxyConfig.java | 29 ++++++++++ .../common/config/web/WebMvcConfig.java | 5 ++ .../common/interceptor/RequestContext.java | 16 ++++++ .../interceptor/RequestContextHolder.java | 18 +++++++ .../RequestContextInterceptor.java | 35 ++++++++++++ .../common/listener/QueryMetricsListener.java | 54 +++++++++++++++++++ 7 files changed, 161 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/common/config/datasource/DataSourceProxyConfig.java create mode 100644 src/main/java/com/example/solidconnection/common/interceptor/RequestContext.java create mode 100644 src/main/java/com/example/solidconnection/common/interceptor/RequestContextHolder.java create mode 100644 src/main/java/com/example/solidconnection/common/interceptor/RequestContextInterceptor.java create mode 100644 src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java diff --git a/build.gradle b/build.gradle index 91cc2e77..958811c9 100644 --- a/build.gradle +++ b/build.gradle @@ -68,6 +68,10 @@ dependencies { implementation 'org.hibernate.validator:hibernate-validator' implementation 'com.amazonaws:aws-java-sdk-s3:1.12.782' implementation 'org.springframework.boot:spring-boot-starter-websocket' + + // Database Proxy + implementation 'net.ttddyy:datasource-proxy:1.11.0' + implementation 'net.ttddyy.observation:datasource-micrometer:1.2.0' } tasks.named('test', Test) { diff --git a/src/main/java/com/example/solidconnection/common/config/datasource/DataSourceProxyConfig.java b/src/main/java/com/example/solidconnection/common/config/datasource/DataSourceProxyConfig.java new file mode 100644 index 00000000..b7bf0b00 --- /dev/null +++ b/src/main/java/com/example/solidconnection/common/config/datasource/DataSourceProxyConfig.java @@ -0,0 +1,29 @@ +package com.example.solidconnection.common.config.datasource; + +import com.example.solidconnection.common.listener.QueryMetricsListener; +import javax.sql.DataSource; +import lombok.RequiredArgsConstructor; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +@RequiredArgsConstructor +@Configuration +public class DataSourceProxyConfig { + + private final QueryMetricsListener queryMetricsListener; + + @Bean + @Primary + public DataSource proxyDataSource(DataSourceProperties props) { + DataSource dataSource = props.initializeDataSourceBuilder().build(); + + return ProxyDataSourceBuilder + .create(dataSource) + .listener(queryMetricsListener) + .name("main") + .build(); + } +} diff --git a/src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java b/src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java index b5600809..fe6617e5 100644 --- a/src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java +++ b/src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java @@ -2,6 +2,7 @@ import com.example.solidconnection.common.filter.HttpLoggingFilter; import com.example.solidconnection.common.interceptor.ApiPerformanceInterceptor; +import com.example.solidconnection.common.interceptor.RequestContextInterceptor; import com.example.solidconnection.common.resolver.AuthorizedUserResolver; import com.example.solidconnection.common.resolver.CustomPageableHandlerMethodArgumentResolver; import java.util.List; @@ -22,6 +23,7 @@ public class WebMvcConfig implements WebMvcConfigurer { private final CustomPageableHandlerMethodArgumentResolver customPageableHandlerMethodArgumentResolver; private final HttpLoggingFilter httpLoggingFilter; private final ApiPerformanceInterceptor apiPerformanceInterceptor; + private final RequestContextInterceptor requestContextInterceptor; @Override public void addArgumentResolvers(List resolvers) { @@ -35,6 +37,9 @@ public void addArgumentResolvers(List resolvers) public void addInterceptors(InterceptorRegistry registry){ registry.addInterceptor(apiPerformanceInterceptor) .addPathPatterns("/**"); + + registry.addInterceptor(requestContextInterceptor) + .addPathPatterns("/**"); } @Bean diff --git a/src/main/java/com/example/solidconnection/common/interceptor/RequestContext.java b/src/main/java/com/example/solidconnection/common/interceptor/RequestContext.java new file mode 100644 index 00000000..b38e4595 --- /dev/null +++ b/src/main/java/com/example/solidconnection/common/interceptor/RequestContext.java @@ -0,0 +1,16 @@ +package com.example.solidconnection.common.interceptor; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class RequestContext { + private final String httpMethod; + private final String bestMatchPath; + + @Builder + public RequestContext(String httpMethod, String bestMatchPath) { + this.httpMethod = httpMethod; + this.bestMatchPath = bestMatchPath; + } +} diff --git a/src/main/java/com/example/solidconnection/common/interceptor/RequestContextHolder.java b/src/main/java/com/example/solidconnection/common/interceptor/RequestContextHolder.java new file mode 100644 index 00000000..0c786bf1 --- /dev/null +++ b/src/main/java/com/example/solidconnection/common/interceptor/RequestContextHolder.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.common.interceptor; + +public class RequestContextHolder { + private static final ThreadLocal CONTEXT = new ThreadLocal<>(); + + public static void initContext(RequestContext requestContext) { + CONTEXT.remove(); + CONTEXT.set(requestContext); + } + + public static RequestContext getContext() { + return CONTEXT.get(); + } + + public static void clear(){ + CONTEXT.remove(); + } +} diff --git a/src/main/java/com/example/solidconnection/common/interceptor/RequestContextInterceptor.java b/src/main/java/com/example/solidconnection/common/interceptor/RequestContextInterceptor.java new file mode 100644 index 00000000..519d6522 --- /dev/null +++ b/src/main/java/com/example/solidconnection/common/interceptor/RequestContextInterceptor.java @@ -0,0 +1,35 @@ +package com.example.solidconnection.common.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.HandlerMapping; + +@Component +public class RequestContextInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler + ) { + String httpMethod = request.getMethod(); + String bestMatchPath = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); + + RequestContext context = new RequestContext(httpMethod, bestMatchPath); + RequestContextHolder.initContext(context); + + return true; + } + + @Override + public void afterCompletion( + HttpServletRequest request, + HttpServletResponse response, + Object handler, Exception ex + ) { + RequestContextHolder.clear(); + } +} diff --git a/src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java b/src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java new file mode 100644 index 00000000..3ab982b4 --- /dev/null +++ b/src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java @@ -0,0 +1,54 @@ +package com.example.solidconnection.common.listener; + +import com.example.solidconnection.common.interceptor.RequestContext; +import com.example.solidconnection.common.interceptor.RequestContextHolder; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import java.util.List; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import net.ttddyy.dsproxy.ExecutionInfo; +import net.ttddyy.dsproxy.QueryInfo; +import net.ttddyy.dsproxy.listener.QueryExecutionListener; +import org.springframework.stereotype.Component; + + +@RequiredArgsConstructor +@Component +public class QueryMetricsListener implements QueryExecutionListener { + + private final MeterRegistry meterRegistry; + + @Override + public void beforeQuery(ExecutionInfo executionInfo, List list) { + + } + + @Override + public void afterQuery(ExecutionInfo exec, List queries) { + long elapsedMs = exec.getElapsedTime(); + String sql = queries.isEmpty() ? "" : queries.get(0).getQuery(); + String type = guessType(sql); + + RequestContext rc = RequestContextHolder.getContext(); + String httpMethod = (rc != null && rc.getHttpMethod() != null) ? rc.getHttpMethod() : "-"; + String httpPath = (rc != null && rc.getBestMatchPath() != null) ? rc.getBestMatchPath() : "-"; + + Timer.builder("db.query") + .tag("sql_type", type) + .tag("http_method", httpMethod) + .tag("http_path", httpPath) + .register(meterRegistry) + .record(elapsedMs, TimeUnit.MILLISECONDS); + } + + private String guessType(String sql) { + if (sql == null) return "OTHER"; + String s = sql.trim().toUpperCase(); + if (s.startsWith("SELECT")) return "SELECT"; + if (s.startsWith("INSERT")) return "INSERT"; + if (s.startsWith("UPDATE")) return "UPDATE"; + if (s.startsWith("DELETE")) return "DELETE"; + return "UNKNOWN"; + } +} From d6793d3548da6e5595b184df5d680c819c1f63c0 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Fri, 2 Jan 2026 00:34:59 +0900 Subject: [PATCH 07/16] =?UTF-8?q?refactor:=20=EB=B9=84=ED=9A=A8=EC=9C=A8?= =?UTF-8?q?=EC=A0=81=EC=9D=B8=20Time=20=EB=B9=8C=EB=8D=94=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Time.builder 를 사용하면 매번 빌더를 생성하여 비효율적인 문제를 meterRegistry.timer 방식으로 해결 --- .../common/listener/QueryMetricsListener.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java b/src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java index 3ab982b4..2ec3e3b2 100644 --- a/src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java +++ b/src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java @@ -3,7 +3,6 @@ import com.example.solidconnection.common.interceptor.RequestContext; import com.example.solidconnection.common.interceptor.RequestContextHolder; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; import java.util.List; import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; @@ -34,11 +33,11 @@ public void afterQuery(ExecutionInfo exec, List queries) { String httpMethod = (rc != null && rc.getHttpMethod() != null) ? rc.getHttpMethod() : "-"; String httpPath = (rc != null && rc.getBestMatchPath() != null) ? rc.getBestMatchPath() : "-"; - Timer.builder("db.query") - .tag("sql_type", type) - .tag("http_method", httpMethod) - .tag("http_path", httpPath) - .register(meterRegistry) + meterRegistry.timer( + "db.query", + "sql_type", type, + "http_method", httpMethod, + "http_path", httpPath) .record(elapsedMs, TimeUnit.MILLISECONDS); } From db2c7f35de85033027a0ae088192cb0f8f8071e9 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Fri, 2 Jan 2026 00:46:35 +0900 Subject: [PATCH 08/16] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B9=85=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20HttpServeletRequest=20=EC=86=8D=EC=84=B1?= =?UTF-8?q?=EC=97=90=20userId=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/filter/TokenAuthenticationFilter.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/example/solidconnection/security/filter/TokenAuthenticationFilter.java b/src/main/java/com/example/solidconnection/security/filter/TokenAuthenticationFilter.java index 8c8dc8f3..6e1899dd 100644 --- a/src/main/java/com/example/solidconnection/security/filter/TokenAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/security/filter/TokenAuthenticationFilter.java @@ -2,6 +2,7 @@ import com.example.solidconnection.security.authentication.TokenAuthentication; import com.example.solidconnection.security.infrastructure.AuthorizationHeaderParser; +import com.example.solidconnection.security.userdetails.SiteUserDetails; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -34,6 +35,7 @@ public void doFilterInternal(@NonNull HttpServletRequest request, TokenAuthentication authToken = new TokenAuthentication(token); Authentication auth = authenticationManager.authenticate(authToken); SecurityContextHolder.getContext().setAuthentication(auth); + extractIdFromAuthentication(request, auth); }); filterChain.doFilter(request, response); @@ -45,4 +47,10 @@ private Optional resolveToken(HttpServletRequest request) { } return authorizationHeaderParser.parseToken(request); } + + private void extractIdFromAuthentication(HttpServletRequest request, Authentication auth) { + SiteUserDetails principal = (SiteUserDetails) auth.getPrincipal(); + Long id = principal.getSiteUser().getId(); + request.setAttribute("userId", id); + } } From 5a11264b2c12e86fc8d5fff5290965e8fd5b4dcd Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Fri, 2 Jan 2026 00:52:43 +0900 Subject: [PATCH 09/16] =?UTF-8?q?refactor:=20logback=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=A4=91=20local=EC=9D=80=20console=EB=A7=8C=20=EC=B0=8D?= =?UTF-8?q?=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 --- 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 a51603ff..a5a1f290 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -4,7 +4,7 @@ + value="timestamp=%d{yyyy-MM-dd'T'HH:mm:ss,Asia/Seoul} level=%-5level traceId=%X{traceId} %msg%n"/> From 1e4cfe2f5c1efdfb3f99f61b1aea809b979ab71d Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Fri, 2 Jan 2026 00:54:51 +0900 Subject: [PATCH 10/16] =?UTF-8?q?refactor:=20FILE=5FPATTERN=20->=20LOG=5FP?= =?UTF-8?q?ATTERN=20=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/logback-spring.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index a5a1f290..7328db0e 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -3,7 +3,7 @@ - @@ -18,7 +18,7 @@ 7 - ${FILE_PATTERN} + ${LOG_PATTERN} INFO @@ -35,7 +35,7 @@ 7 - ${FILE_PATTERN} + ${LOG_PATTERN} WARN @@ -52,7 +52,7 @@ 7 - ${FILE_PATTERN} + ${LOG_PATTERN} ERROR @@ -69,7 +69,7 @@ 7 - ${FILE_PATTERN} + ${LOG_PATTERN} From ac27d4f3ca6b335ee494ec62f3c8f13d7e33d820 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Fri, 2 Jan 2026 17:07:43 +0900 Subject: [PATCH 11/16] =?UTF-8?q?test:=20TokenAuthenticationFilter?= =?UTF-8?q?=EC=97=90=EC=84=9C=20request=EC=97=90=20userId=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - principal 조회 예외를 막기 위해 siteUserDetailsService given 추가 --- .../filter/TokenAuthenticationFilterTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java b/src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java index 36d8c3dd..db342847 100644 --- a/src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java +++ b/src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java @@ -1,12 +1,17 @@ package com.example.solidconnection.security.filter; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.spy; import com.example.solidconnection.auth.token.config.JwtProperties; import com.example.solidconnection.security.authentication.TokenAuthentication; +import com.example.solidconnection.security.userdetails.SiteUserDetails; import com.example.solidconnection.security.userdetails.SiteUserDetailsService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -33,6 +38,9 @@ class TokenAuthenticationFilterTest { @Autowired private JwtProperties jwtProperties; + @Autowired + private SiteUserFixture siteUserFixture; + @MockBean // 이 테스트코드에서 사용자를 조회할 필요는 없으므로 MockBean 으로 대체 private SiteUserDetailsService siteUserDetailsService; @@ -45,6 +53,11 @@ void setUp() { response = new MockHttpServletResponse(); filterChain = spy(FilterChain.class); SecurityContextHolder.clearContext(); + + SiteUser siteUser = siteUserFixture.사용자(1, "test"); + SiteUserDetails userDetails = new SiteUserDetails(siteUser); + given(siteUserDetailsService.loadUserByUsername(anyString())) + .willReturn(userDetails); } @Test @@ -76,6 +89,24 @@ void setUp() { then(filterChain).should().doFilter(request, response); } + @Test + void 토큰이_있으면_컨텍스트에_저장하고_userId를_request에_설정한다() throws Exception { + // given + Long expectedUserId = 1L; + Date validExpiration = new Date(System.currentTimeMillis() + 1000); + String token = createTokenWithExpiration(validExpiration); + request = createRequestWithToken(token); + + // when + tokenAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()) + .isExactlyInstanceOf(TokenAuthentication.class); + assertThat(request.getAttribute("userId")).isEqualTo(expectedUserId); + then(filterChain).should().doFilter(request, response); + } + private String createTokenWithExpiration(Date expiration) { return Jwts.builder() .setSubject("1") From 109a4e4d4c2cbdbc0061775e808adf4c2f4dd8f9 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Fri, 2 Jan 2026 17:19:41 +0900 Subject: [PATCH 12/16] =?UTF-8?q?refacotr:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=9E=98=EB=B9=97=20=EB=A6=AC=EB=B7=B0=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 중복되는 테스트 제거 * refactor: 사용하지 않는 필드 제거 --- .../common/filter/HttpLoggingFilter.java | 2 -- .../filter/TokenAuthenticationFilterTest.java | 16 ---------------- 2 files changed, 18 deletions(-) diff --git a/src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java b/src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java index 0a0d1c53..7b29f6b1 100644 --- a/src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java +++ b/src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java @@ -28,8 +28,6 @@ public class HttpLoggingFilter extends OncePerRequestFilter { "/actuator/**" ); - private final ObjectMapper objectMapper = new ObjectMapper(); - @Override protected void doFilterInternal( HttpServletRequest request, diff --git a/src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java b/src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java index db342847..d0b7d896 100644 --- a/src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java +++ b/src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java @@ -73,22 +73,6 @@ void setUp() { then(filterChain).should().doFilter(request, response); } - @Test - void 토큰이_있으면_컨텍스트에_저장한다() throws Exception { - // given - Date validExpiration = new Date(System.currentTimeMillis() + 1000); - String token = createTokenWithExpiration(validExpiration); - request = createRequestWithToken(token); - - // when - tokenAuthenticationFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(SecurityContextHolder.getContext().getAuthentication()) - .isExactlyInstanceOf(TokenAuthentication.class); - then(filterChain).should().doFilter(request, response); - } - @Test void 토큰이_있으면_컨텍스트에_저장하고_userId를_request에_설정한다() throws Exception { // given From 436256a650fda6d2d1f44d0851cf4ae76bcf1c76 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Sat, 3 Jan 2026 15:26:36 +0900 Subject: [PATCH 13/16] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: ApiPerformanceInterceptor에서 uri 정규화 관련 코드 제거 * refactor: ApiPerformanceInterceptor에서 if-return 문을 if-else 문으로 수정 * refactor: 추가한 interceptor 의 설정에 actuator 경로 무시하도록 셋팅 * refactor: 중복되는 의존성 제거 --- build.gradle | 1 - .../common/config/web/WebMvcConfig.java | 6 +++-- .../ApiPerformanceInterceptor.java | 24 +++++++------------ 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/build.gradle b/build.gradle index 958811c9..deefc611 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-websocket' // Database Proxy - implementation 'net.ttddyy:datasource-proxy:1.11.0' implementation 'net.ttddyy.observation:datasource-micrometer:1.2.0' } diff --git a/src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java b/src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java index fe6617e5..7e2f199c 100644 --- a/src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java +++ b/src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java @@ -36,10 +36,12 @@ public void addArgumentResolvers(List resolvers) @Override public void addInterceptors(InterceptorRegistry registry){ registry.addInterceptor(apiPerformanceInterceptor) - .addPathPatterns("/**"); + .addPathPatterns("/**") + .excludePathPatterns("/actuator/**"); registry.addInterceptor(requestContextInterceptor) - .addPathPatterns("/**"); + .addPathPatterns("/**") + .excludePathPatterns("/actuator/**"); } @Bean diff --git a/src/main/java/com/example/solidconnection/common/interceptor/ApiPerformanceInterceptor.java b/src/main/java/com/example/solidconnection/common/interceptor/ApiPerformanceInterceptor.java index f32a50a4..9af59d63 100644 --- a/src/main/java/com/example/solidconnection/common/interceptor/ApiPerformanceInterceptor.java +++ b/src/main/java/com/example/solidconnection/common/interceptor/ApiPerformanceInterceptor.java @@ -14,7 +14,6 @@ @RequiredArgsConstructor @Component public class ApiPerformanceInterceptor implements HandlerInterceptor { - private static final String ROUTE_PATTERN_ATTRIBUTE = "routePattern"; private static final String START_TIME_ATTRIBUTE = "startTime"; private static final String REQUEST_URI_ATTRIBUTE = "requestUri"; private static final int RESPONSE_TIME_THRESHOLD = 3_000; @@ -32,12 +31,6 @@ public boolean preHandle( request.setAttribute(START_TIME_ATTRIBUTE, startTime); request.setAttribute(REQUEST_URI_ATTRIBUTE, request.getRequestURI()); - Object bestMatchingPattern = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); - - if(bestMatchingPattern instanceof String s) { - request.setAttribute(ROUTE_PATTERN_ATTRIBUTE, s); - } - return true; } @@ -69,16 +62,17 @@ public void afterCompletion( method, uri, responseTime, status ); - return; } + else { + API_PERF.info( + "type=API_Performance method_type={} uri={} response_time={} status={}", + method, uri, responseTime, status + ); - API_PERF.info( - "type=API_Performance method_type={} uri={} response_time={} status={}", - method, uri, responseTime, status - ); + log.info("[API Performance]: {} {} - {}ms [Status: {}]", + method, uri, responseTime, status + ); + } - log.info("[API Performance]: {} {} - {}ms [Status: {}]", - method, uri, responseTime, status - ); } } From f12d81ef32b2b073db2ae08a6679202c289db4d8 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Tue, 6 Jan 2026 16:03:31 +0900 Subject: [PATCH 14/16] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=EC=8B=9C=20=EB=AF=BC=EA=B0=90=ED=95=9C=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EB=A7=88=EC=8A=A4?= =?UTF-8?q?=ED=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EXCLUDE_QUERIES 에 해당하는 쿼리 파라미터 KEY 값의 VALUE 를 masking 값으로 치환 * refactor: 예외 처리 후에도 Response 로그 찍도록 수정 --- .../common/filter/HttpLoggingFilter.java | 81 ++++++++++++++----- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java b/src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java index 7b29f6b1..47111d76 100644 --- a/src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java +++ b/src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java @@ -1,6 +1,5 @@ package com.example.solidconnection.common.filter; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -23,10 +22,9 @@ public class HttpLoggingFilter extends OncePerRequestFilter { private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher(); - - private static final List EXCLUDE_PATTERNS = List.of( - "/actuator/**" - ); + private static final List EXCLUDE_PATTERNS = List.of("/actuator/**"); + private static final List EXCLUDE_QUERIES = List.of("token"); + private static final String MASK_VALUE = "****"; @Override protected void doFilterInternal( @@ -55,12 +53,7 @@ protected void doFilterInternal( try { filterChain.doFilter(request, response); - - Boolean alreadyExceptionLogging = (Boolean) request.getAttribute("exceptionHandlerLogged"); - if (alreadyExceptionLogging == null || !alreadyExceptionLogging) { - printResponse(request, response); - } - + printResponse(request, response); } finally { MDC.clear(); } @@ -80,6 +73,12 @@ private String generateTraceId() { return java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 16); } + private void printRequestUri(HttpServletRequest request) { + String methodType = request.getMethod(); + String uri = buildDecodedRequestUri(request); + log.info("[REQUEST] {} {}", methodType, uri); + } + private void printResponse( HttpServletRequest request, HttpServletResponse response @@ -91,16 +90,25 @@ private void printResponse( log.info("[RESPONSE] {} userId = {}, ({})", uri, userId, status); } - private void printRequestUri(HttpServletRequest request) { - String methodType = request.getMethod(); - String uri = buildDecodedRequestUri(request); - log.info("[REQUEST] {} {}", methodType, uri); + private String buildDecodedRequestUri(HttpServletRequest request) { + String path = request.getRequestURI(); + String query = decodeQuery(request.getQueryString()); + + if(query == null || query.isBlank()){ + return path; + } + + String decodedQuery = decodeQuery(query); + String maskedQuery = maskSensitiveParams(decodedQuery); + + return path + "?" + maskedQuery; } private String decodeQuery(String rawQuery) { - if (rawQuery == null) { - return null; + if(rawQuery == null || rawQuery.isBlank()){ + return rawQuery; } + try { return URLDecoder.decode(rawQuery, StandardCharsets.UTF_8); } catch (IllegalArgumentException e) { @@ -108,9 +116,40 @@ private String decodeQuery(String rawQuery) { } } - private String buildDecodedRequestUri(HttpServletRequest request) { - String path = request.getRequestURI(); - String query = decodeQuery(request.getQueryString()); - return (query == null || query.isBlank()) ? path : path + "?" + query; + private String maskSensitiveParams(String decodedQuery) { + String[] params = decodedQuery.split("&"); + StringBuilder maskedQuery = new StringBuilder(); + + for(int i = 0; i < params.length; i++){ + String param = params[i]; + + if(!param.contains("=")){ + maskedQuery.append(param); + }else{ + int equalIndex = param.indexOf("="); + String key = param.substring(0, equalIndex); + + if(isSensitiveParam(key)){ + maskedQuery.append(key).append("=").append(MASK_VALUE); + }else{ + maskedQuery.append(param); + } + } + + if(i < params.length - 1){ + maskedQuery.append("&"); + } + } + + return maskedQuery.toString(); + } + + private boolean isSensitiveParam(String paramKey) { + for (String sensitiveParam : EXCLUDE_QUERIES){ + if(sensitiveParam.equalsIgnoreCase(paramKey)){ + return true; + } + } + return false; } } From 69022faef31d5c979920e14143191bbf3e83b83d Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Tue, 6 Jan 2026 16:06:17 +0900 Subject: [PATCH 15/16] =?UTF-8?q?refactor:=20CustomExceptionHandler=20?= =?UTF-8?q?=EC=9B=90=EC=83=81=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Response 로그를 통해 user를 추적할 수 있으므로 로그에 userId 를 추가하지 않습니다 --- .../exception/CustomExceptionHandler.java | 59 ++++--------------- 1 file changed, 12 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java b/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java index 6fb4ca3a..5700c304 100644 --- a/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java +++ b/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java @@ -9,7 +9,6 @@ import com.example.solidconnection.common.response.ErrorResponse; import com.fasterxml.jackson.databind.exc.InvalidFormatException; import io.jsonwebtoken.JwtException; -import jakarta.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; import lombok.extern.slf4j.Slf4j; @@ -25,13 +24,8 @@ public class CustomExceptionHandler { @ExceptionHandler(CustomException.class) - protected ResponseEntity handleCustomException( - CustomException ex, - HttpServletRequest request - ) { - request.setAttribute("exceptionHandlerLogged", true); - Long userId = (Long) request.getAttribute("userId"); - log.error("커스텀 예외 발생 userId : {} msg: {}", userId, ex.getMessage()); + protected ResponseEntity handleCustomException(CustomException ex) { + log.error("커스텀 예외 발생 : {}", ex.getMessage()); ErrorResponse errorResponse = new ErrorResponse(ex); return ResponseEntity .status(ex.getCode()) @@ -39,14 +33,9 @@ protected ResponseEntity handleCustomException( } @ExceptionHandler(InvalidFormatException.class) - public ResponseEntity handleInvalidFormatException( - InvalidFormatException ex, - HttpServletRequest request - ) { - request.setAttribute("exceptionHandlerLogged", true); - Long userId = (Long) request.getAttribute("userId"); + public ResponseEntity handleInvalidFormatException(InvalidFormatException ex) { String errorMessage = ex.getValue() + " 은(는) 유효하지 않은 값입니다."; - log.error("JSON 파싱 예외 발생 userId: {} msg: {}", userId, errorMessage); + log.error("JSON 파싱 예외 발생 : {}", errorMessage); ErrorResponse errorResponse = new ErrorResponse(JSON_PARSING_FAILED, errorMessage); return ResponseEntity .status(JSON_PARSING_FAILED.getCode()) @@ -54,20 +43,14 @@ public ResponseEntity handleInvalidFormatException( } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleValidationExceptions( - MethodArgumentNotValidException ex, - HttpServletRequest request - ) { - request.setAttribute("exceptionHandlerLogged", true); - Long userId = (Long) request.getAttribute("userId"); - + public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { List errors = new ArrayList<>(); ex.getBindingResult() .getFieldErrors() .forEach(fieldError -> errors.add(fieldError.getDefaultMessage())); String errorMessage = errors.toString(); - log.error("입력값 검증 예외 발생 userId : {} msg: {}", userId, errorMessage); + log.error("입력값 검증 예외 발생 : {}", errorMessage); ErrorResponse errorResponse = new ErrorResponse(INVALID_INPUT, errorMessage); return ResponseEntity .status(HttpStatus.BAD_REQUEST) @@ -75,14 +58,8 @@ public ResponseEntity handleValidationExceptions( } @ExceptionHandler(DataIntegrityViolationException.class) - public ResponseEntity handleDataIntegrityViolationException( - DataIntegrityViolationException ex, - HttpServletRequest request - ) { - request.setAttribute("exceptionHandlerLogged", true); - Long userId = (Long) request.getAttribute("userId"); - - log.error("데이터 무결성 제약조건 위반 예외 발생 userId : {} msg : {}", userId, ex.getMessage()); + public ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException ex) { + log.error("데이터 무결성 제약조건 위반 예외 발생 : {}", ex.getMessage()); ErrorResponse errorResponse = new ErrorResponse(DATA_INTEGRITY_VIOLATION, "데이터 무결성 제약조건 위반 예외 발생"); return ResponseEntity .status(DATA_INTEGRITY_VIOLATION.getCode()) @@ -90,15 +67,9 @@ public ResponseEntity handleDataIntegrityViolationException( } @ExceptionHandler(JwtException.class) - public ResponseEntity handleJwtException( - JwtException ex, - HttpServletRequest request - ) { - request.setAttribute("exceptionHandlerLogged", true); - Long userId = (Long) request.getAttribute("userId"); - + public ResponseEntity handleJwtException(JwtException ex) { String errorMessage = ex.getMessage(); - log.error("JWT 예외 발생 userId : {} msg : {}", userId, errorMessage); + log.error("JWT 예외 발생 : {}", errorMessage); ErrorResponse errorResponse = new ErrorResponse(JWT_EXCEPTION, errorMessage); return ResponseEntity .status(HttpStatus.BAD_REQUEST) @@ -106,15 +77,9 @@ public ResponseEntity handleJwtException( } @ExceptionHandler(Exception.class) - public ResponseEntity handleOtherException( - Exception ex, - HttpServletRequest request - ) { - request.setAttribute("exceptionHandlerLogged", true); - Long userId = (Long) request.getAttribute("userId"); - + public ResponseEntity handleOtherException(Exception ex) { String errorMessage = ex.getMessage(); - log.error("서버 내부 예외 발생 userId : {} , msg : {}", userId, errorMessage); + log.error("서버 내부 예외 발생 : {}", errorMessage); ErrorResponse errorResponse = new ErrorResponse(NOT_DEFINED_ERROR, errorMessage); return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) From 4d1ffdc0c41fe973fde9b80430256929b6fd8c6c Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Tue, 6 Jan 2026 16:10:22 +0900 Subject: [PATCH 16/16] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: RequestContext 빌더 제거 * refactor: RequestContextInterceptor import 수정 * refactor: logback yml 파일에서 timestamp 서버 시간과 동일한 규격으로 수정 * refactor: ApiPerformanceInterceptor 에서 동일 내용 로그 중복으로 찍는 문제 수정 --- .../common/interceptor/ApiPerformanceInterceptor.java | 11 ----------- .../common/interceptor/RequestContext.java | 2 -- .../common/interceptor/RequestContextInterceptor.java | 5 +++-- .../common/listener/QueryMetricsListener.java | 4 ++-- src/main/resources/logback-spring.xml | 2 +- 5 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/example/solidconnection/common/interceptor/ApiPerformanceInterceptor.java b/src/main/java/com/example/solidconnection/common/interceptor/ApiPerformanceInterceptor.java index 9af59d63..50a95f93 100644 --- a/src/main/java/com/example/solidconnection/common/interceptor/ApiPerformanceInterceptor.java +++ b/src/main/java/com/example/solidconnection/common/interceptor/ApiPerformanceInterceptor.java @@ -8,7 +8,6 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.HandlerMapping; @Slf4j @RequiredArgsConstructor @@ -57,22 +56,12 @@ public void afterCompletion( "type=API_Performance method_type={} uri={} response_time={} status={}", method, uri, responseTime, status ); - - log.warn("[API Performance] {} {} - {}ms [Status: {}]", - method, uri, responseTime, status - ); - } else { API_PERF.info( "type=API_Performance method_type={} uri={} response_time={} status={}", method, uri, responseTime, status ); - - log.info("[API Performance]: {} {} - {}ms [Status: {}]", - method, uri, responseTime, status - ); } - } } diff --git a/src/main/java/com/example/solidconnection/common/interceptor/RequestContext.java b/src/main/java/com/example/solidconnection/common/interceptor/RequestContext.java index b38e4595..1f4d2790 100644 --- a/src/main/java/com/example/solidconnection/common/interceptor/RequestContext.java +++ b/src/main/java/com/example/solidconnection/common/interceptor/RequestContext.java @@ -1,6 +1,5 @@ package com.example.solidconnection.common.interceptor; -import lombok.Builder; import lombok.Getter; @Getter @@ -8,7 +7,6 @@ public class RequestContext { private final String httpMethod; private final String bestMatchPath; - @Builder public RequestContext(String httpMethod, String bestMatchPath) { this.httpMethod = httpMethod; this.bestMatchPath = bestMatchPath; diff --git a/src/main/java/com/example/solidconnection/common/interceptor/RequestContextInterceptor.java b/src/main/java/com/example/solidconnection/common/interceptor/RequestContextInterceptor.java index 519d6522..e42b14e1 100644 --- a/src/main/java/com/example/solidconnection/common/interceptor/RequestContextInterceptor.java +++ b/src/main/java/com/example/solidconnection/common/interceptor/RequestContextInterceptor.java @@ -1,10 +1,11 @@ package com.example.solidconnection.common.interceptor; +import static org.springframework.web.servlet.HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE; + import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.HandlerMapping; @Component public class RequestContextInterceptor implements HandlerInterceptor { @@ -16,7 +17,7 @@ public boolean preHandle( Object handler ) { String httpMethod = request.getMethod(); - String bestMatchPath = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); + String bestMatchPath = (String) request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE); RequestContext context = new RequestContext(httpMethod, bestMatchPath); RequestContextHolder.initContext(context); diff --git a/src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java b/src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java index 2ec3e3b2..8f3258b6 100644 --- a/src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java +++ b/src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java @@ -37,8 +37,8 @@ public void afterQuery(ExecutionInfo exec, List queries) { "db.query", "sql_type", type, "http_method", httpMethod, - "http_path", httpPath) - .record(elapsedMs, TimeUnit.MILLISECONDS); + "http_path", httpPath + ).record(elapsedMs, TimeUnit.MILLISECONDS); } private String guessType(String sql) { diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 7328db0e..52d0bb4e 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -4,7 +4,7 @@ + value="timestamp=%d{yyyy-MM-dd'T'HH:mm:ss,UTC} level=%-5level traceId=%X{traceId:-null} %msg%n"/>