Skip to content

Conversation

@sukangpunch
Copy link
Contributor

@sukangpunch sukangpunch commented Jan 1, 2026

관련 이슈

작업 내용

1. 로깅 필터 추가
- traceId 와 request response 로깅을 통해 , api의 시작과 끝에 대한 로그를 추적하기 쉽게 하기 위해 추가하였습니다.

2. Api 수행시간 interceptor
- api 별 수행시간을 로그로 남기기 위해 추가하였습니다.

3. 데이터 소스 프록시 추가 및 쿼리 별 수행시간 메트릭 저장 기능 추가
- 각 api 에서 발생하는 쿼리들의 수행 시간을 메트릭으로 전송합니다.
- 로그로 남기기엔 너무 용량이 커질 듯 하여 메트릭만 전송합니다.

4. 로그 파일 전략 수정
- 로그 폴더를 info, warn, error, api_perf 로 관리하여 로그 파일의 복잡도를 낮추는 방향으로 수정 해 보았습니다.

특이 사항

  1. log 파일 저장 구조가 변경되어 alloy 파일의 수정이 필요합니다.

  2. 쿼리 메트릭을 대시 보드에서 찍기 위한 설정이 필요합니다.

리뷰 요구사항 (선택)

- traceId 기반 요청 추적
- 요청/응답 로깅
- CustomExceptionHandler와 중복 로깅 방지
- Actuator 엔드포인트 로깅 제외
- info, warn, error, api_perf 로 로그 파일 분리해서 관리
* feat: 데이터소스 프록시 의존성 및 config 파일 추가

* feat: 데이터 소스 프록시가 metric을 찍을 수 있도록 listener 클래스 추가

* feat: 요청 시 method, uri 정보를 listener에서 활용하기 위해 RequestContext 및 관련 interceptor 추가
- Time.builder 를 사용하면 매번 빌더를 생성하여 비효율적인 문제를 meterRegistry.timer 방식으로 해결
@coderabbitai
Copy link

coderabbitai bot commented Jan 1, 2026

Walkthrough

  1. DataSource 프록시 추가. DataSource를 ProxyDataSourceBuilder로 감싸 QueryMetricsListener를 등록했습니다.
  2. HTTP 로깅 필터 추가. OncePerRequestFilter 기반 HttpLoggingFilter가 traceId와 요청/응답 로그를 생성합니다.
  3. API 성능 인터셉터 추가. 요청 시간 측정 및 임계치 기반 경고/정보 로깅을 수행합니다.
  4. 요청 컨텍스트 관리 추가. RequestContext/RequestContextHolder와 RequestContextInterceptor로 메서드·경로를 ThreadLocal에 저장합니다.
  5. DB 쿼리 리스너 추가. QueryMetricsListener가 쿼리 타입·지연시간을 Micrometer에 기록합니다.
  6. Web MVC 구성 업데이트. 새로운 필터와 인터셉터들을 WebMvcConfig에 등록했습니다.
  7. 예외 핸들러 개선. 모든 핸들러에 HttpServletRequest를 주입해 userId와 함께 로깅합니다.
  8. 인증 필터 변경. 인증 성공 시 request 속성에 userId를 설정합니다.
  9. 로깅 설정 변경. logback 설정을 프로필 기반 레벨별 파일 롤링 및 API_PERF 로거로 분리했습니다.
  10. 빌드 의존성 및 테스트 보강. datasource-proxy 및 datasource-micrometer 의존성 추가와 TokenAuthenticationFilter 테스트에 userId 검증을 추가했습니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • wibaek
  • whqtker
  • Hexeong
  • Gyuhyeok99

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 'API 성능 로깅, 쿼리 별 메트릭 전송 추가'로 핵심 기능을 정확히 요약하고 있으며, 변경 사항의 주요 목표와 일치합니다.
Description check ✅ Passed PR 설명이 템플릿 구조에 따라 관련 이슈, 작업 내용(4가지 기능), 특이 사항을 모두 포함하고 있습니다.
Linked Issues check ✅ Passed 코드 변경 사항은 이슈 #601의 모든 요구 사항을 충족합니다: 로깅 필터, API 성능 인터셉터, 쿼리 메트릭 전송 기능이 모두 구현되어 있습니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 이슈 #601의 범위 내에 있으며, 로깅, 인터셉터, 메트릭 수집 관련 기능만 포함되어 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (7)
src/main/java/com/example/solidconnection/security/filter/TokenAuthenticationFilter.java (1)

51-55: Principal 타입 체크 추가를 고려해주세요.

변경 내용:

  1. Authentication 객체에서 Principal을 SiteUserDetails로 캐스팅
  2. userId를 추출하여 request attribute에 저장

잠재적 이슈:

  • Line 52에서 auth.getPrincipal()SiteUserDetails로 무조건 캐스팅하면 다른 타입의 principal인 경우 ClassCastException 발생 가능

일반적인 흐름에서는 TokenAuthentication이 항상 SiteUserDetails를 반환하겠지만, 방어적 프로그래밍 관점에서 타입 체크를 추가하는 것을 권장합니다.

🔎 방어적 타입 체크 추가 제안
 private void extractIdFromAuthentication(HttpServletRequest request, Authentication auth) {
+    Object principal = auth.getPrincipal();
+    if (!(principal instanceof SiteUserDetails)) {
+        return;
+    }
-    SiteUserDetails principal = (SiteUserDetails) auth.getPrincipal();
+    SiteUserDetails userDetails = (SiteUserDetails) principal;
-    Long id = principal.getSiteUser().getId();
+    Long id = userDetails.getSiteUser().getId();
     request.setAttribute("userId", id);
 }
src/main/java/com/example/solidconnection/common/interceptor/ApiPerformanceInterceptor.java (2)

17-19: 사용되지 않는 ROUTE_PATTERN_ATTRIBUTE 정리가 필요합니다.

변경 내용:

  1. Line 17에서 ROUTE_PATTERN_ATTRIBUTE 상수 정의
  2. Line 38에서 해당 attribute를 request에 설정

이슈:

  • ROUTE_PATTERN_ATTRIBUTE가 저장되지만 afterCompletion에서 읽혀지지 않습니다
  • Lines 58-59에서 request.getRequestURI()를 직접 사용
  • 향후 사용을 위해 준비된 것이거나 리팩터링 과정에서 남은 코드일 수 있습니다

코드 정리를 위해 사용하지 않는 경우 제거하거나, 사용할 계획이라면 주석으로 의도를 명시하는 것을 권장합니다.

Also applies to: 38-39


44-83: API 성능 로깅 로직이 효과적으로 구현되었습니다.

변경 내용을 정리하면:

  1. 요청 시작부터 완료까지의 응답 시간 계산
  2. 3초(3000ms) 임계값을 기준으로 warn/info 레벨 분리
  3. API_PERF 로거와 표준 로거 두 곳에 동시 기록
    • API_PERF: 구조화된 형식으로 파싱 용이하게 기록
    • 표준 로거: 사람이 읽기 쉬운 형식으로 기록

구현이 올바르며, logback-spring.xml의 분리된 로그 파일 전략과 잘 통합되어 있습니다.

선택적 개선사항:

  • Lines 63-66과 75-78, 그리고 68-70과 80-82에 중복된 로깅 코드가 있습니다
  • 로깅 메서드를 별도로 추출하면 코드 중복을 줄이고 유지보수성을 높일 수 있습니다
🔎 로깅 중복 제거 리팩터링 제안
+    private void logPerformance(String method, String uri, long responseTime, int status, boolean isWarning) {
+        String logLevel = isWarning ? "WARN" : "INFO";
+        String apiPerfMessage = "type=API_Performance method_type={} uri={} response_time={} status={}";
+        String standardMessage = "[API Performance]: {} {} - {}ms [Status: {}]";
+        
+        if (isWarning) {
+            API_PERF.warn(apiPerfMessage, method, uri, responseTime, status);
+            log.warn(standardMessage, method, uri, responseTime, status);
+        } else {
+            API_PERF.info(apiPerfMessage, method, uri, responseTime, status);
+            log.info(standardMessage, method, uri, responseTime, status);
+        }
+    }
+
     @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
-        );
+        boolean isWarning = responseTime > RESPONSE_TIME_THRESHOLD;
+        logPerformance(method, uri, responseTime, status, isWarning);
     }
src/main/resources/logback-spring.xml (1)

14-28: 시간 기반 롤링 정책에 크기 제한 추가를 권장합니다.

현재는 일별로만 로그 파일이 분리되는데, 트래픽이 많은 경우 하루 동안 로그 파일이 무제한으로 커질 수 있습니다. 다음과 같이 크기 제한을 추가하는 것을 권장합니다:

🔎 크기 기반 롤링 정책 추가 예시

INFO_FILE 앱더를 예로 들면:

 <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
     <file>${LOG_PATH}/info/info.log</file>
-    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
-        <fileNamePattern>${LOG_PATH}/info/info.%d{yyyy-MM-dd}.log</fileNamePattern>
+    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+        <fileNamePattern>${LOG_PATH}/info/info.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+        <maxFileSize>100MB</maxFileSize>
         <maxHistory>7</maxHistory>
+        <totalSizeCap>3GB</totalSizeCap>
     </rollingPolicy>

WARN_FILE, ERROR_FILE 앱더에도 동일하게 적용하시면 됩니다.

Also applies to: 31-45, 48-62

src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java (1)

28-30: 배치 쿼리에서 첫 번째 쿼리만 사용됩니다.

Line 29에서 queries.get(0)로 첫 번째 쿼리의 SQL만 가져와 타입을 판단합니다. 배치 쿼리의 경우 나머지 쿼리들의 타입 정보가 손실됩니다.

대부분의 경우 배치 내 쿼리들이 동일한 타입일 것으로 예상되므로 현재 구현도 합리적이지만, 필요하다면 각 쿼리별로 별도 메트릭을 기록하는 것도 고려해볼 수 있습니다.

src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java (1)

33-34: 사용자 컨텍스트 로깅이 효과적으로 추가되었습니다!

모든 예외 핸들러에서 userId를 로깅하여 에러 발생 시 사용자 추적이 가능해졌습니다:

  1. 일관된 패턴: 6개 모든 핸들러에서 동일한 방식으로 userId 로깅
  2. 예외 핸들러별 적용:
    • 커스텀 예외 (line 33-34)
    • JSON 파싱 예외 (line 47-49)
    • 입력값 검증 예외 (line 62, 70)
    • 데이터 무결성 예외 (line 83-85)
    • JWT 예외 (line 98-101)
    • 서버 내부 예외 (line 114-117)

참고: userIdnull일 수 있지만 (인증되지 않은 요청), 로그에 "null"로 표시되는 것은 허용 가능하며 원본 예외를 숨기지 않으므로 현재 구현이 적절합니다.

🔎 (선택사항) 가독성 개선 제안

로그 가독성을 약간 높이려면 다음과 같이 null 처리를 명시적으로 할 수 있습니다:

-Long userId = (Long) request.getAttribute("userId");
+Long userId = (Long) request.getAttribute("userId");
+String userIdStr = userId != null ? userId.toString() : "anonymous";
-log.error("커스텀 예외 발생 userId : {} msg: {}", userId, ex.getMessage());
+log.error("커스텀 예외 발생 userId : {} msg: {}", userIdStr, ex.getMessage());

다만 현재 구현도 충분히 명확하므로 이는 선택적 개선사항입니다.

Also applies to: 47-49, 62-62, 70-70, 83-85, 98-101, 114-117

src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java (1)

36-43: 인터셉터 등록 순서 개선 제안

현재 등록 순서는 기능상 문제없이 작동하고 있습니다. 다만 다음과 같은 이유로 순서 조정을 검토할 만한 가치가 있습니다:

  1. 현재 동작 분석:

    • ApiPerformanceInterceptor.preHandle()은 단순히 타이밍 정보만 기록하며, RequestContext에 의존하지 않습니다
    • RequestContextInterceptor.preHandle()이 실행되는 시점이 컨트롤러 실행 전이므로, 쿼리 실행 시점에는 이미 RequestContext가 설정되어 있습니다
  2. 개선 제안 이유:

    • 의존성 설정 후 사용하는 논리적 순서로 정렬하면 코드의 의도가 더 명확해집니다
    • RequestContextInterceptorApiPerformanceInterceptor 순서가 "컨텍스트 준비 → 성능 측정 시작"의 흐름을 더 직관적으로 나타냅니다
  3. 등록 순서 변경 권장:

    registry.addInterceptor(requestContextInterceptor)
            .addPathPatterns("/**");
    
    registry.addInterceptor(apiPerformanceInterceptor)
            .addPathPatterns("/**");
    

기능상 필수는 아니지만, 코드 가독성과 유지보수성 측면에서 순서 조정을 권장합니다.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1cc8c3 and 1e4cfe2.

📒 Files selected for processing (12)
  • build.gradle
  • src/main/java/com/example/solidconnection/common/config/datasource/DataSourceProxyConfig.java
  • src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java
  • src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java
  • src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java
  • src/main/java/com/example/solidconnection/common/interceptor/ApiPerformanceInterceptor.java
  • src/main/java/com/example/solidconnection/common/interceptor/RequestContext.java
  • src/main/java/com/example/solidconnection/common/interceptor/RequestContextHolder.java
  • src/main/java/com/example/solidconnection/common/interceptor/RequestContextInterceptor.java
  • src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java
  • src/main/java/com/example/solidconnection/security/filter/TokenAuthenticationFilter.java
  • src/main/resources/logback-spring.xml
🧰 Additional context used
🧬 Code graph analysis (5)
src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java (1)
src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java (1)
  • Slf4j (23-123)
src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java (2)
src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java (1)
  • RequiredArgsConstructor (15-53)
src/main/java/com/example/solidconnection/common/config/datasource/DataSourceProxyConfig.java (1)
  • RequiredArgsConstructor (12-29)
src/main/java/com/example/solidconnection/common/interceptor/RequestContextInterceptor.java (1)
src/main/java/com/example/solidconnection/common/interceptor/RequestContextHolder.java (1)
  • RequestContextHolder (3-18)
src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java (3)
src/main/java/com/example/solidconnection/common/interceptor/RequestContextHolder.java (1)
  • RequestContextHolder (3-18)
src/main/java/com/example/solidconnection/common/config/datasource/DataSourceProxyConfig.java (1)
  • RequiredArgsConstructor (12-29)
src/main/java/com/example/solidconnection/common/interceptor/RequestContextInterceptor.java (1)
  • Component (9-35)
src/main/java/com/example/solidconnection/security/filter/TokenAuthenticationFilter.java (1)
src/main/java/com/example/solidconnection/security/userdetails/SiteUserDetails.java (1)
  • SiteUserDetails (9-56)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (19)
src/main/java/com/example/solidconnection/common/config/datasource/DataSourceProxyConfig.java (2)

12-29: 데이터소스 프록시 설정 잘 구현되었습니다.

변경 내용을 정리하면:

  1. @Primary 어노테이션을 사용하여 프록시된 DataSource가 애플리케이션 전체에서 사용되도록 보장
  2. QueryMetricsListener를 생성자 주입으로 받아 프록시에 연결
  3. ProxyDataSourceBuilder를 사용하여 기존 DataSource를 래핑

쿼리 메트릭 수집을 위한 구조가 명확하고 올바르게 구현되었습니다.


20-28: 연결 풀 설정이 프록시를 통해 올바르게 전달됩니다.

검증 결과, 현재 구현이 완벽하게 올바른 패턴을 따르고 있습니다:

  1. DataSourceProperties에서 설정이 먼저 적용됩니다

    • props.initializeDataSourceBuilder().build()는 application.yml의 모든 HikariCP 설정을 읽어 DataSource에 적용합니다
  2. 프록시 래핑은 투명하게 동작합니다

    • ProxyDataSourceBuilder(datasource-proxy 1.11.0)는 이미 완전히 구성된 DataSource를 감싼 투명한 데코레이터입니다
    • 연결 풀의 설정을 변경하거나 손실하지 않습니다
  3. Spring Boot 3.1.5 + HikariCP 기본 포함

    • DataSource는 기본으로 HikariCP를 사용하며, 모든 풀 설정(maxPoolSize, minimumIdle, timeout 등)이 보존됩니다

코드는 Spring Boot 모범 사례를 정확히 따르고 있으므로 추가 조치가 필요하지 않습니다.

src/main/java/com/example/solidconnection/security/filter/TokenAuthenticationFilter.java (1)

38-38: 인증 성공 후 userId 추출 로직 위치가 적절합니다.

변경 내용:

  1. 인증이 성공하고 SecurityContext에 설정된 직후에 userId를 추출하여 request attribute로 설정
  2. 이를 통해 HttpLoggingFilter, CustomExceptionHandler 등 다운스트림 컴포넌트에서 userId 활용 가능

요청 추적 및 로깅 인프라와 잘 통합되었습니다.

src/main/java/com/example/solidconnection/common/interceptor/RequestContextInterceptor.java (2)

12-25: 요청 컨텍스트 초기화 로직이 잘 구현되었습니다.

변경 내용을 정리하면:

  1. preHandle에서 HTTP method와 best matching path를 캡처
  2. RequestContext 객체를 생성하여 RequestContextHolder의 ThreadLocal에 저장
  3. 이를 통해 QueryMetricsListener가 쿼리 메트릭에 HTTP 컨텍스트를 태깅 가능

참고사항:

  • Line 19의 bestMatchPath는 핸들러 매핑이 없는 경우(정적 리소스, 에러 등) null일 수 있습니다
  • QueryMetricsListener에서 null 체크 후 "-"로 대체하므로 안전하게 처리됩니다

27-34: ThreadLocal 정리가 올바르게 구현되었습니다.

변경 내용:

  1. afterCompletion에서 RequestContextHolder.clear() 호출
  2. 요청 처리 완료 후 ThreadLocal 값을 제거하여 메모리 누수 방지

중요:

  • afterCompletion은 예외 발생 시에도 호출되므로 ThreadLocal 정리에 적합한 위치입니다
  • ThreadLocal 사용 시 반드시 필요한 정리 로직이 정확하게 구현되었습니다
src/main/java/com/example/solidconnection/common/interceptor/ApiPerformanceInterceptor.java (1)

23-42: 요청 시작 시점 기록 로직이 적절합니다.

변경 내용을 정리하면:

  1. 요청 시작 시간(startTime)을 기록하여 나중에 응답 시간 계산
  2. Request URI와 best matching pattern을 attribute에 저장
  3. afterCompletion에서 성능 메트릭 계산에 사용

구현이 명확하고 올바르게 작동합니다.

src/main/java/com/example/solidconnection/common/interceptor/RequestContext.java (1)

1-16: LGTM! 잘 설계된 불변 컨텍스트 객체입니다.

HTTP 메서드와 경로를 담는 단순한 불변 데이터 홀더로, ThreadLocal 환경에서 안전하게 사용할 수 있도록 잘 설계되었습니다.

src/main/resources/logback-spring.xml (2)

6-7: 구조화된 로그 패턴으로 파싱이 용이합니다.

traceId를 포함한 키-값 형식의 로그 패턴은 로그 수집 시스템(예: Loki, ELK)에서 파싱하기 좋은 형태입니다.


65-79: API 성능 로그 분리가 잘 구성되었습니다.

additivity=false 설정으로 중복 로깅을 방지하고, 별도 파일로 분리하여 성능 분석이 용이하도록 잘 구성되었습니다.

PR 목표에서 언급한 대로 Alloy 설정 변경이 필요합니다. 다음 항목을 확인해주세요:

  1. /var/log/spring 디렉터리 및 하위 폴더(info/, warn/, error/, api-perf/) 생성 및 권한 설정
  2. Alloy 설정 파일에서 새로운 로그 경로 반영
  3. 디스크 용량 모니터링 설정 (로그 4개 스트림 * 7일 보관)
src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java (3)

85-94: userId 로깅이 개인정보 보호 정책을 준수하는지 확인이 필요합니다.

응답 로그에 userId를 기록하고 있습니다. 다음 사항을 검토해주세요:

  1. 규정 준수: GDPR, CCPA 등 적용 가능한 개인정보 보호 규정에서 userId 로깅을 허용하는지 확인
  2. 로그 보관 정책: 7일간 보관되는 로그에 userId가 포함되어도 문제없는지 확인
  3. 접근 통제: /var/log/spring 디렉터리에 대한 접근 권한이 적절히 제한되어 있는지 확인

userId가 민감 정보로 분류되지 않는다면 문제없지만, 조직의 정책을 확인하는 것이 좋습니다.


40-54: traceId 관리 및 예외 안전성이 우수합니다.

다음 항목들이 잘 구현되어 있습니다:

  1. traceId 생성: 16자 hex 문자열로 충분한 고유성 보장
  2. 제외 경로 처리: Actuator 엔드포인트는 로깅을 건너뛰지만 traceId는 유지하여 하위 레이어 로깅 지원
  3. MDC 정리: finally 블록에서 MDC.clear() 호출로 ThreadLocal 누수 방지
  4. 중복 로깅 방지: exceptionHandlerLogged 속성으로 CustomExceptionHandler와의 중복 방지

Also applies to: 56-69


102-117: URL 디코딩 오류 처리가 안전하게 구현되었습니다.

잘못된 URL 인코딩으로 인한 IllegalArgumentException을 catch하여 원본 쿼리 스트링을 반환하는 방식이 적절합니다.

src/main/java/com/example/solidconnection/common/listener/QueryMetricsListener.java (2)

44-52: SQL 타입 분류 로직이 적절합니다.

간단한 prefix 매칭으로 주요 SQL 타입을 분류하고 있으며, 메트릭 수집 목적으로는 충분합니다. null 처리도 안전하게 구현되어 있습니다.


27-42: 해당 우려사항들은 코드에서 실제 문제가 아닙니다.

원본 리뷰의 주요 지적사항들을 검증한 결과, 다음과 같은 사실을 확인했습니다:

  1. 경로 카디널리티 위험은 존재하지 않습니다

    • bestMatchPath는 Spring Framework의 HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE에서 가져온 값으로, /users/{id} 같은 패턴입니다
    • 실제 요청 경로(/users/123 같은 값)가 아니므로 카디널리티가 제한됩니다
    • 메트릭 저장소에 미치는 영향은 미미합니다
  2. 타이머 인스턴스 캐싱 최적화는 불필요합니다

    • Micrometer는 메서드 이름과 태그 조합(Meter.Id)으로 타이머를 내부적으로 캐싱합니다
    • meterRegistry.timer()를 동일한 태그로 여러 번 호출해도 캐시된 인스턴스를 반환합니다
    • 별도의 Map 기반 캐시는 필요하지 않습니다

현재 구현은 성능 및 카디널리티 측면에서 안전합니다.

Likely an incorrect or invalid review comment.

src/main/java/com/example/solidconnection/common/interceptor/RequestContextHolder.java (1)

4-17: ThreadLocal 정리는 안전하게 이루어집니다.

검증 결과, 현재 구현은 메모리 누수 없이 안전하게 작동합니다:

  1. 정리 보장 ✓

    • RequestContextInterceptorafterCompletion() 메서드는 Spring의 HandlerInterceptor 계약에 따라 요청 처리 완료 시 항상 호출됩니다
    • 예외 발생 시에도 보장되므로 clear()가 반드시 실행됩니다
  2. 비동기 작업 처리 ✓

    • @Async 메서드들(UpdateViewCountScheduler, FileUploadService 등)은 별도 스레드에서 실행되지만, RequestContextHolder를 사용하지 않습니다
    • RequestContextHolder.getContext()QueryMetricsListener에서만 호출되며, 이는 요청 스레드 내에서 작동하는 Hibernate 쿼리 리스너입니다
    • 따라서 비동기 작업과의 상호작용이 없어 ThreadLocal 누수 위험이 없습니다
  3. 인터셉터 등록 ✓

    • RequestContextInterceptorWebMvcConfig에서 addPathPatterns("/**")로 등록되어 모든 경로를 포괄합니다
src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java (2)

3-5: 관찰성 컴포넌트 의존성 주입이 적절합니다!

새로운 필터와 인터셉터들이 깔끔하게 추가되었습니다:

  1. HttpLoggingFilter - 요청/응답 로깅
  2. ApiPerformanceInterceptor - API 성능 측정
  3. RequestContextInterceptor - 요청 컨텍스트 전파

생성자 주입 방식으로 의존성이 명확하게 관리되고 있어 좋습니다.

Also applies to: 10-11, 13-13, 15-15, 24-26


45-51: 로깅 필터 등록이 적절합니다!

HttpLoggingFilterHIGHEST_PRECEDENCE로 등록한 것이 좋습니다:

  1. 우선순위 설정: 가장 높은 우선순위로 설정되어 모든 요청/응답을 빠짐없이 로깅할 수 있습니다
  2. 명시적 등록: FilterRegistrationBean을 사용하여 필터 설정을 명확하게 제어하고 있습니다
  3. traceId 기반 추적: PR 목표에 맞게 요청 시작부터 종료까지 추적이 가능하도록 구성되었습니다
src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java (2)

28-31: 예외 핸들러에 HttpServletRequest 추가가 일관되게 적용되었습니다!

모든 예외 핸들러에 HttpServletRequest가 추가되어 요청 컨텍스트에 접근할 수 있게 되었습니다:

  1. handleCustomException (line 28-31)
  2. handleInvalidFormatException (line 42-45)
  3. handleValidationExceptions (line 57-60)
  4. handleDataIntegrityViolationException (line 78-81)
  5. handleJwtException (line 93-96)
  6. handleOtherException (line 109-112)

Spring의 @ExceptionHandler가 서블릿 객체 주입을 지원하므로 이 방식은 적절하며, 일관된 패턴으로 구현되어 유지보수가 용이합니다.

Also applies to: 42-45, 57-60, 78-81, 93-96, 109-112


32-32: 중복 로깅 방지 플래그가 잘 구현되었습니다!

모든 예외 핸들러에서 exceptionHandlerLogged 플래그를 설정하여 HttpLoggingFilter와의 협력이 잘 이루어지고 있습니다:

  1. 목적: 예외 발생 시 응답이 이미 로깅되었음을 필터에 알려 중복 로깅을 방지
  2. 일관성: 6개 모든 핸들러에 동일하게 적용
  3. 통합: PR의 다른 변경사항(HttpLoggingFilter)과 깔끔하게 연동

이 패턴은 분산된 로깅 로직 간의 조율을 명확하게 하여 좋은 설계입니다.

Also applies to: 46-46, 61-61, 82-82, 97-97, 113-113

Comment on lines +72 to +74
// Database Proxy
implementation 'net.ttddyy:datasource-proxy:1.11.0'
implementation 'net.ttddyy.observation:datasource-micrometer:1.2.0'
Copy link

@coderabbitai coderabbitai bot Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

datasource-proxy net.ttddyy latest version Maven Central 2025

💡 Result:

Latest Maven Central version: 1.11.0 (released July 14, 2025).

Maven dependency:

net.ttddyy
datasource-proxy
1.11.0

Sources: [Maven Repository (1.11.0)] [1], [javadoc.io (1.11.0)] [2], [Maven Central index/metadata] [3].

References

  1. https://mvnrepository.com/artifact/net.ttddyy/datasource-proxy/1.11.0
  2. https://javadoc.io/doc/net.ttddyy/datasource-proxy/1.11.0
  3. https://maven-eu.nuxeo.org/nexus/content/groups/public/net/ttddyy/datasource-proxy/

🌐 Web query:

datasource-micrometer net.ttddyy.observation latest version Maven Central 2025

💡 Result:

The latest Maven Central version is net.ttddyy.observation:datasource-micrometer: 2.0.1. [1][2]

Sources:

  • mvnrepository entry for datasource-micrometer 2.0.1. [1]
  • Official docs (installation snippet showing version 2.0.1). [2]

🌐 Web query:

datasource-proxy 1.11.0 security vulnerability CVE

💡 Result:

I found no public CVE that specifically affects the net.ttddyy datasource‑proxy 1.11.0 library itself. Maven listings for datasource‑proxy 1.11.0 show reported vulnerabilities coming from its dependencies (examples listed there include CVE-2025-68161, CVE-2024-38819, CVE-2023-6378), not a direct CVE for datasource‑proxy. [1]

Note: There is a separate, unrelated Grafana "datasource proxy" advisory (CVE-2025-3454) that affects Grafana’s server API — not the net.ttddyy datasource‑proxy Java library. Do not confuse the two. [2]

Sources:

  • Maven / mvnrepository entry for net.ttddyy:datasource-proxy 1.11.0 (vulnerability list). [1]
  • Grafana security advisory (CVE-2025-3454) — unrelated Grafana product issue. [2]

References:
[1] https://mvnrepository.com/artifact/net.ttddyy/datasource-proxy/1.11.0
[2] https://grafana.com/security/security-advisories/cve-2025-3454/


🌐 Web query:

datasource-micrometer 1.2.0 security vulnerability CVE

💡 Result:

I found no public CVE specifically affecting net.ttddyy.observation:datasource-micrometer version 1.2.0. The Maven listing for that artifact/version shows no CVE entry for the artifact itself. [1]

There are unrelated CVEs for other metric libraries (for example CVE-2020-1960 affecting Apache Flink's JMX reporter), but those do not apply to datasource-micrometer. [2][3]

If you want, I can:

  • scan OSS advisories (GitHub Security Advisories, NVD) for any recent updates, or
  • check your SBOM/dependency tree for transitive vulnerabilities that might affect your build. Which would you prefer?

Sources:
[1] mvnrepository — net.ttddyy.observation:datasource-micrometer 1.2.0.
[2] Micrometer security policy / advisories.
[3] NVD / GitLab advisory — CVE-2020-1960 (example of an unrelated metrics CVE).


데이터소스 프록시 의존성이 추가되었습니다. datasource-micrometer 버전 업그레이드를 권장합니다.

변경 내용:

  1. datasource-proxy:1.11.0 - 최신 버전이며, 쿼리 인터셉션을 위한 핵심 프록시 라이브러리입니다.
  2. datasource-micrometer:1.2.0 - 최신 버전은 2.0.1이므로 업그레이드를 고려하세요. Micrometer와의 통합을 위한 라이브러리입니다.

DataSourceProxyConfig 및 QueryMetricsListener와 연동되어 쿼리별 메트릭 수집을 지원합니다. 현재 datasource-micrometer는 1.2.0 사용 중이지만, 2.0.1이 최신 버전으로 이용 가능합니다. 특정 보안 취약점은 발견되지 않았으나, 최신 버전으로 업그레이드하면 개선 사항과 안정성을 함께 얻을 수 있습니다.

🤖 Prompt for AI Agents
In build.gradle around lines 72 to 74, the datasource-micrometer dependency is
pinned to 1.2.0 but should be upgraded to the current 2.0.1; update the
implementation coordinate from
'net.ttddyy.observation:datasource-micrometer:1.2.0' to
'net.ttddyy.observation:datasource-micrometer:2.0.1' and then run a quick build
and tests to verify compatibility (adjust any import or API usages in
DataSourceProxyConfig/QueryMetricsListener if the newer API changed).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

datasource-micrometer 의존성이 내부적으로 datasource-proxy 를 포함하기에, datasource-proxy 의존성은 제거해도 될 듯 합니다.

버전은 1.2.x 그대로 사용하면 될 듯 합니다. 메이저 버전이 1인 경우 마이너 버전 2가 최신이고, 저희 Spring Boot 버전과 호환됩니다. 메이저 버전 2는 Spring Boot 4.x에서 지원하네요. [참고1], [참고2]

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@whqtker 정확한 지적 감사합니다! 제 분석이 잘못되었네요.

말씀하신 내용 정리:

  1. 중복 의존성 제거 필요

    • datasource-micrometer가 내부적으로 datasource-proxy를 포함하고 있어, Line 72의 implementation 'net.ttddyy:datasource-proxy:1.11.0' 라인은 제거해도 됩니다.
  2. 버전 1.2.0이 적절함

    • 현재 프로젝트가 Spring Boot 3.1.5를 사용 중이므로, datasource-micrometer:1.2.0이 올바른 버전입니다.
    • 2.x 버전은 Spring Boot 4.x용이므로 업그레이드하면 안 됩니다.

참고 자료 제공해주셔서 이해가 명확해졌습니다. 중복 의존성을 제거하면 의존성 관리가 더 깔끔해질 것 같습니다!


✏️ Learnings added
Learnt from: whqtker
Repo: solid-connection/solid-connect-server PR: 602
File: build.gradle:72-74
Timestamp: 2026-01-02T08:56:21.107Z
Learning: In the solid-connect-server project using Spring Boot 3.1.5, datasource-micrometer 1.2.x is the correct version. Version 2.x is for Spring Boot 4.x and should not be used with Spring Boot 3.x.

Learnt from: whqtker
Repo: solid-connection/solid-connect-server PR: 602
File: build.gradle:72-74
Timestamp: 2026-01-02T08:56:21.107Z
Learning: The datasource-micrometer dependency (net.ttddyy.observation:datasource-micrometer) internally includes datasource-proxy as a transitive dependency, so explicit datasource-proxy dependency is redundant and should be removed to avoid duplication.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

- principal 조회 예외를 막기 위해 siteUserDetailsService given 추가
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1e4cfe2 and ac27d4f.

📒 Files selected for processing (1)
  • src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java (2)
src/main/java/com/example/solidconnection/security/authentication/TokenAuthentication.java (1)
  • TokenAuthentication (7-42)
src/main/java/com/example/solidconnection/security/userdetails/SiteUserDetails.java (1)
  • SiteUserDetails (9-56)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (3)
src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java (3)

4-5: 테스트에 필요한 import 추가가 적절합니다.

새로운 테스트 설정과 mocking을 위해 필요한 import들이 올바르게 추가되었습니다.

Also applies to: 11-11, 13-14


41-42: 테스트 픽스처 사용이 좋습니다.

SiteUserFixture를 활용하여 테스트 데이터를 생성하는 것은 재사용성과 일관성 측면에서 좋은 접근입니다.


56-60: Mock 설정이 적절합니다.

setUp에서 SiteUserDetailsService의 mock을 구성하여 모든 테스트에서 일관된 사용자 정보를 제공하도록 한 것이 좋습니다. anyString()을 사용한 것은 이 테스트 범위에서는 적절합니다.

* test: 중복되는 테스트 제거

* refactor: 사용하지 않는 필드 제거
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java (1)

3-3: 사용되지 않는 ObjectMapper import를 제거해주세요.

ObjectMapper 클래스를 import하고 있지만, 코드 내에서 사용되지 않습니다.

🔎 제안하는 수정
-import com.fasterxml.jackson.databind.ObjectMapper;
🧹 Nitpick comments (1)
src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java (1)

77-77: 이전 리뷰 피드백이 잘 반영되었습니다.

중복 테스트를 제거하고 기존 테스트를 확장하여 다음 항목들을 검증하도록 개선했습니다:

  1. SecurityContext에 인증 정보가 저장되는지 확인
  2. Request 속성에 userId가 설정되는지 확인

테스트 메서드명도 검증 내용을 정확히 반영하도록 업데이트되었습니다.


선택 사항: 토큰 생성 메서드 파라미터화 고려

Line 96의 createTokenWithExpiration 메서드에서 subject를 하드코딩("1")하고 있는데, 이를 파라미터로 받도록 수정하면 expectedUserId와의 관계가 더 명확해질 수 있습니다.

🔎 선택적 리팩토링 제안
     @Test
     void 토큰이_있으면_컨텍스트에_저장하고_userId를_request에_설정한다() throws Exception {
         // given
         Long expectedUserId = 1L;
         Date validExpiration = new Date(System.currentTimeMillis() + 1000);
-        String token = createTokenWithExpiration(validExpiration);
+        String token = createTokenWithExpiration(String.valueOf(expectedUserId), 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) {
+    private String createTokenWithExpiration(String subject, Date expiration) {
         return Jwts.builder()
-                .setSubject("1")
+                .setSubject(subject)
                 .setIssuedAt(new Date())
                 .setExpiration(expiration)
                 .signWith(SignatureAlgorithm.HS256, jwtProperties.secret())
                 .compact();
     }

Also applies to: 79-79, 90-90

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ac27d4f and 109a4e4.

📒 Files selected for processing (2)
  • src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java
  • src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java
🧰 Additional context used
🧬 Code graph analysis (2)
src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java (2)
src/main/java/com/example/solidconnection/security/authentication/TokenAuthentication.java (1)
  • TokenAuthentication (7-42)
src/main/java/com/example/solidconnection/security/userdetails/SiteUserDetails.java (1)
  • SiteUserDetails (9-56)
src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java (1)
src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java (1)
  • Slf4j (23-123)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
src/test/java/com/example/solidconnection/security/filter/TokenAuthenticationFilterTest.java (3)

4-5: 추가된 import 문들이 적절합니다.

Mockito의 anyString(), given() 메서드와 함께 SiteUserDetails, SiteUser, SiteUserFixture를 추가하여 향상된 테스트 설정을 지원합니다. 모든 import가 실제로 사용되고 있어 불필요한 의존성이 없습니다.

Also applies to: 11-11, 13-14


41-42: Fixture 활용이 좋습니다.

SiteUserFixture를 Autowire하여 테스트 데이터 생성을 표준화하고 일관성을 확보했습니다. 테스트 유지보수성이 향상됩니다.


56-60: Mock 설정이 정확합니다.

SiteUserDetailsService를 적절히 모킹하여 토큰 인증 흐름을 테스트 가능하게 만들었습니다. 생성된 사용자(ID=1)가 토큰의 subject("1") 및 예상 userId(1L)와 일관되게 정렬되어 있습니다.

src/main/java/com/example/solidconnection/common/filter/HttpLoggingFilter.java (2)

32-67: 필터 로직이 잘 구현되어 있습니다.

다음과 같은 흐름이 명확하고 안전하게 처리되고 있습니다:

  1. traceId 생성 및 MDC 설정 - 모든 요청에 추적 ID 부여
  2. 제외 경로 처리 - /actuator/** 같은 경로는 로깅 제외하되 traceId는 유지 (하위 레이어 로그에 활용)
  3. 조건부 응답 로깅 - 예외 핸들러에서 이미 로깅했으면 중복 로깅 방지
  4. MDC 정리 - finally 블록에서 확실한 정리로 메모리 누수 방지

특히 exceptionHandlerLogged 속성을 활용한 중복 로깅 방지 로직이 CustomExceptionHandler와 잘 협력하고 있습니다.


69-115: 헬퍼 메서드들이 안전하게 구현되어 있습니다.

각 메서드가 명확한 단일 책임을 가지고 있으며, 특히 다음 부분들이 잘 처리되었습니다:

  1. decodeQuery 메서드 (lines 100-109) - URL 디코딩 실패 시 원본 쿼리 문자열 반환하는 방어적 프로그래밍
  2. generateTraceId 메서드 (lines 79-81) - UUID 기반 16자 추적 ID 생성
  3. buildDecodedRequestUri 메서드 (lines 111-115) - 쿼리 문자열 유무를 안전하게 처리

Copy link
Member

@whqtker whqtker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정말 고생 많으셨습니다 ㅠㅠ 궁금한 점 등 리뷰 남겼습니다 ~!

try {
filterChain.doFilter(request, response);

Boolean alreadyExceptionLogging = (Boolean) request.getAttribute("exceptionHandlerLogged");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CustomExceptionHandler에서의 로그와 HttpLoggingFilter에서의 로그 형태가 서로 다른 목적을 가지고 있다고 생각해서 개인적으로 둘 다 있으면 좋을 거 같긴 합니다 ..! 음 근게 이건 다른 분들의 의견도 궁금하긴 하네요

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가로 exceptionHandlerLogged를 상수로 분리하면 좋을 거 같은데 ... CustomExceptionHandler에도 사용되어서 조금 까다롭네요 ㅠㅠ

) {
request.setAttribute("exceptionHandlerLogged", true);
Long userId = (Long) request.getAttribute("userId");
log.error("커스텀 예외 발생 userId : {} msg: {}", userId, ex.getMessage());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외 로그에 userId를 추가로 받도록 하신 이유가 있나요 ?


if(bestMatchingPattern instanceof String s) {
request.setAttribute(ROUTE_PATTERN_ATTRIBUTE, s);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 확인한 바로는 routePattern 속성이 저장은 되나 사용되지 않는 거 같은데, 그렇다면 제거해도 무방할 듯 합니다 !

String method = request.getMethod();
int status = response.getStatus();

if (responseTime > RESPONSE_TIME_THRESHOLD) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개인적으론 if-else 구조가 로직을 이해하는 데 더 좋을 거 같습니다 !

.addPathPatterns("/**");

registry.addInterceptor(requestContextInterceptor)
.addPathPatterns("/**");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

필터에서 /actuator 경로에 대해 처리하지 않도록 설정되었고, 필터가 최우선적으로 실행되긴 하지만 인터셉터 등록 시에도 명시적으로 /actuator 경로를 무시하도록 하는 것이 좋을 거 같습니다 !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: API 성능 로깅 인터셉터 및 요청 추적 필터, 쿼리 별 메트릭 전송 추가

2 participants