
인프런을 통해 스프링 강좌를 듣다가 문득 필터와 인터셉터의 개념에 대해 전혀 감이 오지 않게 되었다. 아마 처음 들었을 때에도 바로 이해하기에는 조금 막혀서 건너뛰었던 듯 하다. 그래서 이번 기회에 정리를 해보고 싶다. 웹페이지에 로그인을 하는데 필터와 인터셉터라는 게 무엇이길래 왜 필요한 걸까?
애플리케이션은 각종 컨트롤러들이 함께 작동하면서 컨트롤러 간 공통 관심사(cross-cutting concerns)를 갖게 된다. 공통 관심사란 각각의 로직이 가진 관심사가 있다고 할 때, 로직별로 하나의 관심사, 즉 프로그램의 한 방향 또는 관점(Aspect)이 중첩될 수 있다는 것이다. 그래서 이러한 공통 관심사에 의해 복잡하게 얽힌 의존성이 발생하게 되는데, 이를 처리할 때에는 스프링의 AOP(Aspect Oriented Programming)를 사용하는 경우가 많지만, 웹과 관련된 경우에는 공통 관심사를 처리할 때 HTTP 헤더나 URL 정보들이 필요하므로 HttpServletRequest를 제공하는 서블릿 필터나 스프링 인터셉터를 사용하는 것이 좋다.
AOP의 개념은 다음 링크([Spring] Spring AOP 개념)를 참고하자.
그렇다면 서블릿 필터와 스프링 인터셉터는 무엇일까? 우선 필터는 서블릿에서 제공하고 인터셉터는 스프링 MVC에서 제공하는 기술로, 이 둘은 적용 순서와 범위, 그리고 사용방법에서 차이가 있다.

- HTTP 요청 → WAS → [필터] → 서블릿 → 컨트롤러
- HTTP 요청 → WAS → 필터 → 서블릿 → [인터셉터] → 컨트롤러
필터는 서블릿 호출 이전에 호출되고, 인터셉터는 스프링 MVC의 시작점이 디스패처 서블릿이기 때문에 서블릿 호출 이후, 그리고 컨트롤러 호출 직전에 호출된다. 만약 서블릿에서 예외가 발생한다면 인터셉터는 호출되지 않는다.
// 로그인 사용자
- HTTP 요청 → WAS → 필터 → 서블릿 → 컨트롤러
- HTTP 요청 → WAS → 필터 → 서블릿 → 인터셉터 → 컨트롤러
// 비 로그인 사용자 (부적절한 요청으로 판단)
- HTTP 요청 → WAS → [필터] (서블릿 호출 X)
- HTTP 요청 → WAS → 필터 → 서블릿 → [인터셉터] (컨트롤러 호출 X)
필터와 인터셉터는 요청이 부적절한 경우 자신의 상태에서 종료할 수 있고, 이때 스프링 인터셉터는 필터와 달리 서블릿까지 통과한 후에 제한된다.
- HTTP 요청 → WAS → [필터1] → [필터2] → [필터3] → 서블릿 → 컨트롤러
- HTTP 요청 → WAS → 필터 → 서블릿 → [인터셉터1] → [인터셉터2] → 컨트롤러
필터와 인터셉터 둘 다 그 갯수를 자유롭게 추가할 수 있고, 로그를 남기는 필터나 인터셉터를 적용 후 로그인 여부를 확인하는 필터나 인터셉터를 적용할 수 있다.
... 그럼 이 둘이 호출되는 순서의 차이 말고는 무엇이 어떻게 다른걸까? 코드를 통해 살펴보자.
필터는 디스패처 서블릿에 요청이 전달되기 전/후에 URL 패턴에 맞는 모든 요청에 대해 부가 작업을 처리할 수 있는 기능을 제공한다. 즉, 스프링 컨테이너가 아닌 톰캣과 같은 웹 컨테이너에 의해 관리가 되고, 스프링 범위 밖에서 처리된다.
public interface Filter {
public default void init(FilterConfig filterConfig) throws ServletException {}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
public default void destroy() {}
}
javax.servlet에 등록된 Filter(필터) 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리한다.
init() : 필터 초기화 메서드로 서블릿 컨테이너가 생성될 때 호출된다.doFilter() : 고객의 요청이 올 때마다 해당 메서드가 호출되고, 필터의 로직을 구현한다. 파라미터로 FilterChain이 있는데, chain.doFilter()를 통해 다음 대상으로 요청을 전달할 수 있게 된다.destroy() : 필터 종료 메서드로 서블릿 컨테이너가 종료될 때 호출된다.필터 인터페이스를 통해 모든 요청에 대한 로그를 남기는 필터는 다음과 같이 구현한다.
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("log filter doFilter");
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
String uuid = UUID.randomUUID().toString();
try{
log.info("REQUEST [{}][{}]", uuid, requestURI);
chain.doFilter(request, response);
}catch(Exception e){
throw e;
}finally {
log.info("RESPONSE [{}][{}]", uuid, requestURI);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
UUID.randomUUID().toString() : HTTP 요청을 구분하기 위해 요청당 임의의 uuid를 만든다.chain.doFilter(request, response) : 다음 필터가 있으면 다음 필터를 호출하고 필터가 없으면 서블릿을 호출한다. 만약 이 로직을 호출하지 않으면 다음 단계로 진행되지 않는다.필터를 구현하였으면 이를 Configuration에 등록하여야 한다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
setFilter(new LogFilter()) : 등록할 필터를 지정한다.setOrder(1) : 필터는 체인으로 동작하기에 순서가 필요하다. 순서가 낮을수록 먼저 동작한다.addUrlPatterns("/*") : 필터를 적용할 URL 패턴을 지정하며, 하나 이상의 패턴을 지정할 수도 있다.요청에 대한 로그를 남기는 필터를 구현하였으면, 로그인 인증 체크 필터는 다음과 같이 구현한다.
@Slf4j
public class LoginCheckFilter implements Filter {
private static final String[] whitelist = {"/", "/members/add", "/login", "/logout", "/css/*"};
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
log.info("인증 체크 필터 시작{}", requestURI);
if (isLoginCheckPath(requestURI)) {
log.info("인증 체크 로직 실행 {}", requestURI);
HttpSession session = httpRequest.getSession(false);
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("미인증 사용자 요청 {}", requestURI);
//로그인으로 리다이렉트
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
return;
}
}
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("인증 체크 필터 종료 {}", requestURI);
}
}
/**
* whiteList의 경우 인증 체크를 안하도록 한다.
*/
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}
}
whitelist = {"/", "/members/add", "/login", "/logout","/css/*"}; : 인증 필터를 적용해도 홈, 회원가입, 로그인 화면, css 같은 정적 리소스에 접근하거나 로그인, 로그아웃할 때에는 로그인을 하지 않아도 접근할 수 있어야 한다. 화이트리스트를 제외한 나머지 모든 경로에는 인증 체크 로직을 적용한다.isLoginCheckPath(requestURI) : 매개변수로 전달받은 requestURI가 화이트리스트와 일치하는지 검사한다. 이때 PatternMatchUtils라는 정적 헬퍼 클래스를 이용하여 쉽게 경로 검사가 가능하다.httpResponse.sendRedirect("/login?redirectURL=" + requestURI); : 미인증 사용자는 로그인 화면으로 리다이렉트 한다. 그런데 로그인 이후에 다시 홈으로 이동해버리면, 원하는 경로를 다시 찾아가야 하는 불편함이 있다. 그러므로 로그인한 사용자가 원래 접근하려는 페이지로 다시 이동시켜주면 사용자 입장에선 편리할 것이다.return; : 필터를 더는 진행하지 않는다. redirect를 사용했기에 redirect가 응답으로 적용되고 요청이 끝난다. 작성한 로그인 체크 필터를 Configuration에 등록한다.
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
/*으로 URL을 전부 검사를 허용하지만, 필터 내부에 화이트리스트가 있기 때문에 검사가 불필요한 경로는 검사를 하지 않는다.등록한 필터를 컨트롤러에서 @Valid 검증 애노테이션을 사용해 동작여부를 확인할 수 있다.
@PostMapping("login")
public String login(@Valid @ModelAttribute LoginForm form,
BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletResponse response,
HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//세션 매니저를 통해 세션 생성및 회원정보 보관
//세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
HttpSession session = request.getSession();
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
if (redirectURL != null) {
return "redirect:" + redirectURL;
}
return "redirect:/";
}
디스패처 서블릿이 컨트롤러를 호출하기 전/후에 인터셉터가 끼어들어 요청과 응답을 참조하거나 가공할 수 있는 기능을 제공한다. 웹 컨테이너에서 동작하는 필터와 달리 인터셉터는 스프링 컨텍스트에서 동작한다.
디스패처 서블릿이 핸들러 매핑을 통해 컨트롤러를 찾도록 요청하는데, 그 결과로 실행 체인(HandlerExecutionChain)을 돌려준다. 즉, 인터셉터가 없다면 바로 컨트롤러를 실행하도록 하며, 실제로 인터셉터가 직접 컨트롤러로 요청을 위임하는 것은 아니다.
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
preHandler : 컨트롤러 호출 전에 호출되며 반환 타입은 Boolean으로, 반환 값이 false이면 그 뒤는 진행하지 않는다. 컨트롤러 이전에 처리해야 하는 전처리 작업이나 요청 정보를 가공 및 추가하는 경우에 사용한다.postHandler : 컨트롤러 호출 후(핸들러 어댑터 호출 후) 호출되며, 컨트롤러에서 예외가 발생하면 호출되지 않는다. 서블릿 필터의 경우 단순히 request, response만 제공했지만, 인터셉터는 어떤 컨트롤러(handler)가 호출되는지 호출 정보도 받을 수 있다. 그리고 어떤 modelAndView가 반환되는지 응답 정보도 받을 수 있다. 컨트롤러 이후에 처리해야 하는 후처리 작업이 있을 때 사용하고, 최근에는 JSON 형태로 데이터를 제공하는 RestAPI 기반의 컨트롤러(@RestController)를 만들면서 자주 사용되지는 않는다.afterCompletion : 예외 발생과 상관없이 뷰가 렌더링 된 후에 항상 호출된다. 예외가 발생하면 afterCompletion()에 예외 정보(ex)를 포함해서 호출된다.여기서 스프링 MVC 구조에 특화된 인터셉터를 통해 예외 정보 확인이 가능하다는 점에서 스프링 MVC를 사용하고 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 것이 보다 편리함을 느낄 수 있다.
이제 HandlerInterceptor를 통해 로그를 남기는 스프링 인터셉터를 구현한다.
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
public static final String LOG_ID = "logId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String uuid = UUID.randomUUID().toString();
request.setAttribute(LOG_ID, uuid);
//@RequestMapping: HandlerMethod가 넘어온다.
//정적 리소스: ResourcehttpRequesthandler가 넘어온다.
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
}
log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("pohstHandler [{}]", modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String requestURI = request.getRequestURI();
String uuid = (String) request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}][{}]", uuid, requestURI, handler);
if (ex != null) {
log.error("afterCompletion error:", ex);
}
}
}
reqeust.setAttribute(LOG_ID, uuid) : 서블릿 필터와는 다르게 스프링 인터셉터는 호출 시점이 분리되어 있기에 각각의 메서드가 호출되는 시점에 변수들의 값 유지가 어렵다. 그래서 preHandler에서 지정한 값을 postHandler, afterCompletion에서 사용하려면 어딘가에 담아둬야 하는데, 이 인터셉터는 싱글톤처럼 사용되기에 멤버변수를 사용하면 안되기 때문에 request 인스턴스에 담아뒀다.이제 생성한 스프링 인터셉터를 Configuration에 등록해준다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
}
@Bean
public LoginInterceptor loginInterceptor() {
return new LoginInterceptor();
}
}
WebMvcConfigurer 인터페이스를 구현하여 addInterceptor 메서드를 재정의해서 인터셉터 등록이 가능하다. addInterceptor : 인터셉터를 등록한다. addPathPatterns("/**") : 인터셉터를 적용할 URL 패턴을 지정한다.excludePathPatterns("/css/**", "/*.ico", "/error") : 인터셉터에서 제외할 패턴을 지정한다.필터는 request와 response를 조작할 수 있지만, 인터셉터는 조작할 수 없다. 필터가 다음 필터를 호출하기 위해서는 필터 체이닝(다음 필터 호출)을 해주어야 한다. 이때 request, response 객체를 넘겨주므로 우리가 원하는 request, response 객체를 넣어줄 수 있다.
하지만 인터셉터는 처리 과정이 필터와 다르다. 디스패처 서블릿이 여러 인터셉터 목록을 가지고 있고, 순차적으로 실행시킨다.
그리고 true를 반환하면 다음 인터셉터가 실행되거나 컨트롤러로 요청이 전달되며, false가 반환되면 요청이 중단된다.
그러므로 다른 request, response 객체를 넘겨줄 수 없다.
- 보안 및 인증/인가 관련 작업
- 모든 요청에 대한 로깅 또는 검사
- 이미지/데이터 압축 및 문자열 인코딩
- Spring과 분리되어야 하는 기능
- 필터는 기본적으로 스프링과 무관하게 전역적으로 처리해야 하는 작업들을 처리할 수 있다.
필터는 인터셉터보다 앞단에서 동작하기 때문에 보안 검사(XSS 방어 등)를 하여 올바른 요청이 아닐 경우 차단할 수 있다. 그러면 스프링 컨테이너까지 요청이 전달되지 못하고 차단되므로 안전성을 더욱 높일 수 있다. 또한 필터는 이미지나 데이터의 압축, 문자열 인코딩과 같이 웹 어플리케이션에 전반적으로 사용되는 기능을 구현하기에 적당하다.
- 세부적인 보안 및 인증/인가 공통 작업
- API 호출에 대한 로깅 또는 검사
- 컨트롤러로 넘겨주는 정보(데이터)의 가공
- 인터셉터에서는 클라이언트의 요청과 관련되어 전역적으로 처리해야 하는 작업들을 처리할 수 있다.
대표적으로 세부적으로 적용해야 하는 인증이나 인가와 같이 예를 들어 특정 그룹의 사용자는 어떤 기능을 사용하지 못하는 경우가 있는데, 이러한 작업들은 컨트롤러로 넘어가기 전에 검사해야 하므로 인터셉터가 처리하기에 적합하다. 또한 인터셉터는 필터와 다르게 HttpServletRequest나 HttpServletResponse 등과 같은 객체를 제공받으므로 객체 자체를 조작할 수는 없다. 대신 해당 객체가 내부적으로 갖는 값은 조작할 수 있으므로 컨트롤러로 넘겨주기 위한 정보를 가공하기에 용이하다.

필터와 인터셉터 모두 비즈니스 로직과 분리되어 특정 요구사항(보안, 인증, 인코딩 등)을 만족시켜야 할 때 적용한다.
필터(Filter)는 특정 요청과 컨트롤러에 관계없이 전역적으로 처리해야 하는 작업이나 웹 어플리케이션에 전반적으로 사용되는 기능을 구현할 때, 인터셉터(Interceptor)는 클라이언트의 요청과 관련된 작업에 대해 추가적인 요구사항을 만족해야 할 때 적용이 적합하다.
참조 링크 :
1. 7. 로그인 처리2 - 필터, 인터셉터
2. [Spring] 필터(Filter)와 인터셉터(Interceptor)의 개념 및 차이