Server/Spring

[Spring] 로그를 어떻게 남겨야 할까?

강서월 2024. 9. 9. 18:10

1단계 : HTTP Request / Response 로그를 어디에서 처리해야 할까?

HTTP Request / Response 로그는 공통 처리가 필요한 영역입니다. 이를 처리하기 위한 방법으로 주로 사용되는 세가지가 있습니다.

 

1. Servlet Filter

Servlet Filter 는 Dispatcher Servlet 의 전 / 후에 동작하며, 사용자의 요청이나 응답을 가장 먼저 마주합니다. 필터는 스프링의 고유 기능이 아니라 자바 서블릿에서 제공하는 기능입니다. Filter 는 동일한 Servlet Container (e.g Tomcat) 내에서 필요한 자원들을 활용하여 동작합니다.

 

2. Handler Interceptor

Interceptor 는 Dispatcher Servlet 이 실행된 후 호출됩니다. Spring Context 에서 관리되기 때문에 모든 빈(Bean)에 접근이 가능하다는 장점이 있습니다.

 

3. AOP

Interceptor 와 filter 는 주소로 대상을 구분해서 걸러내야 하는 반면, AOP 는 주소, 파라미터, 어노테이션 등 다양한 방법으로 대상을 지정할 수 있습니다. AOP advice 와 Handler Interceptor 의 가장 큰 차이점은 파라미터의 차이점입니다. Advice 의 경우 JoinPoint 나 ProceedingJointPoint 등을 활용해서 호출하지만, HandlerInterceptor 는 Filter 와 유사하게 HttpServletRequest, HttpServletResponse 를 파라미터로 사용합니다.

 

결론적으로 저는 Filter 를 선택하였습니다.

HTTP 요청은 Spring 의 외부 영역, 즉 웹 서버 레이어에서 시작됩니다. 만약 Aspect 실행 전에 오류가 발생하면 HTTP 요청 로그를 남길 수 없게 됩니다. AOP와 Interceptor 는 Spring 영역에서 동작하므로 HTTP 요청/응답 로그를 남기기 위해서는 Filter 가 적합하다고 판단하였습니다.

 

2단계 : 요청된 HTTP 정보에 어떻게 접근해야 할까?

InputStream 은 1번만 읽을 수 있습니다. 따라서 이미 캐싱 기능 구현된 ContentCachingRequestWrapper 를 사용하거나 HttpServletRequestWrapper 를 상속해서 직접 캐싱 기능을 구현하는 방법을 선택할 수 있습니다.

 

1. ContentCachingRequestWrapper

이 클래스는 getContentAsByteArray() 메서드를 제공하여, body 를 여러번 읽을 수 있게 해줍니다. 이 메서드는 스트림이 소비된 시점에 캐싱되기 때문에, 한 번 read 가 되어야만 캐싱이 이루어집니다.

 

문제 상황

Servlet Filter 에서 요청을 출력하려 했으나, request body 가 빈 값으로 나오는 문제가 발생하였습니다.

val cachingRequest = ContentCachingRequestWrapper(request)
val cachingResponse = ContentCachingResponseWrapper(response)

try {
	logRequest(cachingRequest)
	filterChain.doFilter(cachingRequest, cachingResponse)
} finally {
	logResponse(cachingResponse)
	cachingResponse.copyBodyToResponse()
}

원인은 InputStream 이 read 된 시점에서 캐싱이 이루어지는데, 이 코드에서는 InputStream 이 아직 읽히지 않았기 때문에 request body 가 비어 있었습니다. 이를 해결하기 위해 doFilter() 이후에 request 를 출력하려 했으나, 예외가 발생하면 로그를 볼 수 없는 문제가 발생하였습니다. 

 

이 문제를 해결하기 위해 HttpServletRequestWrapper 를 상속받아 RequestWrapper 를 커스텀하여 사용하였습니다. 

 

2. HttpServletRequestWrapper

class RequestWrapper(request: HttpServletRequest) : HttpServletRequestWrapper(request) {
    private val cachedInputStream: ByteArray

    init {
        val requestInputStream = request.inputStream
        cachedInputStream = StreamUtils.copyToByteArray(requestInputStream)
    }

    override fun getInputStream(): ServletInputStream {
        return object : ServletInputStream() {
            private val cachedBodyInputStream = ByteArrayInputStream(cachedInputStream)

            override fun isFinished(): Boolean {
                return try {
                    cachedBodyInputStream.available() == 0
                } catch (e: IOException) {
                    e.printStackTrace()
                    false
                }
            }

            override fun isReady(): Boolean {
                return true
            }

            override fun setReadListener(readListener: ReadListener) {
                throw UnsupportedOperationException()
            }

            override fun read(): Int {
                return cachedBodyInputStream.read()
            }
        }
    }

}
 val cachingRequest = RequestWrapper(request)
 val cachingResponse = ContentCachingResponseWrapper(response)

 try {
	logRequest(cachingRequest)
	filterChain.doFilter(cachingRequest, cachingResponse)
} finally {
	logResponse(cachingResponse)
	cachingResponse.copyBodyToResponse()
	MDC.clear()
}