이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Servlet Filter는 chain.doFilter를 기준으로 전처리와 후처리를 어떻게 나누는가?
  • Spring Security가 Filter 기반으로 동작해야 하는 이유는 무엇인가?
  • 커스텀 Filter를 추가할 때 순서와 중단 조건을 어떻게 판단해야 하는가?

개요

Servlet Filter는 HTTP 요청과 응답을 가장 앞에서 감싸는 표준 확장 지점이다. Filter는 다음 Filter 또는 Servlet으로 요청을 넘기기 전후에 코드를 실행할 수 있다. Spring MVC의 Controller가 실행되기 전에 동작하므로 보안, CORS, 인코딩, request wrapping, access log 같은 기능에 적합하다.

Spring Security가 Filter 기반으로 동작하는 이유도 여기에 있다. 인증과 인가는 Controller 내부 로직이 아니라 애플리케이션 진입 전 요청 자체에 대한 판단이어야 한다.

Filter 흐름

public class TimingFilter extends OncePerRequestFilter {
 
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {
        long start = System.nanoTime();
        try {
            filterChain.doFilter(request, response);
        } finally {
            long elapsed = System.nanoTime() - start;
            log.info("{} {} status={} elapsed={}ns",
                    request.getMethod(),
                    request.getRequestURI(),
                    response.getStatus(),
                    elapsed);
        }
    }
}

filterChain.doFilter 이전은 전처리다. 요청 header를 읽거나 request id를 만들 수 있다. 이후는 후처리다. 응답 status와 처리 시간을 기록할 수 있다. doFilter를 호출하지 않으면 뒤 필터와 DispatcherServlet은 실행되지 않는다.

Security FilterChain

Spring Security는 Servlet Filter 하나로 끝나지 않는다. DelegatingFilterProxy가 Servlet Container와 Spring Bean을 연결하고, FilterChainProxy가 요청에 맞는 SecurityFilterChain을 선택한다. 선택된 체인 안에는 인증 필터, 예외 변환 필터, 인가 필터 등 여러 Security Filter가 순서대로 들어 있다. 보안 필터들은 보통 Servlet Container에 각각 직접 등록되는 것이 아니라 FilterChainProxy 내부 체인으로 관리된다.

커스텀 JWT 필터를 추가할 때도 이 순서가 중요하다. JWT를 읽어 Authentication을 SecurityContext에 넣는 필터는 인가 판단보다 앞에 있어야 한다. 그렇지 않으면 AuthorizationFilter가 인증되지 않은 사용자로 판단할 수 있다. 반대로 AuthorizationFilter 뒤에 추가한 필터는 이미 접근 결정이 끝난 뒤 실행될 수 있다.

중단과 응답

Filter는 요청을 중단할 수 있다. 인증 실패, rate limit 초과, 잘못된 request body 같은 상황에서 직접 응답을 쓰고 더 이상 doFilter를 호출하지 않을 수 있다.

하지만 중단 책임이 명확해야 한다. 모든 예외를 Filter에서 직접 JSON으로 바꾸기 시작하면 Spring MVC 예외 처리와 Security 예외 처리 흐름이 섞인다. Security 인증/인가 실패는 entry point와 access denied handler를 쓰는 편이 일관적이다.

실전 팁

  • Filter는 HTTP 요청 전체를 감싸야 하는 기능에 둔다.
  • doFilter 호출 여부와 위치를 코드 리뷰에서 반드시 확인한다.
  • 커스텀 Security Filter는 어떤 기본 필터 앞이나 뒤에 둘지 명시한다.
  • 같은 기능의 필터를 Servlet Container와 SecurityFilterChain 양쪽에 중복 등록하지 않는다.
  • 인증 실패 응답은 Filter 내부 try-catch보다 Security 예외 처리 구성과 맞춘다.
  • request body를 여러 번 읽어야 하면 wrapper를 사용하고 메모리 비용을 고려한다.

위험 신호!

  • Filter에서 doFilter를 호출하지 않아 모든 요청이 Controller에 도달하지 않는다.
  • 커스텀 JWT Filter가 모든 경로에서 예외를 던져 public endpoint도 막는다.
  • Security Filter 순서를 모른 채 addFilterBefore를 복사한다.
  • Security용 커스텀 필터를 일반 Servlet Filter로 등록해 Spring Security 체인 순서 밖에서 동작하게 만든다.
  • Filter에서 HandlerMethod 정보를 얻기 위해 MVC 내부를 억지로 호출한다.
  • request body logging Filter가 큰 파일 업로드를 메모리에 모두 올린다.

확인 질문

확인 질문

  • doFilter 앞뒤는 각각 어떤 의미인가?
    • 앞은 다음 필터와 Servlet 실행 전 전처리이고, 뒤는 실행 후 후처리다.
  • Spring Security가 Filter 기반인 이유는 무엇인가?
    • Controller 이전에 요청의 인증, 인가, 예외, CSRF, CORS를 처리해야 하기 때문이다.
  • 커스텀 인증 필터의 위치를 정할 때 핵심 기준은 무엇인가?
    • Authentication을 만들어야 하는 시점이 인가 필터보다 앞인지 확인하는 것이다.

참고 문서