본문 바로가기
SpringBoot/구현 고민들

[Spring] Servlet Filter access log 관리

by rla124 2025. 4. 27.

문제 상황

이전에 Spring logback을 통해서 콘솔 로그를 파일로 관리해서 보다 쉽게 디버깅을 할 수 있도록 했다면 이제는 운영 관점에서 보다 쉽게 어떤 요청이 들어왔을 때 서버에서 어떤 응답이 내려줬는지 확인하여 문제점 분석을 용이하게 하고 싶었다.

실제 엑세스 로그를 관리하는 것의 중요성을 여러 경험을 통해 체감했기 때문에 이 부분은 내가 꼭 실제 구현을 해보며 짚고 넘어가고자 했다.

 

해결 방법 : Servlet Filter로 요청과 응답 가로채기

PR 링크를 통해 filter 내용 및 req/res를 파일에 남기는 일련의 과정을 확인할 수 있다. 아래는 filter 로직 절차이다.

아래 내가 공부해며 구현했던 사고 과정을 정리했다.

 

1. 필터란?

Servlet Filter는 요청/응답 전후 공통 처리(지금 상황에서는 로깅)를 할 수 있도록 도와주는 스블릿 스펙의 한 기능이다. security filter chain처럼 filter chain을 통해 여러 필터들이 순서대로 실행된다. 

이 filter를 implements하여 만든 AccessLogFilter는 HTTP 요청과 응답을 로깅하기 위해 만들었다. 

Servlet Filter는 DispatcherServlet 앞단에서 동작해서 요청 및 응답을 모두 먼저 가로채버리는 특징이 있다. 이로 인한 문제점은 아래와 같다.

 

 

2. HTTP req/res의 일회성

Servlet의 request는 HttpServletRequest.getInputStream() 스트림 기반이기 때문에 일회성 특징을 가진다. 따라서 필터나 인터셉터에서 읽어버리면 이후 실제 비즈니스 로직을 처리해야 하는 컨트롤러에서 req를 읽을 수 없는 문제가 있다. 그래서 그에 대한 대안으로 아래 전략을 사용했다,

 

3. ContentCachingRequestWrapper, ResponseWrapper 도입

요청이 서버에 들어오면 먼저 ContentCachingRequestWrapper로 감쌌다.

req를 메모리에 캐시해서 나중에 getContentAsByteArray로 req body를 여러 번 읽을 수 있도록 했다. 이 wrapper를 사용하면 스트림을 메모리에 저장하므로 필터에서 로깅을 하더라도 컨트롤러에서도 req body를 문제 없이 사용할 수 있게 된다. 

 

req와 마찬가지로 res도 HttpServletResponse.getOutputStream()이라 기본적으로 한 번만 쓸 수 있다. 그래서 ResponseWrapper라는 커스텀 응답 래퍼를 만들어 출력 스트림을 가로채고 ByteArrayOutputStream에 복사(메모리에 저장)해서 로깅과 실제 응답 둘 다 처리했다. (원래 응답도 그래도 클라이언트에게 전달하고, 로깅도 하고!)

 

조금 더 자세하게 절차를 설명하면,

요청/응답 둘 다 스트림을 가로채 메모리에 저장한 이후 chain.doFilter()를 호출해 실제 컨트롤러로 요청을 전달한다. 요청 처리가 끝난 이후 다시 필터로 돌아오고, 이때 요청과 응답 본문을 포함한 모든 데이터를 하나의 로그로 묶어 파일에 기록하고자 했다. 

그리고 메모리에 저장된 응답 본문을 다시 HttpServletResponse의 OutputStream에 복사해서 실제 클라이언트에 전달했다. 

(수동으로 최종 출력 스트림에 복사하는 로직이 없으면 클라이언트는 아무 응답도 받지 못하고 연결이 끊긴다.)

 

4. access_log 파일화

앞서 실제 doFilter() 호출 전 ContentCachingRequestWrapper, ResponseWrapper로 미리 감싸두었다고 했는데,

요청 바디는 이미 요청 객체에 들어있기 때문에 감싸자마자 바로 읽을 수 있지만 응답 바디는 컨트롤러가 처리하고 나서 생기기 때문에 아직 아무것도 들어있지 않다. chain.doFilter(requestWrapper, responseWrapper) 로직 이후에야 응답 데이터가 메모리에 채워진다. 

그리고 다시 filter로 돌아와서 요청/응답을 모두 로깅한 뒤 responseWrapper 내용을 복사하여 클라이언트에게 반환한 것이다.

 

로그 파일이 너무 커지면 관리가 힘들기 때문에 하루 단위로 로그를 구분해 저장하여 파일 용량 관리나 장애 대응 시 특정 날짜에 빠르게 확인할 수 있도록 했다. 

 

 

엑세스 로그를 파일로 관리하는 과정을 코드로 구현하면서 Servlet Filter의 작동원리에 대해 알 수 있었다.

중간 고사 시험 치는 주간... 시험이 더 남아있지만 집중력이 흐뜨러질 때 이제 개발 공부를 하는 상황이 당연해졌다 다시 할 거 합시다