Log

Filter 로그 처리 후 Controller에 요청 전달하기

땍땍 2024. 8. 8. 18:54

이번 글에서 활용할 Filter 코드는 아래와 같다.

( 해당 코드에 대한 설명과 과정은 아래 글을 참고 )

https://taekt.tistory.com/12

@Slf4j
@Component
public class LogFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);

        filterChain.doFilter(requestWrapper,responseWrapper);

        byte[] requestBody = requestWrapper.getContentAsByteArray();
        log.info("request : " + new String(requestBody, StandardCharsets.UTF_8));

        byte[] responseBody = responseWrapper.getContentAsByteArray();
        log.info("response : " + new String(responseBody, StandardCharsets.UTF_8));

        responseWrapper.copyBodyToResponse();

    }
}

해당 방법은 요청/응답 값을 출력하는 것 자체는 가능하나, Controller에 요청을 우선 전달한 후 후 로그를 출력하기 때문에 스프링에 요청 값을 전달하기 전 이를 가공하거나 제한하는 등의 과정에서 제한되는 사항들이 많을 것이다.

때문에 이러한 문제를 해결하기 위해 Controller 전달 전 RequestBody값을 우선적으로 로그에 띄우려 한다.

이를 위해서는 ContentCachingRequestWrapper와 Filter 체인의 동작 방식을 알아두면 좋은데, 자세한 내용은 맨위의 링크를 통해 확인을 부탁하며, 간단하게 설명하자면

  1. HttpServletRequest의 InputStream은 한 번 밖에 읽지 못 한다.
  2. ContentCachingRequestWrapper에 값을 저장하기 위해서는 InputStream을 필수로 읽어야 한다.
  3. doFilter 를 활용해 Controller로 요청을 전달하는 과정에서 InputStream이 존재하여야 한다.

위와 같은 세 가지 조건을 모두 성립하기 위해서는 doFilter 를 사용하기 전, 요청 값을 복사해 저장해두고 사용하는 방법이 가장 적합하다는 생각이 들었다.

때문에 요청 값을 저장하고 가져다 쓸 수 있는 RequestWrapper 클래스를 생성했다.

public class RequestWrapper extends HttpServletRequestWrapper {
    private final byte[] cachedContent;

    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.cachedContent = StreamUtils.copyToByteArray(request.getInputStream());
    }

    public ServletInputStream getInputStream(){
        return new ServletInputStream() {
            private final InputStream cachedBodyInputStream = new ByteArrayInputStream(cachedContent);
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int read() throws IOException {
                return cachedBodyInputStream.read();
            }
        };
    }
}

  1. InputStream 값을 복사해둘 cachedContent 생성
  2. 클래스 선언과 동시에 HttpServletRequest값 저장
  3. getInputStream을 통하여 저장된 값 읽어 반환
@Slf4j
@Component
public class LogFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        RequestWrapper requestWrapper = new RequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);

        log.info("Request Body: " + getBody(requestWrapper.getInputStream()));

        filterChain.doFilter(requestWrapper, responseWrapper);

        log.info("Response Body : " + getBody(responseWrapper.getContentInputStream()));

        responseWrapper.copyBodyToResponse();
    }

    public String getBody(InputStream is) throws IOException {
        byte[] content = StreamUtils.copyToByteArray(is);
        if (content.length == 0) {
            return null;
        }
        return new String(content, StandardCharsets.UTF_8);
    }
}

이와같이 코드를 작성하면 Controller에 전달 전에 body 값을 로그로 출력할 수 있다.