refactor: 기존 log 관련 클래스 모듈 분리 및 일부 클래스는 재사용성 고려 리팩터링

This commit is contained in:
이상진 2025-09-27 22:31:56 +09:00
parent 51a0dab2b4
commit 33406fbc93
4 changed files with 91 additions and 84 deletions

View File

@ -0,0 +1,55 @@
package com.sangdol.common.log
import ch.qos.logback.classic.spi.ILoggingEvent
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.sangdol.common.log.message.AbstractLogMaskingConverter
import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.equals.shouldBeEqual
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.mockk.every
import io.mockk.mockk
class TestLogMaskingConverter : AbstractLogMaskingConverter(
sensitiveKeys = setOf("account", "address"),
objectMapper = jacksonObjectMapper()
)
class AbstractLogMaskingConverterTest : FunSpec({
val converter = TestLogMaskingConverter()
val event: ILoggingEvent = mockk()
val account = "sangdol@example.com"
val address = "서울특별시 강북구 수유1동 123-456"
context("sensitiveKeys=${converter.sensitiveKeys}에 있는 항목은 가린다.") {
context("평문 로그를 처리할 때, 여러 key / value가 있는 경우 서로 간의 구분자는 trim 처리한다.") {
listOf(":", "=", " : ", " = ").forEach { keyValueDelimiter ->
listOf(",", ", ").forEach { valueDelimiter ->
test("key1${keyValueDelimiter}value1${valueDelimiter}key2${keyValueDelimiter}value2 형식을 처리한다.") {
every {
event.formattedMessage
} returns "account$keyValueDelimiter$account${valueDelimiter}address$keyValueDelimiter$address"
assertSoftly(converter.convert(event)) {
this shouldBe "account${keyValueDelimiter}${account.first()}${converter.mask}${account.last()}${valueDelimiter}address${keyValueDelimiter}${address.first()}${converter.mask}${address.last()}"
}
}
}
}
}
context("JSON 로그") {
test("정상 처리") {
val json = "{\"request_body\":{\"account\":\"%s\",\"address\":\"%s\"}}"
every {
event.formattedMessage
} returns json.format(account, address)
converter.convert(event) shouldBeEqual json.format("${account.first()}${converter.mask}${account.last()}", "${address.first()}${converter.mask}${address.last()}")
}
}
}
})

View File

@ -1,8 +1,9 @@
package com.sangdol.roomescape.common.log package com.sangdol.common.log
import com.sangdol.common.config.JacksonConfig import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.sangdol.roomescape.auth.exception.AuthErrorCode import com.sangdol.common.log.config.LogType
import com.sangdol.roomescape.auth.exception.AuthException import com.sangdol.common.log.message.ApiLogMessageConverter
import com.sangdol.common.log.message.ConvertResponseMessageRequest
import io.kotest.core.spec.style.StringSpec import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.mockk.every import io.mockk.every
@ -11,7 +12,7 @@ import jakarta.servlet.http.HttpServletRequest
import org.slf4j.MDC import org.slf4j.MDC
class ApiLogMessageConverterTest : StringSpec({ class ApiLogMessageConverterTest : StringSpec({
val converter = ApiLogMessageConverter(JacksonConfig().objectMapper()) val converter = ApiLogMessageConverter(jacksonObjectMapper())
val request: HttpServletRequest = mockk() val request: HttpServletRequest = mockk()
beforeTest { beforeTest {
@ -58,11 +59,11 @@ class ApiLogMessageConverterTest : StringSpec({
type = LogType.CONTROLLER_SUCCESS, type = LogType.CONTROLLER_SUCCESS,
endpoint = endpoint, endpoint = endpoint,
httpStatus = 200, httpStatus = 200,
exception = AuthException(AuthErrorCode.MEMBER_NOT_FOUND, "테스트 메시지!") exception = RuntimeException("테스트 메시지!")
) )
converter.convertToResponseMessage(request) shouldBe """ converter.convertToResponseMessage(request) shouldBe """
{"type":"CONTROLLER_SUCCESS","endpoint":"$endpoint","status_code":200,"principal_id":1,"exception":{"class":"AuthException","message":"테스트 메시지!"}} {"type":"CONTROLLER_SUCCESS","endpoint":"$endpoint","status_code":200,"principal_id":1,"exception":{"class":"RuntimeException","message":"테스트 메시지!"}}
""".trimIndent() """.trimIndent()
} }
}) })

View File

@ -0,0 +1,28 @@
package com.sangdol.common.log
import com.sangdol.common.log.sql.SlowQueryPredicate
import com.sangdol.common.log.sql.SqlLogFormatter
import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
class MDCAwareSlowQueryListenerWithoutParamsTest : StringSpec({
"SQL 메시지에서 Params 항목은 가린다." {
val message = """Query:["select * from members m where m.email=?"], Params:[(a@a.a)]"""
val expected = """Query:["select * from members m where m.email=?"]"""
val result = SqlLogFormatter().maskParams(message)
result shouldBe expected
}
"입력된 thresholdMs 보다 소요시간이 긴 쿼리를 기록한다." {
val slowQueryThreshold = 10L
val slowQueryPredicate = SlowQueryPredicate(thresholdMs = slowQueryThreshold)
assertSoftly(slowQueryPredicate) {
this.test(slowQueryThreshold) shouldBe true
this.test(slowQueryThreshold + 1) shouldBe true
this.test(slowQueryThreshold - 1) shouldBe false
}
}
})

View File

@ -1,77 +0,0 @@
package com.sangdol.roomescape.common.log
import ch.qos.logback.classic.spi.ILoggingEvent
import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.equals.shouldBeEqual
import io.kotest.matchers.string.shouldContain
import io.mockk.every
import io.mockk.mockk
class RoomescapeLogMaskingConverterTest : FunSpec({
val converter = RoomescapeLogMaskingConverter()
val event: ILoggingEvent = mockk()
context("평문 로그에서는 key=value 형식을 처리한다.") {
test("2글자 초과이면 맨 앞, 맨 뒤를 제외한 나머지를 가린다.") {
val email = "a@a.a"
val password = "password12"
val accessToken = "accessToken12"
every {
event.formattedMessage
} returns "email=${email}, password=${password}, accessToken = $accessToken"
assertSoftly(converter.convert(event)) {
this shouldContain "email=${email}"
this shouldContain "password=${password.first()}****${password.last()}"
this shouldContain "accessToken = ${accessToken.first()}****${accessToken.last()}"
}
}
test("2글자 이하이면 전부 가린다.") {
val email = "a@a.a"
val password = "pa"
val accessToken = "a"
every {
event.formattedMessage
} returns "email=${email}, password=${password}, accessToken = ${accessToken}"
assertSoftly(converter.convert(event)) {
this shouldContain "email=${email}"
this shouldContain "password=****"
this shouldContain "accessToken = ****"
}
}
}
context("JSON 형식 로그를 처리한다.") {
val json = "{\"request_body\":{\"email\":\"a@a.a\",\"password\":\"password12\"}}"
test("2글자 초과이면 맨 앞, 맨 뒤를 제외한 나머지를 가린다.") {
val password = "password12"
val json = "{\"request_body\":{\"email\":\"a@a.a\",\"password\":\"${password}\"}}"
every {
event.formattedMessage
} returns json
converter.convert(event) shouldBeEqual "{\"request_body\":{\"email\":\"a@a.a\",\"password\":\"${password.first()}****${password.last()}\"}}"
}
test("2글자 이하이면 전부 가린다.") {
val password = "pa"
val json = "{\"request_body\":{\"email\":\"a@a.a\",\"password\":\"${password}\"}}"
every {
event.formattedMessage
} returns json
converter.convert(event) shouldBeEqual "{\"request_body\":{\"email\":\"a@a.a\",\"password\":\"****\"}}"
}
}
})