Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3ceade5
[BOOK-93] feat: infra - AOP 설정을 위한 AopConfig 클래스 추가
move-hoon Aug 14, 2025
291929c
[BOOK-93] feat: apis, infra - InfraBaseConfigGroup에 AOP 설정 클래스 추가 및 a…
move-hoon Aug 14, 2025
f94c79d
[BOOK-93] feat: infra, global-utils - 서비스 및 컨트롤러 레이어에 대한 로깅을 위한 Aspec…
move-hoon Aug 14, 2025
a82d5fc
[BOOK-93] chore: domain, infra - Spring Data Commons 및 Spring Web 추가 …
move-hoon Aug 14, 2025
ab71716
[BOOK-93] chore: logback 의존성 제외 후 Log4j2 추가 및 서비스 레이어 AOP 로깅 시 파라미터 이…
move-hoon Aug 14, 2025
72e95a8
[BOOK-93] chore: Dependencies.kt에 Spring Data Commons, Spring Web, Lo…
move-hoon Aug 14, 2025
b5c06b4
[BOOK-93] delete: domain - 사용하지 않는 CoreDomainConfig.kt 파일 삭제
move-hoon Aug 14, 2025
1dcb448
[BOOK-93] feat: gateway - MDC 기반 요청 로깅 필터 구현 및 사용자/클라이언트 정보 기록
move-hoon Aug 14, 2025
79002a4
[BOOK-93] feat: gateway - MdcLoggingFilter를 BearerTokenAuthentication…
move-hoon Aug 14, 2025
26fa8f3
[BOOK-93] feat: infra - Log4j2 설정 파일 추가로 로깅 구성 및 패턴 정의
move-hoon Aug 14, 2025
f0db1a0
[BOOK-93] feat: infra - 비동기 MDC 전파 작업을 위한 MdcTaskDecorator 클래스 추가 및 A…
move-hoon Aug 14, 2025
849c00a
Merge branch 'develop' into BOOK-93-feature/#27
move-hoon Aug 14, 2025
0275ea9
[BOOK-93] chore: infra - 사용하지 않는 import문 제거
move-hoon Aug 14, 2025
8a76d12
[BOOK-93] feat: global-utils - MethodArgumentTypeMismatchException 전역…
move-hoon Aug 16, 2025
726e8f2
[BOOK-93] refactor: infra - mdc에서 이미 로깅하기 때문에 시간 로깅 제외
move-hoon Aug 16, 2025
1f5d04b
[BOOK-93] chore: kapt 관련 의존성 정리 및 코드레빗 리뷰 반영
move-hoon Aug 16, 2025
24bf8bb
[BOOK-93] chore: infra - datetime(6)로 로그 패턴 수정
move-hoon Aug 16, 2025
7f080ea
[BOOK-93] refactor: gateway - 신뢰할 수 있는 프록시 서버 뒤에서만 XFF를 신뢰할 수 있도록 변경
move-hoon Aug 16, 2025
ee50243
[BOOK-93] chore: global-utils - 필요하지 않는 괄호 제외
move-hoon Aug 16, 2025
3daeacb
[BOOK-93] chore: buildSrc - KAPT 플러그인 추가
move-hoon Aug 16, 2025
17d477f
[BOOK-93] fix: infra - 함수/메서드 오버라이드 관련 경고 해결
move-hoon Aug 16, 2025
d674403
[BOOK-93] refactor: infra - 마스킹 로직 리팩토링
move-hoon Aug 16, 2025
0befe12
[BOOK-93] refactor: infra - 실행 전 MDC를 명시적으로 clear하여 이전 작업의 MDC 완전히 제거
move-hoon Aug 16, 2025
7daba0f
[BOOK-93] chore: apis - 디버깅용 로그 주석처리
move-hoon Aug 16, 2025
219744d
[BOOK-93] refactor: infra - prod 환경에서만 마스킹처리하도록 리팩토링
move-hoon Aug 16, 2025
5b0dae7
[BOOK-93] refactor: infra - 배열의 타입까지 로그에 남길 수 있도록 리팩토링
move-hoon Aug 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apis/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies {
implementation(Dependencies.BouncyCastle.BC_PROV)
implementation(Dependencies.BouncyCastle.BC_PKIX)

annotationProcessor(Dependencies.Spring.CONFIGURATION_PROCESSOR)
kapt(Dependencies.Spring.CONFIGURATION_PROCESSOR)
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

AOP 스타터 의존성 존재 여부 확인 필요

본 PR의 핵심인 AOP(애스펙트) 로깅이 실제 런타임에서 동작하려면 실행 모듈(예: apis)에 spring-boot-starter-aop가 필요합니다. 현재 제공된 빌드 파일들에서는 확인되지 않습니다.

아래 스크립트로 리포지토리 전체에서 AOP 스타터 추가 여부를 점검해 주세요.

apis 모듈에 추가가 필요하다면 다음 예시처럼 선언해 주세요(프로젝트 상수로 관리 중이면 해당 상수를 사용하세요).

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-aop")
}

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "[Search] spring-boot-starter-aop 사용 여부"
rg -n -S 'spring-boot-starter-aop|STARTER_AOP|BOOT_STARTER_AOP' -g 'build*.gradle*' || echo "AOP starter 미검출"

Length of output: 233


apis 모듈에 AOP 스타터 의존성 추가 필요

스크립트 결과, 전체 build*.gradle* 파일에서 spring-boot-starter-aop 의존성이 미검출되었습니다. AOP(애스펙트) 기반 로깅이 런타임에서 동작하려면 apis/build.gradle.ktsdependencies 블록에 아래 의존성을 추가해주세요.

  • 파일: apis/build.gradle.kts
  • 위치: dependencies { … } 블록 내부
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-aop")
    // …기존 의존성
}
🤖 Prompt for AI Agents
In apis/build.gradle.kts around line 25, the project is missing the Spring AOP
starter dependency required for AOP-based runtime logging; open the dependencies
{ … } block in that file and add the Spring Boot AOP starter as an
implementation dependency
(implementation("org.springframework.boot:spring-boot-starter-aop")) alongside
the existing dependencies so AOP aspects are available at runtime.


testImplementation(Dependencies.Spring.BOOT_STARTER_TEST)
testImplementation(Dependencies.TestContainers.MYSQL)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class AladinBookQueryService(
private val MAX_ALADIN_RESULTS = 200 // Added constant

override fun searchBooks(request: BookSearchRequest): BookSearchResponse {
log.info { "Service - Converting BookSearchRequest to AladinBookSearchRequest and calling Aladin API for book search." }
// log.info { "Service - Converting BookSearchRequest to AladinBookSearchRequest and calling Aladin API for book search." }
val aladinSearchRequest = AladinBookSearchRequest.of(
request.validQuery(),
request.queryType,
Expand All @@ -44,7 +44,7 @@ class AladinBookQueryService(
throw BookException(BookErrorCode.ALADIN_API_SEARCH_FAILED, exception.message)
}

log.info { "Before filtering - Full Response: $response" }
// log.info { "Before filtering - Full Response: $response" }

val transformedItems = response.item.mapNotNull { item ->
val validIsbn13 = getValidAndFilteredIsbn13(item)
Expand Down Expand Up @@ -75,13 +75,13 @@ class AladinBookQueryService(
searchCategoryName = response.searchCategoryName,
item = transformedItems
)
log.info { "After filtering - Full Response: $filteredResponse" }
// log.info { "After filtering - Full Response: $filteredResponse" }

return BookSearchResponse.of(filteredResponse, isLastPage) // Passed isLastPage
}

override fun getBookDetail(request: BookDetailRequest): BookDetailResponse {
log.info("Service - Converting BookDetailRequest to AladinBookLookupRequest and calling Aladin API for book detail lookup.")
// log.info("Service - Converting BookDetailRequest to AladinBookLookupRequest and calling Aladin API for book detail lookup.")
val aladinLookupRequest = AladinBookLookupRequest.from(request.validIsbn13())
val response: AladinBookDetailResponse = aladinApi.lookupBook(aladinLookupRequest)
.onSuccess { response ->
Expand Down
1 change: 1 addition & 0 deletions apis/src/main/kotlin/org/yapp/apis/config/InfraConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.yapp.infra.InfraBaseConfigGroup
InfraBaseConfigGroup.REST_CLIENT,
InfraBaseConfigGroup.QUERY_DSL,
InfraBaseConfigGroup.PAGE,
InfraBaseConfigGroup.AOP
]
)
class InfraConfig
10 changes: 10 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,22 @@ subprojects {
apply(plugin = Plugins.Kotlin.JPA)
apply(plugin = Plugins.Kotlin.JVM)
apply(plugin = Plugins.JACOCO)
apply(plugin = Plugins.Kotlin.KAPT)
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

KAPT 플러그인 서브모듈 적용 OK — 루트 plugins 블록은 apply false 권장

현재 루트 plugins { kotlin("kapt") version ... }로 루트에도 플러그인이 적용되고, 서브모듈에 다시 apply하고 있습니다. 루트는 apply false로 선언하고 서브모듈에서만 apply(plugin = Plugins.Kotlin.KAPT) 하는 패턴이 더 깔끔합니다.

아래와 같이 변경을 고려해 주세요(루트 plugins 블록):

plugins {
    id(Plugins.SPRING_BOOT) version Versions.SPRING_BOOT
    id(Plugins.SPRING_DEPENDENCY_MANAGEMENT) version Versions.SPRING_DEPENDENCY_MANAGEMENT
    kotlin(Plugins.Kotlin.Short.KAPT) version Versions.KOTLIN apply false
    kotlin(Plugins.Kotlin.Short.JVM) version Versions.KOTLIN apply false
    kotlin(Plugins.Kotlin.Short.SPRING) version Versions.KOTLIN apply false
    kotlin(Plugins.Kotlin.Short.JPA) version Versions.KOTLIN apply false
    id(Plugins.DETEKT) version Versions.DETEKT
    id(Plugins.JACOCO)
    id(Plugins.SONAR_QUBE) version Versions.SONAR_QUBE
}
🤖 Prompt for AI Agents
build.gradle.kts around line 51: 현재 루트 plugins 블록에서 kotlin("kapt")가 전역으로 적용되어 있고
서브모듈에서도 apply(plugin = Plugins.Kotlin.KAPT)을 사용하고 있으니, 루트 plugins 블록에서 해당 KAPT(및
다른 kotlin 플러그인들)를 version 선언 후 apply false로 변경하고 서브모듈에서만 apply(plugin =
Plugins.Kotlin.KAPT) 하도록 변경하세요; 구체적으로 루트의 plugins { ... }에 kotlin
kapt/jvm/spring/jpa 플러그인을 version과 함께 apply false로 선언하고 루트에서의 직접 적용을 제거한 뒤 각
서브모듈 build.gradle.kts에서는 기존대로 apply(plugin = Plugins.Kotlin.KAPT) 를 유지해 빌드스크립트의
플러그인 적용 범위를 서브모듈로 한정하세요.


java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(Versions.JAVA_VERSION.toInt()))
}
}

configurations.all {
exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
}

dependencies {
implementation(Dependencies.Spring.STARTER_LOG4J2)
}

plugins.withId(Plugins.Kotlin.ALLOPEN) {
extensions.configure<org.jetbrains.kotlin.allopen.gradle.AllOpenExtension> {
annotation("jakarta.persistence.Entity")
Expand All @@ -71,6 +80,7 @@ subprojects {
"-Xconsistent-data-class-copy-visibility"
)
jvmTarget = Versions.JAVA_VERSION
javaParameters = true
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ object Dependencies {
const val BOOT_STARTER_OAUTH2_CLIENT = "org.springframework.boot:spring-boot-starter-oauth2-client"
const val KOTLIN_REFLECT = "org.jetbrains.kotlin:kotlin-reflect"
const val CONFIGURATION_PROCESSOR = "org.springframework.boot:spring-boot-configuration-processor"
const val SPRING_DATA_COMMONS = "org.springframework.data:spring-data-commons"
const val SPRING_WEB = "org.springframework:spring-web"
const val STARTER_LOG4J2 = "org.springframework.boot:spring-boot-starter-log4j2"
}

object Database {
Expand Down
1 change: 1 addition & 0 deletions buildSrc/src/main/kotlin/Plugins.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ object Plugins {
const val SPRING = "org.jetbrains.kotlin.plugin.spring"
const val JPA = "org.jetbrains.kotlin.plugin.jpa"
const val JVM = "org.jetbrains.kotlin.jvm"
const val KAPT = "org.jetbrains.kotlin.kapt"

object Short {
const val KAPT = "kapt"
Expand Down
4 changes: 2 additions & 2 deletions domain/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import org.springframework.boot.gradle.tasks.bundling.BootJar

dependencies {
implementation(project(Dependencies.Projects.GLOBAL_UTILS))
implementation(Dependencies.Spring.BOOT_STARTER_WEB)
implementation(Dependencies.Spring.BOOT_STARTER_DATA_JPA)
testImplementation(Dependencies.Spring.BOOT_STARTER_TEST)
implementation(Dependencies.Spring.SPRING_DATA_COMMONS) // Pageable, Page용
implementation(Dependencies.Spring.SPRING_WEB) // HttpsStatus용으로 추가한 것이기 때문에 추후 Global-utils 모듈에 httpStatus를 정의하고 이를 의존해서 사용하도록 바꾸면 해당 의존성 삭제 가능
}

tasks {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.yapp.gateway.filter

import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.MDC
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import java.util.*

@Component
class MdcLoggingFilter : OncePerRequestFilter() {
companion object {
private const val TRACE_ID_HEADER = "X-Request-ID"
private const val XFF_HEADER = "X-Forwarded-For"
private const val X_REAL_IP_HEADER = "X-Real-IP"
private const val TRACE_ID_KEY = "traceId"
private const val USER_ID_KEY = "userId"
private const val CLIENT_IP_KEY = "clientIp"
private const val REQUEST_INFO_KEY = "requestInfo"
private const val DEFAULT_GUEST_USER = "GUEST"
}

override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val traceId = resolveTraceId(request)
populateMdc(request, traceId)

try {
filterChain.doFilter(request, response)
} finally {
MDC.clear()
}
}

private fun resolveTraceId(request: HttpServletRequest): String {
val incomingTraceId = request.getHeader(TRACE_ID_HEADER)
return incomingTraceId?.takeIf { it.isNotBlank() }
?: UUID.randomUUID().toString().replace("-", "")
}

private fun populateMdc(request: HttpServletRequest, traceId: String) {
MDC.put(TRACE_ID_KEY, traceId)
MDC.put(CLIENT_IP_KEY, extractClientIp(request))
MDC.put(REQUEST_INFO_KEY, "${request.method} ${request.requestURI}")

val userId = resolveUserId()
MDC.put(USER_ID_KEY, userId ?: DEFAULT_GUEST_USER)
}
Comment on lines +52 to +54
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

PII 최소화: 원본 userId 대신 가명(userHash) 기록 권장

로그에 원본 userId를 직접 노출하면 PII 이슈가 발생할 수 있습니다. 원본을 유지해야 한다면 별도 키(userHash)에 해시를 함께 기록하고, log4j2 패턴은 userHash로 바꾸는 것이 안전합니다. log4j2-spring.xml에 대한 코멘트도 함께 확인 바랍니다.

-        val userId = resolveUserId()
-        MDC.put(USER_ID_KEY, userId ?: DEFAULT_GUEST_USER)
+        val userId = resolveUserId() ?: DEFAULT_GUEST_USER
+        MDC.put(USER_ID_KEY, userId)              // 필요 시 유지
+        MDC.put("userHash", hashSha256(userId))   // 로그 패턴은 userHash 사용 권장

추가 함수(파일 내 임의 위치, 예: 클래스 하단)에 정의해 주세요:

// import 필요: java.security.MessageDigest
private fun hashSha256(input: String): String {
    val md = java.security.MessageDigest.getInstance("SHA-256")
    val bytes = md.digest(input.toByteArray(Charsets.UTF_8))
    val hex = StringBuilder(bytes.size * 2)
    for (b in bytes) {
        val i = b.toInt() and 0xff
        if (i < 0x10) hex.append('0')
        hex.append(i.toString(16))
    }
    return hex.toString()
}


private fun extractClientIp(request: HttpServletRequest): String {
val xffHeader = request.getHeader(XFF_HEADER)
if (!xffHeader.isNullOrBlank()) {
return xffHeader.split(",").first().trim()
}

val xRealIp = request.getHeader(X_REAL_IP_HEADER)
if (!xRealIp.isNullOrBlank()) {
return xRealIp.trim()
}

return request.remoteAddr
}

private fun resolveUserId(): String? {
val authentication = SecurityContextHolder.getContext().authentication ?: return null

return when (val principal = authentication.principal) {
is Jwt -> principal.subject
else -> principal?.toString()
}
Comment on lines +73 to +76
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

principal.toString()는 과다/민감 정보 노출 위험 — authentication.name 사용 권장

toString()은 객체 상세를 포함할 수 있어 로그 과다/PII 위험이 큽니다. 일반 케이스는 authentication.name 이 안전하고 일관적입니다.

-        return when (val principal = authentication.principal) {
-            is Jwt -> principal.subject
-            else -> principal?.toString()
-        }
+        return when (val principal = authentication.principal) {
+            is Jwt -> principal.subject
+            else -> authentication.name
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return when (val principal = authentication.principal) {
is Jwt -> principal.subject
else -> principal?.toString()
}
return when (val principal = authentication.principal) {
is Jwt -> principal.subject
else -> authentication.name
}
🤖 Prompt for AI Agents
gateway/src/main/kotlin/org/yapp/gateway/filter/MdcLoggingFilter.kt around lines
73 to 76: the code returns principal?.toString() which can expose excessive or
sensitive details in logs; change the fallback to use authentication.name (a
safer, consistent identifier) instead of calling toString() on the principal,
i.e. return principal.subject for Jwt and otherwise return authentication.name
so logs do not leak PII.

}
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@ package org.yapp.gateway.security

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.convert.converter.Converter
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.core.convert.converter.Converter
import org.springframework.http.HttpMethod
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter
import org.springframework.security.web.SecurityFilterChain
import org.yapp.gateway.filter.MdcLoggingFilter

@Configuration
@EnableWebSecurity
class SecurityConfig(
private val jwtAuthenticationConverter: Converter<Jwt, out AbstractAuthenticationToken>,
private val customAuthenticationEntryPoint: CustomAuthenticationEntryPoint,
private val customAccessDeniedHandler: CustomAccessDeniedHandler
private val customAccessDeniedHandler: CustomAccessDeniedHandler,
private val mdcLoggingFilter: MdcLoggingFilter
) {
companion object {
private val WHITELIST_URLS = arrayOf(
Expand Down Expand Up @@ -53,6 +55,7 @@ class SecurityConfig(
it.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
it.anyRequest().authenticated()
}
.addFilterAfter(mdcLoggingFilter, BearerTokenAuthenticationFilter::class.java)

return http.build()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.yapp.globalutils.annotation

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class NoLogging
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.springframework.web.HttpRequestMethodNotSupportedException
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException

/**
* Global exception handler for the application.
Expand Down Expand Up @@ -159,6 +160,27 @@ class GlobalExceptionHandler {
return ResponseEntity(error, commonErrorCode.getHttpStatus())
}

/**
* 메서드 파라미터 타입 변환 실패 처리
*
* 주로 @RequestParam, @PathVariable 등에서 클라이언트가 잘못된 타입의 값을 전달했을 때 발생합니다.
* 예: 문자열을 int 타입으로 변환 시도
*/
@ExceptionHandler(MethodArgumentTypeMismatchException::class)
protected fun handleMethodArgumentTypeMismatch(ex: MethodArgumentTypeMismatchException): ResponseEntity<ErrorResponse> {
val commonErrorCode = CommonErrorCode.INVALID_REQUEST

log.warn { "Method argument type mismatch: ${ex.name}, value: ${ex.value}, requiredType: ${ex.requiredType}" }

val error = ErrorResponse.builder()
.status(commonErrorCode.getHttpStatus().value())
.message("Invalid value '${ex.value}' for parameter '${ex.name}'. Expected type: ${ex.requiredType?.simpleName}")
.code(commonErrorCode.getCode())
.build()

return ResponseEntity(error, commonErrorCode.getHttpStatus())
}
Comment on lines +163 to +182
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

민감정보/대용량 값의 로그·응답 반영 가능성 — 값 마스킹/절단 및 널-세이프 처리 권장

  • 현재 ex.value가 로그와 클라이언트 응답 메시지에 그대로 반영됩니다. PathVariable/RequestParam에 토큰, 이메일 등 PII가 포함될 경우 노출 위험이 있습니다. PR에서 제시한 “민감 정보 마스킹” 원칙과도 상충합니다.
  • ex.requiredType는 null 가능성이 있어 메시지에 null이 출력될 수 있습니다. 기본값을 두는 편이 안전합니다.
  • 로그에서는 requiredType 전체 객체가 출력되어 "class java.lang.Integer"와 같은 노이즈가 생길 수 있으므로 simpleName 사용을 권장합니다.

아래와 같이 값 마스킹/절단 및 널-세이프 처리를 적용해 주세요.

@@
-        log.warn { "Method argument type mismatch: ${ex.name}, value: ${ex.value}, requiredType: ${ex.requiredType}" }
+        // 민감 값 마스킹/절단 및 널-세이프 타입명 구성
+        val valueStr = ex.value?.toString()?.let { if (it.length > 100) it.take(100) + "..." else it } ?: "null"
+        val requiredTypeName = ex.requiredType?.simpleName ?: "Unknown"
+        log.warn { "Method argument type mismatch: name=${ex.name}, value=$valueStr, requiredType=$requiredTypeName" }
@@
-            .message("Invalid value '${ex.value}' for parameter '${ex.name}'. Expected type: ${ex.requiredType?.simpleName}")
+            .message("Invalid value '$valueStr' for parameter '${ex.name}'. Expected type: $requiredTypeName")

위 수정으로

  • 로그/응답에 과도한 원문 값 노출을 줄이고,
  • 타입명이 null이거나 장황하게 출력되는 문제를 방지합니다.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 메서드 파라미터 타입 변환 실패 처리
*
* 주로 @RequestParam, @PathVariable 등에서 클라이언트가 잘못된 타입의 값을 전달했을 때 발생합니다.
*: 문자열을 int 타입으로 변환 시도
*/
@ExceptionHandler(MethodArgumentTypeMismatchException::class)
protected fun handleMethodArgumentTypeMismatch(ex: MethodArgumentTypeMismatchException): ResponseEntity<ErrorResponse> {
val commonErrorCode = CommonErrorCode.INVALID_REQUEST
log.warn { "Method argument type mismatch: ${ex.name}, value: ${ex.value}, requiredType: ${ex.requiredType}" }
val error = ErrorResponse.builder()
.status(commonErrorCode.getHttpStatus().value())
.message("Invalid value '${ex.value}' for parameter '${ex.name}'. Expected type: ${ex.requiredType?.simpleName}")
.code(commonErrorCode.getCode())
.build()
return ResponseEntity(error, commonErrorCode.getHttpStatus())
}
/**
* 메서드 파라미터 타입 변환 실패 처리
*
* 주로 @RequestParam, @PathVariable 등에서 클라이언트가 잘못된 타입의 값을 전달했을 때 발생합니다.
*: 문자열을 int 타입으로 변환 시도
*/
@ExceptionHandler(MethodArgumentTypeMismatchException::class)
protected fun handleMethodArgumentTypeMismatch(ex: MethodArgumentTypeMismatchException): ResponseEntity<ErrorResponse> {
val commonErrorCode = CommonErrorCode.INVALID_REQUEST
// 민감 값 마스킹/절단 및 널-세이프 타입명 구성
val valueStr = ex.value
?.toString()
?.let { if (it.length > 100) it.take(100) + "..." else it }
?: "null"
val requiredTypeName = ex.requiredType?.simpleName ?: "Unknown"
log.warn { "Method argument type mismatch: name=${ex.name}, value=$valueStr, requiredType=$requiredTypeName" }
val error = ErrorResponse.builder()
.status(commonErrorCode.getHttpStatus().value())
.message("Invalid value '$valueStr' for parameter '${ex.name}'. Expected type: $requiredTypeName")
.code(commonErrorCode.getCode())
.build()
return ResponseEntity(error, commonErrorCode.getHttpStatus())
}
🤖 Prompt for AI Agents
In
global-utils/src/main/kotlin/org/yapp/globalutils/exception/GlobalExceptionHandler.kt
around lines 163 to 182, ex.value is currently logged and returned raw and
ex.requiredType can be null or verbose; change to create a safe, maskedValue by
null-checking ex.value, converting to string, masking common sensitive patterns
(emails/tokens) and truncating long values (e.g., to 100 chars with "…"), then
use that maskedValue in both the log and the ErrorResponse message; for the type
use ex.requiredType?.simpleName ?: "Unknown" to avoid nulls and noisy class
names; update the log to include maskedValue and simpleName-only type.


/**
* 그 외 모든 예외 처리
*
Expand Down
13 changes: 4 additions & 9 deletions infra/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
kotlin(Plugins.Kotlin.Short.KAPT) version Versions.KOTLIN
}

dependencies {
implementation(project(Dependencies.Projects.GLOBAL_UTILS))
implementation(project(Dependencies.Projects.DOMAIN))
implementation(Dependencies.Spring.BOOT_STARTER_WEB)
implementation(Dependencies.Spring.BOOT_STARTER_DATA_JPA)
implementation(Dependencies.Spring.BOOT_STARTER_DATA_REDIS)
implementation(Dependencies.Spring.KOTLIN_REFLECT)

implementation(Dependencies.RestClient.HTTP_CLIENT5)
implementation(Dependencies.RestClient.HTTP_CORE5)
testImplementation(Dependencies.Spring.BOOT_STARTER_TEST)

implementation(Dependencies.Spring.KOTLIN_REFLECT)

implementation(Dependencies.Database.MYSQL_CONNECTOR)


implementation(Dependencies.Flyway.MYSQL)

implementation(Dependencies.QueryDsl.JPA)
kapt(Dependencies.QueryDsl.APT)

kapt(Dependencies.QueryDsl.APT)

testImplementation(Dependencies.Spring.BOOT_STARTER_TEST)
testImplementation(Dependencies.TestContainers.MYSQL)
testImplementation(Dependencies.TestContainers.JUNIT_JUPITER)
testImplementation(Dependencies.TestContainers.REDIS)
Expand Down
4 changes: 3 additions & 1 deletion infra/src/main/kotlin/org/yapp/infra/InfraBaseConfigGroup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.yapp.infra

import org.yapp.infra.config.external.api.RestClientConfig
import org.yapp.infra.config.external.redis.RedisConfig
import org.yapp.infra.config.internal.aop.AopConfig
import org.yapp.infra.config.internal.async.AsyncConfig
import org.yapp.infra.config.internal.jpa.JpaConfig
import org.yapp.infra.config.internal.page.PageConfig
Expand All @@ -15,5 +16,6 @@ enum class InfraBaseConfigGroup(
PAGE(PageConfig::class.java),
REDIS(RedisConfig::class.java),
REST_CLIENT(RestClientConfig::class.java),
QUERY_DSL(QuerydslConfig::class.java)
QUERY_DSL(QuerydslConfig::class.java),
AOP(AopConfig::class.java)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.yapp.infra.aop.aspect

import jakarta.servlet.http.HttpServletRequest
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
import org.yapp.infra.aop.properties.LoggingAopProperties
import java.time.Duration
import java.time.Instant

@Aspect
@Component
class ControllerLoggingAspect(
private val properties: LoggingAopProperties
) {
private val log = LoggerFactory.getLogger(ControllerLoggingAspect::class.java)

@Around("org.yapp.infra.aop.pointcut.CommonPointcuts.controller() && !org.yapp.infra.aop.pointcut.CommonPointcuts.noLogging()")
fun logController(joinPoint: ProceedingJoinPoint): Any? {
if (!properties.controller.enabled) {
return joinPoint.proceed()
}

val startTime = Instant.now()
logRequestStart(joinPoint)

try {
val result = joinPoint.proceed()
logRequestSuccess(startTime)
return result
} catch (e: Throwable) {
throw e
}
}

private fun logRequestStart(joinPoint: ProceedingJoinPoint) {
val signature = joinPoint.signature
val className = signature.declaringType.simpleName
val methodName = signature.name

val request = getCurrentRequest()
val httpMethod = request?.method ?: "UNKNOWN"
val uri = request?.requestURI ?: "UNKNOWN"

log.info("[API-REQ] {} {} | Controller: {}.{}", httpMethod, uri, className, methodName)
}

private fun logRequestSuccess(startTime: Instant) {
val executionTimeMs = getExecutionTimeMs(startTime)
log.info("[API-RES] Logic Duration: {}ms", executionTimeMs)
}

private fun getCurrentRequest(): HttpServletRequest? =
(RequestContextHolder.getRequestAttributes() as? ServletRequestAttributes)?.request

private fun getExecutionTimeMs(startTime: Instant): Long =
Duration.between(startTime, Instant.now()).toMillis()
}
Loading