새로운 내용을 공부할 때
새로운 내용의 공부를 시작할 때 용어의 정의를 이해하지 못하거나 정확하게 알지 못한다면 그 용어가 포함된 문장을 이해하지 못합니다.
작은 단어 하나가 내용을 이해하지 못하게 하기 때문에 용어를 정확하게 이해하는 것이 중요합니다.

4 분 소요

📌 2025-04-05 TIL

주제

  • 필터와 인터셉터의 학습 목적
  • 필터와 인터셉터 비교

학습 목적

웹 공통 관심사

필터와 인터셉터는 애플리케이션에서 인증, 인가, 인코딩, 로깅, 보안 등 여러 로직이 공통으로 적용되어야 하는 부분(공통 관심사)를 처리합니다.

  • 컨트롤러마다 이런 로직을 직접 구현하면 코드 중복이 발생하고, 수정시 모든 컨트롤러를 변경해야하는 유지보수 문제가 생깁니다.

  • 스프링에서는 스프링 AOP를 활용할 수 있지만, 웹 관련 공통 관심사(HTTP 헤더, URL 정보 및 분기 처리가 필요한 경우)는 서블릿 필터와 스프링 인터셉터가 적합합니다.

    AOP로 구현할 수 있지만 동료 개발자가 웹 공통 관심사이며 구조 위치를 알 수 있는 필터와 인터셉터를 사용하는 것이 팀 프로젝트에 더 적합하다고 생각도 듭니다.

  • HttpServletRequestHttpServletReponse를 제공하여 웹 요청/응답을 직접 다룰 수 있습니다.

공통 관심사가 왜 필요할까

  1. 코드 중복 최소화

    인증, 인가, 인코딩, 로깅등 공통으로 필요한 기능을 각 컨트롤러에 직접 구현하면 코드 중복이 발생하고 유지보수가 어려워집니다.

    개발자가 실수로 모두 수정하지 못할 경우 문제가 발생할 수 있으며 찾기도 어려워 집니다. 그러면서 중복된 코드끼리 버전이 생기기도 합니다.

  2. 관심사의 분리

    애플리케이션의 비즈니스 로직과 웹 요청 전처리/후처리 로직을 분리하여, 코드 가독성과 유지보수하기 편해집니다.

  3. 재사용성 및 일관성

    한 곳에서 공통 관심사를 처리하면, 전체 애플리케이션에서 일관된 보안 및 처리를 할 수 있습니다.

본론

웹 요청 흐름을 간단한 다이어 그램으로 그려보면

 [WAS]
   │
   ▼
[필터 체인]          ← 서블릿 컨테이너 레벨 (스프링 외 요청 포함)
   │
   ▼
[서블릿 (DispatcherServlet)]
   │
   ▼
[인터셉터 체인]      ← 스프링 MVC 내부 (preHandle → 컨트롤러 → postHandle → afterCompletion)
   │
   ▼
[컨트롤러]
   │
   ▼
 [응답]
  • 필터 : WAS의 서블릿 사이에 동작합니다. 정적 리소스 요청이나 스프링 외 요청까지 포함한 전역 제어 가능합니다.
  • 인터셉터 : 스프링 MVC의 DispatcherServlet 내부에서 컨트롤러 호출 전후로 동작하며, 스프링 컨택스트에 의해 관리됩니다.

필터(Servlet Filter)

필터는 서블릿 전/후처리 컴포넌트다.

특징 및 장점

전체 요청 제어

서블릿 컨테이너 레벨에서 실행되어, 정적 리소스 요청까지 포함한 모든 HTTP 요청을 제어할 수 있습니다.

chan.doFilter()를 호출하지 않고, return;하면 요청 흐름을 여기서 완전히 종료할 수 있습니다.

정적 체인 구성

URL 패턴, HTTP 메서드, 보안 설정 등에 따라 요청별로 정적인 필터 체인을 구성할 수 있습니다.

예를 들어 /api/**/admin/에 서로 다른 필터 체인 적용가능합니다.

새 요청마다 새로운 필터 체인을 만듭니다.

// dir: org.apache.catalina.core.ApplicationFilterFactory
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
  /* 
  앞에 로직 생략
  */
  for (FilterMap filterMap : filterMaps) {
      if (!matchDispatcher(filterMap, dispatcher)) {
          continue;
      }
      if (!FilterUtil.matchFiltersURL(filterMap, requestPath)) {
          continue;
      }
      ApplicationFilterConfig filterConfig =
              (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
      if (filterConfig == null) {
          log.warn(sm.getString("applicationFilterFactory.noFilterConfig", filterMap.getFilterName()));
          continue;
      }
      filterChain.addFilter(filterConfig);
  }
}

사용자가 /api/v1/member로 요청을 하면 필터는 해당 요청의 dispatcherTypefilterUrl이 일치하면 필터체인에 추가합니다.

새로 생성한 필터 체인에 값을 추가합니다.

응답 커스터마이징 및 예외 처리

doFilter()내에서 try-catch로 예외를 잡고, response.reset()을 통해 응답을 재구성할 수 있습니다.

public class ExceptionHandlingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletResponse res = (HttpServletResponse) response;
        try {
            chain.doFilter(request, response); // 정상 처리
        } catch (Exception e) {
            res.reset(); // 응답 초기화
            res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            res.setContentType("application/json");
            PrintWriter writer = res.getWriter();
            writer.write("{\"error\": \"Internal Server Error: " + e.getMessage() + "\"}");
            writer.flush();
        }
    }
}

다만, 예외가 아니라 response.sendError()으로 마스킹을 했다면 서블릿 컨테이너가 해당 정보를 확인하고 다시 재요청을 합니다.

필터도 응답 처리 방식을 해야한다.

필터나 인터셉터 모두 이후 동작을 하지 않을 수 있습니다.

그러면 상위 계층에게 알리거나 자신이 직접 처리해야합니다.

그렇지 않으면 아무 의미 없는 아래와 같은 결과가 나오게 됩니다.

HTTP/1.1 200 
Connection: keep-alive
Content-Length: 0
Date: Sun, 06 Apr 2025 02:53:39 GMT
Keep-Alive: timeout=60


스프링 컨테이너 외부에서 동작

필터는 스프링 컨택스트 외부에서 동작하여, 스프링에 종속되지 않고 다른 프레임워크에서 사용 가능합니다

  • 스프링 애플리케이션 컨텍스트 초기화 전에 실행 가능합니다.

  • 스프링 프레임워크의 내부 처리 과정을 거치지 않습니다.

    스프링 프레임워크의 내부처리는 핸들러 매핑, 어댑터 찾기, 스프링 요청 및 응답 프로세스 과정을 거치지 않습니다.

  • 서블릿 API를 직접 사용하여 요청/응답 처리를 합니다.

인터셉터(Spring HandlerInterceptor)

특징 및 장점

스프링 MVC 내부 처리

DispatcherServlet 내부에서 동작하여, 컨트롤러 전 후 응답 후 와 같이 세밀한 처리가 가능합니다.

고정된 체인 구성

InterceptorRegistry에 등록된 순서(@Order)로 고정된 체인 실행합니다.

요청별 동적 체인 구성은 어려우나 필요하다면 인터셉터 내의 분기문으로 실행이 가능합니다.

1. 조건부 로직 적용
@Override
public boolean preHandle(...) {
    if (isApiRequest(request)) {
        // API 요청 전용 로직
    } else if (isAdminRequest(request)) {
        // 관리자 요청 전용 로직
    }
    return true;
}
2. 컨택스트 기반 인터셉터
@Autowired
private UserContextHolder contextHolder;

@Override
public boolean preHandle(...) {
    UserContextHolder context = contextHolder.getContext();
    if (context.hasRole("ADMIN")) {
        // 관리자용 로직
    }
    return true;
}

제한된 응답 커스터마이징 및 예외 처리

preHandle에서 종료를 원하는 경우 3가지 방법을 선택할 수 있습니다.

  1. throw new Exception() 을 통해 스프링 MVC에게 처리 위임하기

  2. 직접 응답 생성하기(getWriter)

  3. response.sendError을 통해 서블릿 컨테이너에게 전달하기

  4. response.sendRedirect으로 리다이렉트

    HTTP/1.1 302 
    Connection: keep-alive
    Content-Length: 0
    Date: Sun, 06 Apr 2025 05:13:52 GMT
    Keep-Alive: timeout=60
    Location: http://localhost:8080/login
    
  5. RequestDispatcher으로 포워딩

    request.getRequestDispatcher("/api/error").forward(request, response)
    
  6. 비동기 처리 방식 > 추후 ` TIL `작성

afterCompletion에서 예외 처리 가능하나, 응답이 이미 커밋된 경우 재구성 어렵습니다.

DI 및 스프링 통합

스프링 빈으로 관리되어 DI 활용 가능하며, HandlerMethod로 컨트롤러 메서드 정보(예: 어노테이션) 활용에 유리합니다.

public class ExceptionHandlingInterceptor implements HandlerInterceptor {
  @Override
  public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
    if (handler instanceof HandlerMethod) {
        HandlerMethod method = (HandlerMethod) handler;
        if (method.hasMethodAnnotation(AdminOnly.class) 
            && request.getHeader("X-Custom-Auth") == null) {
            response.sendRedirect("/login?redirect="+request.getRequestURI());
            return false;
        }
    }
    return true;
  }
}

어노테이션을 체크하여 정보로 활용할 수 있습니다.

비교표

비교 항목 필터 (정적 체인 구성) 인터셉터
실행 위치 WAS → 서블릿 컨테이너 (스프링 외부 포함) 스프링 MVC 내부 (DispatcherServlet 이후)
체인 구성 요청별 정적 구성
요청이 들어올 때마다 새 체인을 만든다.
고정된 순서로 모든 인터셉터 순회
응답 커스터마이징 response.reset()으로 자유롭게 재구성 뷰 렌더링 후 수정 어려움
예외 처리 즉시 잡아 처리 및 분기 가능 afterCompletion 또는 @ControllerAdvice로 처리
스프링 의존성 스프링 외부에서 독립적 스프링 컨텍스트에 의존, DI 용이
테스트 용이성 모킹 환경 구성 복잡 MockMvc로 통합 테스트 쉬움
예외발생시 서블릿 컨테이너에서 예외처리 스프링 컨테이너에서 예외 처리
사용 목적 토큰이나 인코딩 등 HTTP 레벨의 공통 관심사 비즈니스 레벨에서 인증, 인가 등 공통 관심사
DispatcherType 다양한 타입에 동작할 수 있습니다. 주로 REQUEST에 대해 동작합니다.

댓글남기기