Spring 숙련 (Filter, Interceptor)

KimGwangmin·2026년 9월 14일

Filter

HTTP 요청이 DispatcherServlet 전과 후 실행되는 기능
요청 전에만 실행되지 않고, 후에도 실행될 수 있다

OncePerRequestFilter

같은 요청에서 필터가 중복 실행되지 않게 관리하는 클래스

  • OncePerRequestFilter를 상속받아 필터를 구현할 수 있다.
    • 원래 필터를 구현하는 방식은 Filter라는 인터페이스를 구현하는 것이었다.
    • 하지만, 이 방식에는 한 요청에 두 번 이상 필터가 동작하는 문제가 있다.
      • 300번대 응답(Redirect)에서 발생할 수 있는 현상이다.
    • 따라서 현대 Spring에서는 OncePerRequestFilter를 사용하는 것을 권장하고 있다.
      • 이 클래스도 따라 올라가다보면 Filter를 구현한 추상 클래스이다.
      • 내부적으로 이미 처리된 필터인지를 추적하는 기능이 있다.

예제

필터 정의

@Slf4j
public class RequestLoggingFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain
    ) throws ServletException, IOException {
        long start = System.nanoTime();
        log.info("BEFORE {} {}", request.getMethod(), request.getRequestURI());
        try {
            chain.doFilter(request, response);
        } finally {
            long tookMs = (System.nanoTime() - start) / 1_000_000;
            log.info("AFTER {} {} -> {} ({}ms)", request.getMethod(),
                    request.getRequestURI(), response.getStatus(), tookMs);
        }
    }
}

필터 등록
앞 강의 프로퍼티 바인딩과 비슷하게, 별도 설정 클래스에서 등록할 수도 있고, 필터 클래스 자체를 빈 등록 할 수도 있다.
1. @Configuration 클래스 사용
- addUrlPatterns을 통해 특정 경로의 API에 대해 필터가 동작하도록 할 수 있다.

@Configuration
public class FilterConfig {
    @Bean // 메서드 레벨 빈 등록
    public FilterRegistrationBean<RequestLoggingFilter> requestLoggingFilter() {
        FilterRegistrationBean<RequestLoggingFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new RequestLoggingFilter());
        registration.addUrlPatterns("/posts", "/posts/*"); // 필터 적용할 요청 종류
        registration.setOrder(1); // 필터 적용 순서 (이 경우 첫 번째)
        return registration;
    }
}
  1. @Component로 자체 등록, @Order(n)로 순서 지정
    • 단, 이 방식으로는 위의 addUrlPatterns 같은 효과를 보기가 어렵다. (가능은 하지만 조금 복잡하다.)
      • 필터 내에서 API를 읽어와서 직접 분기를 나누는 방식으로 구현할 수 있다.
    • 전역적으로 모든 API에 대해 필터가 동작한다.
@Slf4j
@Component
@Order(1)
public class RequestLoggingFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain
    ) throws ServletException, IOException {
        long start = System.nanoTime();
        log.info("BEFORE {} {}", request.getMethod(), request.getRequestURI());
        try {
            chain.doFilter(request, response);
        } finally {
            long tookMs = (System.nanoTime() - start) / 1_000_000;
            log.info("AFTER {} {} -> {} ({}ms)", request.getMethod(),
                    request.getRequestURI(), response.getStatus(), tookMs);
        }
    }
}

Interceptor

  • 컨트롤러 실행 전후에 공통 처리를 넣는다.
  • 인터셉터는 HandlerInterceptor 인터페이스를 구현하여 사용할 수 있다.

(`preHandle` = 컨트롤러 전 / `postHandle` = 컨트롤러 후 / `afterCompletion` = 요청 처리 완전 종료 후)
  • 핸들러(컨트롤러 메서드)를 선택한 뒤 인터셉터가 실행되기 때문에 인터셉터는 해당 정보를 확인할 수 있다.

예제

  • preHandle, postHandle, afterCompletion 중 필요한 메서드만 구현한다.
  • 이 중 preHandle은 리턴값이 boolean이다. (나머지는 void)
    • true라면 컨트롤러 혹은 다음 인터셉터로 계속 진행한다.
    • false라면 중단한다. (컨트롤러 메서드가 실행되지 않는다.)
  • postHandle은 컨트롤러 메서드 실행 직후 호출된다. 컨트롤러에서 예외 발생 시 건너뛴다.
  • afterCompletion은 클라이언트에게 응답 전송을 마치고 요청처리가 완전히 끝난 시점에 호출된다. (DispatcherServlet 기준) 정상 종료든 예외 상황이든 무조건 실행된다. (preHandle이 true를 반환했다면 afterCompletion 역시 반드시 호출된다.)
@Slf4j
@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler
    ) {
        log.info("preHandle: {}", request.getRequestURI());
        return true;
    }

    @Override
    public void postHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler,
            ModelAndView modelAndView
    ) {
        log.info("postHandle: {} -> {}", request.getRequestURI(), response.getStatus());
    }

    @Override
    public void afterCompletion(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler,
            Exception ex
    ) {
        log.info("afterCompletion: {} -> {}", request.getRequestURI(), response.getStatus());
    }
}

필터와 마찬가지로, 인터셉터도 빈 등록을 해야 한다.

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
    private final RequestLoggingInterceptor requestLoggingInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(requestLoggingInterceptor)
                .addPathPatterns("/posts/**") // 인터셉터를 적용할 경로
                .excludePathPatterns("/posts/public/**"); // 인터셉터를 제외할 경로
    }
}

.

  • addPathPatterns와 excludePathPatterns가 충돌하는 경우 excludePathPatterns가 항상 우선시된다. (먼저 통합 경로를 지정하고, 그 중에서 세부 경로를 쳐내는 방식)

    • addPathPatterns 안에 여러 경로를 함께 넣을 수 있다. (가변인자도 되고, 리스트로 넘겨도 됨) 혹은 반복적으로 체이닝도 가능하다. (excludePathPatterns도 마찬가지)
  • **도 *와 같은 와일드카드이지만, 조금 더 포함 범위가 넓다.

    • *(단일 와일드카드): 하나의 경로 세그먼트(/ 사이 문자열)
      • /posts/*라면 /posts/1, /posts/2는 되지만, /posts/1/comments와는 매칭 안됨
    • **(다중 와일드카드): 0개 이상의 경로 세그먼트 전체
      • /posts/**라면 경로 깊이에 무관하게 /posts로 시작하는 모든 URL과 매칭됨 (하위 세그멘트가 없는 /posts도 가능)

Filter와 Interceptor

  • Filter
    • 웹 앱의 가장 바깥에서 부적절한 요청을 걸러내는 역할
    • 시스템 외부(DispatcherServlet에 도착하기 전후, 스프링 MVC 영역 밖)에서 처리
    • 요청 자체의 바이트, 헤더 조작이나 전역 차단에 적합
  • Interceptor
    • 요청이 시스템 내부로 진입한 상태에서 컨트롤러로 향하는 요청을 가로채는 역할
    • 어떤 컨트롤러 메서드를 실행할지 결정한 상황에서 추가 작업 수행
    • 메서드 세부 정보나 어노테이션에 따라 분기 처리를 하는 데에 적합

전체 실행 순서 흐름

  1. Filter (전처리 로직): chain.doFilter() 호출 전
  2. DispatcherServlet 진입
  3. Interceptor (preHandle)
  4. Controller (핸들러 실행)
  5. Interceptor (postHandle): 정상 수행 시 뷰 렌더링 직전 호출
  6. 뷰 렌더링 / 응답 바디 완성 (DispatcherServlet 내부 작업 마무리)
  7. Interceptor (afterCompletion): DispatcherServlet이 HTTP 요청 처리를 완전히 끝내기 직전 호출
  8. DispatcherServlet 실행 종료 후 호출자(서블릿 필터 체인)로 반환
  9. Filter (후처리 로직): chain.doFilter() 다음 코드 및 finally 블록 실행

0개의 댓글