[Spring] Filter & Interceptor

박상민·2025년 4월 24일

Spring

목록 보기
11/12
post-thumbnail

서론

웹 요청 흐름 속에서 필터와 인터셉터의 역할
Spring Application에서 사용자의 요청은 단순히 컨트롤러에 바로 도달하지 않는다.
요청은 여러 계층을 거쳐 처리되며, 이 과정에서 Filter와 Interceptor는 각각의 위치에서 중요한 역할을 수행한다.

  • Filter: Servlet Container 수준에서 요청과 응답을 감싸며, 보안, 로깅, 인코딩 처리 등의 전역적인 전처리에 사용된다.
  • Interceptor: Spring MVC의 DispatcherServlet 이후에 동작하며, 컨트롤러 실행 전후에 추가 로직을 삽입할 수 있다.

결국, Filter & Interceptor는 웹 요청 흐름에서 공통 관심사(Cross-Cutting Concern)를 깔끔하게 분리할 수 있도록 도와주며, 유지보수성과 확장성을 크게 높여준다.

왜 알아야 할까?
실무에서는 사용자 인증/인가, 요청 로깅, 성능 측정, 응답 조작 등 다양한 전/후처리 작업이 요구된다.이때 Filter와 Interceptor를 적절히 선택하고 구현할 수 있어야, 깔끔하고 안정적인 API 서비스를 제공할 수 있다.

또한, 이 둘은 AOP, ArgumentResolver 등과 함께 Spring 생태계에서 핵심적인 미들웨어 구성 요소로, 그 동작 위치와 원리를 이해하는 것은 필수적이다.

Filter란 무엇인가?

Servlet Filter 개념
Filter는 JavaEE(or Jakarta EE)의 표준 Servlet 스펙의 일부로, 클라이언트 요청이 서블릿(혹은 Spring의 DispatcherServlet)에 도달하기 이전 또는 이후에 요청/응답 객체를 가로채 처리할 수 있는 컴포넌트이다.

실행 시점 예시
요청 인코딩 처리, 응답 압축(Gzip), XSS 방지, JWT 토큰 추출 등

실행 시점: DispatcherServlet 이전
Filter는 DispatcherServlet보다 앞서 동작하기 때문에, Spring Context에 접근하지 않아도 모든 요청을 가로챌 수 있다. 따라서 Spring의 Controller, Service 로직과는 별개로 전역적인 요청 흐름 제어에 적합하다

전/후 처리 방식
Filter는 아래 구조처럼 동작하며, 체인 방식으로 필터링 로직을 구성할 수 있다.

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    
    // 요청 전 처리
    chain.doFilter(request, response); // 다음 필터 or 서블릿으로 전달
    // 응답 후 처리
}

코드 예제: OncePerRequestFilter 기반 로깅 필터

  • Spring Boot에서는 OncePerRequestFilter를 상속받아 한 요청당 한 번만 실행되는 필터를 손쉽게 구현할 수 있다.
@Component
public class LoggingFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        long startTime = System.currentTimeMillis();

        log.info("Request: [{}] {}", request.getMethod(), request.getRequestURI());

        filterChain.doFilter(request, response); // 다음 필터 or 서블릿으로 전달

        long duration = System.currentTimeMillis() - startTime;

        log.info("Response: [{}] {} ({}ms)",
                 response.getStatus(), request.getRequestURI(), duration);
    }
}

위 필터는 모든 요청과 응답을 로깅하며, 처리 시간을 측정하는 데도 활용 가능하다.

Interceptor란 무엇인가?

Spring HandlerInterceptor 개념
Interceptor는 Spring MVC에서 제공하는 기능으로, 컨트롤러 핸들러(ex. @RequestMapping)가 실행되기 전/후에 공통 로직을 삽입할 수 있도록 도와주는 컴포넌트다.

  • org.springframework.web.servlet.HandlerInterceptor 인터페이스 구현
  • 주로 인증, 권한 검사, 요청/응답 로깅, 사용자 세션 검사 등에 활용됨
  • Spring Context 안에서 동작하므로, 의존성 주입이 자유롭고, Spring 빈 사용도 가능

실행 시점: DispatcherServlet 이후, Controller 전/후
Filter와 달리 Interceptor는 Spring의 DispatcherServlet 이후에 동작하며, HandlerMapping에 의해 선택된 선트롤러가 실행되기 전/후의 시점을 제어할 수 있다.

즉, Spring MVC 컨트롤러와 밀접하게 연결되어 있으며, 요청 흐름 안에서 더욱 세밀한 제어가 가능하다.

코드 예제: 로그인 인증 인터셉터

@Component
public class AuthInterceptor implements HandlerInterceptor {

    private final JwtTokenProvider jwtTokenProvider;

    public AuthInterceptor(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws IOException {

        String authHeader = request.getHeader("Authorization");

        // Authorization 헤더 없거나 형식이 잘못된 경우
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("Unauthorized: No or Invalid Authorization header");
            return false;
        }

        String token = authHeader.substring(7); // "Bearer " 제거

        // 토큰 유효성 검증 실패
        if (!jwtTokenProvider.validateToken(token)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("Unauthorized: Invalid token");
            return false;
        }

        // 토큰에서 사용자 정보 추출 후 context에 저장 (선택 사항)
        String userId = jwtTokenProvider.getUserIdFromToken(token);
        request.setAttribute("userId", userId);

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) {
        // 요청 완료 후 리소스 정리, 로깅 등
        log.info("✅ Completed request: {}", request.getRequestURI());
    }
}

preHandle, postHandle, afterCompletion 동작 원리

메서드실행 시점주요 역할
preHandle()컨트롤러 실행 이전인증 검사, 요청 거절 가능 (false 반환 시 중단)
postHandle()컨트롤러 실행 이후, View 렌더링 이전모델 데이터 가공, 추가 정보 삽입 등
afterCompletion()View 렌더링 이후예외 처리, 리소스 정리, 로깅 등

Filter VS Interceptor

항목FilterInterceptor
실행 위치DispatcherServlet 이전, 가장 바깥단에서 동작DispatcherServlet 이후, 컨트롤러 호출 전/후에 동작
기반 기술Java EE Servlet 표준 (javax.servlet.Filter)Spring MVC (HandlerInterceptor)
주요 목적요청/응답 전처리, 인코딩, CORS, 보안 필터링인증, 인가, 컨트롤러 후처리, 로깅 등
적용 대상모든 요청 (HttpServletRequest, HttpServletResponse)컨트롤러 핸들러 (@Controller, @RequestMapping 등)
등록 방식@Component, FilterRegistrationBean 사용WebMvcConfigurer.addInterceptors() 오버라이딩
동작 메서드doFilter() 메서드 하나만 사용preHandle(), postHandle(), afterCompletion() 세 가지 메서드 제공
예외 처리예외 발생 시 체인 다음 필터로 전달 안됨afterCompletion()에서 예외 후처리 가능
빈 주입Servlet 스펙상 Spring 빈 주입이 제한적임Spring 컨텍스트 기반이므로 DI 완전 지원
사용 예시XSS 방지, 로깅, 응답 압축, 토큰 파싱 등사용자 인증, 권한 체크, 요청/응답 로깅, 세션 검증 등

실무에서는 언제 무엇을 써야 할까?

  • 전역 전처리 로직 (모든 요청에 공통 적용되는 작업): Filter 사용 권장
    예: CORS 설정, XSS 방지, 공통 인코딩 처리 등

  • 컨트롤러 전후 흐름 제어가 필요한 경우: Interceptor 사용
    예: 사용자 인증/인가, 세션 체크, 응답 데이터 후처리 등

결론

핵심 요약

  • Filter는 Servlet 기반, 전역 요청 흐름 제어에 적합
  • Interceptor는 Spring MVC 기반, 컨트롤러 전후 세밀한 로직 제어에 적합
  • 실무에서는 목적과 위치에 따라 적절한 도구 선택이 중요함

확장 개념 추천 학습

  • AOP (Aspect-Oriented Programming): 더 세밀하고 유연한 전후처리
  • HandlerMethodArgumentResolver: 컨트롤러 파라미터 가공
  • ExceptionHandler / ControllerAdvice: 글로벌 에러 핸들링

출처
https://www.baeldung.com/spring-boot-add-filter
https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-config/interceptors.html

0개의 댓글