새로운 내용을 공부할 때
새로운 내용의 공부를 시작할 때 용어의 정의를 이해하지 못하거나 정확하게 알지 못한다면 그 용어가 포함된 문장을 이해하지 못합니다.
작은 단어 하나가 내용을 이해하지 못하게 하기 때문에 용어를 정확하게 이해하는 것이 중요합니다.
TIL) 필터와 인터셉터- 공통 관심사 처리
📌 2025-04-05 TIL
주제
- 필터와 인터셉터의 학습 목적
- 필터와 인터셉터 비교
학습 목적
웹 공통 관심사
필터와 인터셉터는 애플리케이션에서 인증, 인가, 인코딩, 로깅, 보안 등 여러 로직이 공통으로 적용되어야 하는 부분(공통 관심사)를 처리합니다.
-
컨트롤러마다 이런 로직을 직접 구현하면 코드 중복이 발생하고, 수정시 모든 컨트롤러를 변경해야하는 유지보수 문제가 생깁니다.
-
스프링에서는 스프링 AOP를 활용할 수 있지만, 웹 관련 공통 관심사(HTTP 헤더, URL 정보 및 분기 처리가 필요한 경우)는 서블릿 필터와 스프링 인터셉터가 적합합니다.
AOP로 구현할 수 있지만 동료 개발자가 웹 공통 관심사이며 구조 위치를 알 수 있는 필터와 인터셉터를 사용하는 것이 팀 프로젝트에 더 적합하다고 생각도 듭니다.
-
HttpServletRequest
와HttpServletReponse
를 제공하여 웹 요청/응답을 직접 다룰 수 있습니다.
공통 관심사가 왜 필요할까
-
코드 중복 최소화
인증, 인가, 인코딩, 로깅등 공통으로 필요한 기능을 각 컨트롤러에 직접 구현하면 코드 중복이 발생하고 유지보수가 어려워집니다.
개발자가 실수로 모두 수정하지 못할 경우 문제가 발생할 수 있으며 찾기도 어려워 집니다. 그러면서 중복된 코드끼리 버전이 생기기도 합니다.
-
관심사의 분리
애플리케이션의 비즈니스 로직과 웹 요청 전처리/후처리 로직을 분리하여, 코드 가독성과 유지보수하기 편해집니다.
-
재사용성 및 일관성
한 곳에서 공통 관심사를 처리하면, 전체 애플리케이션에서 일관된 보안 및 처리를 할 수 있습니다.
본론
웹 요청 흐름을 간단한 다이어 그램으로 그려보면
[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
로 요청을 하면 필터는 해당 요청의 dispatcherType
과 filterUrl
이 일치하면 필터체인에 추가합니다.
새로 생성한 필터 체인에 값을 추가합니다.
응답 커스터마이징 및 예외 처리
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가지 방법을 선택할 수 있습니다.
-
throw new Exception()
을 통해 스프링 MVC에게 처리 위임하기 -
직접 응답 생성하기(
getWriter
) -
response.sendError
을 통해 서블릿 컨테이너에게 전달하기 -
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
-
RequestDispatcher
으로 포워딩request.getRequestDispatcher("/api/error").forward(request, response)
-
비동기 처리 방식 > 추후 ` 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에 대해 동작합니다. |
댓글남기기