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()
}
'Server > Spring' 카테고리의 다른 글
[Spring/JPA] EntityListener 로 효율적인 이벤트 처리를 구현해보도록 하겠습니다. (1) | 2024.09.06 |
---|---|
[Spring] 스프링 4.2 이후 version 에서 Application Event 도입을 해보았습니다. (0) | 2024.09.02 |
[Spring] failed to lazily initialize a collection of role (2) | 2024.08.14 |
[Spring/JPA] 엔티티 생명주기(Entity Lifecycle) 에 대해 알아보겠습니다. (0) | 2023.10.20 |
[Spring/JPA] 영속성 컨텍스트(Persistence Context)에 대해 알아보겠습니다. (0) | 2023.10.17 |