problem
기존에 로그를 aop를 사용하여 관리 했다.
aop 는 리플렉션을 사용해야 하기 때문에 성능 저하를 일으키는데
로그 같이 데이터의 사용이 많은 곳에서는 더 큰 재해를 불러 올 것 같다는 생각에
인터셉터로 구현해 보았다.
solution
1. HandlerInterceptor
@Component
class LoggingInterceptor(
private val objectMapper: ObjectMapper
) : HandlerInterceptor {
companion object {
private val logger: Logger = LoggerFactory.getLogger(LoggingInterceptor::class.java)
}
}
HandlerInterceptor 를 구현한 LoggingInterceptor 클래스를 만들어준다.
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
1.preHandle
컨트롤러 메서드 전
특정 조건이 충족되지 않으면 요청을 중단하고 응답을 반환
• true: 컨트롤러 메서드를 계속 실행합니다.
• false: 컨트롤러 호출을 중단하고 바로 응답을 반환합니다.
2.postHandle
컨트롤러 메서드 후
• 뷰에 전달될 ModelAndView 객체 조작
• 응답 데이터를 가공하거나 수정
3.afterCompletion
뷰가 렌더링된 후 실행
(MVC 패턴에서 말하는 View 이다 - HTML, JSP, Thymeleaf 같은 템플릿 엔진을 말한다)
현재는 react / view 와 같은 프론트엔드 뷰를 사용하면 MVC 가 완전히 종료되고 뷰가 렌더링 된다.
- 예외가 발생했을 때 에러 처리나 후속 작업 수행
1. 전체 코드
//LogInfoClass
data class LogInfo(
var url: String = "",
var apiName: String = "",
var httpMethod: String = "",
var header: Map<String, Any> = mutableMapOf(),
var parameters: Map<String, Any> = mutableMapOf(),
var body: Map<String, String> = mutableMapOf(),
var ipAddress: String = ""
) {
var exception: String? = ""
}
//LoggingInterceptor Class
@Component
class LoggingInterceptor(
private val objectMapper: ObjectMapper
) : HandlerInterceptor {
companion object {
private val logger: Logger = LoggerFactory.getLogger(LoggingInterceptor::class.java)
}
override fun preHandle(
request: HttpServletRequest,
response: HttpServletResponse,
handler: Any
): Boolean {
if (request.method == "GET") return true
if (handler !is HandlerMethod) {
logger.warn("등록되지 않은 Method 요청: ${request.requestURI}")
return true
}
val logInfo = createLogInfo(
request = request,
handler= handler)
request.setAttribute("logInfo", logInfo)
logger.info("요청 받은 url: ${logInfo.url}")
return true
}
override fun afterCompletion(
request: HttpServletRequest,
response: HttpServletResponse,
handler: Any,
ex: Exception?
) {
val logInfo = request.getAttribute("logInfo") as? LogInfo
if (logInfo != null) {
if (logInfo.exception != null) {
val errorMessage = logToStringConvert(logInfo)
logger.warn(errorMessage)
} else {
val logMessage = objectMapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(mapOf("logInfo" to logInfo))
logger.info(logMessage)
}
}
}
private fun logToStringConvert(logInfo: LogInfo): String? {
val errorMessage = objectMapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(mapOf("logInfo" to logInfo))
return errorMessage
}
private fun createLogInfo(request: HttpServletRequest, handler: HandlerMethod): LogInfo {
val headers = extractHeaders(request)
val parameters = request.parameterMap.mapValues { it.value.joinToString(",") }
val body = extractBody(request)
val ipAddress = extractIpAddress(request)
return LogInfo(
url = request.requestURI,
apiName = handler.method.name,
httpMethod = request.method,
header = headers,
parameters = parameters,
body = body,
ipAddress = ipAddress
)
}
private fun extractHeaders(request: HttpServletRequest): Map<String, Any> {
return request.headerNames.toList().associateWith { headerName ->
val headerValues = request.getHeaders(headerName).toList()
when {
headerName.equals("sec-ch-ua", ignoreCase = true) -> {
parseSecChUa(headerValues.firstOrNull() ?: "")
}
headerName.equals("sec-ch-ua-platform", ignoreCase = true) -> {
parseSecChUaPlatform(headerValues.firstOrNull() ?: "")
}
else -> {
if (headerValues.size == 1) headerValues[0] else headerValues
}
}
}
}
private fun parseSecChUaPlatform(platformValue: String): String {
return platformValue.replace("\"", "").trim()
}
private fun parseSecChUa(headerValue: String): List<Map<String, String>> {
return headerValue.split(", ")
.map { entry ->
val parts = entry.split(";v=")
mapOf("brand" to parts[0].replace("\"", ""), "version" to parts[1].replace("\"", ""))
}
}
private fun extractBody(request: HttpServletRequest): Map<String, String> {
return try {
objectMapper.readValue(
request.inputStream, object : TypeReference<Map<String, String>>() {}
)
} catch (e: Exception) {
emptyMap()
}
}
private fun extractIpAddress(request: HttpServletRequest): String {
return request.getHeader("X-Forwarded-For")
?: request.getHeader("Proxy-Client-IP")
?: request.getHeader("WL-Proxy-Client-IP")
?: request.getHeader("HTTP_CLIENT_IP")
?: request.getHeader("HTTP_X_FORWARDED_FOR")
?: request.remoteAddr
}
}
1. PreHandle
override fun preHandle(
request: HttpServletRequest,
response: HttpServletResponse,
handler: Any
): Boolean {
if (handler !is HandlerMethod) {
logger.warn("Handler Method 가 아닌 url : ${request.requestURI}")
return true
}
logger.info("요청 받은 url: ${request.requestURI}")
return true
}
HandlerMethod 가 아닌 요청이 들어오면 로그를 남겨준다.
( HandlerMethod 가 아닌 요청 /static/, /public/, /resources/와 같은 경로에서 CSS, JS, 이미지 파일 등을 요청한 경우 )
요청이 들어오면 컨트롤러에 전달 해준다.
2. AfterCompletion
override fun afterCompletion(
request: HttpServletRequest,
response: HttpServletResponse,
handler: Any,
ex: Exception?
) {
if (request.method != "GET") {
val logInfo = createLogInfo(
request = request,
handler= handler as HandlerMethod
)
if (logInfo.exception != null) {
val errorMessage = logToStringConvert(logInfo)
logger.warn(errorMessage)
} else {
val logMessage = objectMapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(mapOf("logInfo" to logInfo))
logger.info(logMessage)
}
}
}
컨트롤러에서 요청을 처리 후
처리가 완료되면 logInfo 객체를 만들어 준다.
만약 logInfo 객체에 exception 이 있다면 예외인 warn 레벨의 로그를 남긴다.
3. 헤더,바디 이스케이프
요청에는 많은 정보들이 담겨 온다.
보통 이중 컬렉션 형태로 되어 있어서 파싱을 따로 해주어야 한다.
"sec-ch-ua": "\"Chromium\";v=\"130\", \"Google Chrome\";v=\"130\", \"Not?A_Brand\";v=\"99\""
{
"sec-ch-ua": [
{ "brand": "Chromium", "version": "130" },
{ "brand": "Google Chrome", "version": "130" },
{ "brand": "Not?A_Brand", "version": "99" }
]
}
private fun extractBody(request: HttpServletRequest): Map<String, String> {
return try {
objectMapper.readValue(
request.inputStream, object : TypeReference<Map<String, String>>() {}
)
} catch (e: Exception) {
emptyMap()
}
}
private fun extractHeaders(request: HttpServletRequest): Map<String, Any> {
return request.headerNames.toList().associateWith { headerName ->
val headerValues = request.getHeaders(headerName).toList()
when {
headerName.equals("sec-ch-ua", ignoreCase = true) -> {
parseSecChUa(headerValues.firstOrNull() ?: "")
}
headerName.equals("sec-ch-ua-platform", ignoreCase = true) -> {
parseSecChUaPlatform(headerValues.firstOrNull() ?: "")
}
else -> {
if (headerValues.size == 1) headerValues[0] else headerValues
}
}
}
4.인터셉터 등록
@Configuration
class WebConfig(
private val loggingInterceptor: LoggingInterceptor) : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(loggingInterceptor)
}
}
마지막으로 인터셉터를 등록 해주면 된다.