개요
기존 게시글에서는 쿠키와 세션 개념을 정리하면서 이 두 개념을 이용하여 로그인 관련 코드 적용도 해봤습니다.
(https://jaimemin.tistory.com/1885, https://jaimemin.tistory.com/1886)
하지만, 로그인 처리를 할 때 쿠키와 세션만으로는 해결 안 되는 점이 있습니다.
예를 들자면, 로그인을 해야지만 접근 가능한 페이지들이 있을 때 쿠키와 세션만 적용했을 때는 해당 페이지 url을 직접 쳐서 들어갈 경우 그대로 화면이 노출된다는 문제점이 있습니다.
사실 시스템 요구사항에서는 로그인하지 않은 상태에서 해당 페이지들에 접근할 경우 우선 로그인 페이지로 리다이렉트 한 후 다시 그 페이지로 리다이렉트 하는 방식을 원할 것입니다.
여기서 필터와 인터셉터가 쓰이는데 필터와 인터셉터는 웹과 관련된 공통 관심사를 처리할 때 사용됩니다. (AOP와 유사)
저를 포함해서 필터와 인터셉터의 차이를 혼동하는 사람들이 많다는 것을 실무 개발을 하면서 느꼈습니다.
따라서, 이번 게시글을 통해 필터와 인터셉터의 차이에 대해 확실히 정리해보겠습니다.
1. 필터
- 정확히 명칭은 서블릿 필터 (스프링에서 제공하는 기능이 아님)
- 필터를 적용할 경우 필터가 호출된 다음에 서블릿이 호출
- 따라서, 모든 고객의 요청 로그를 남기는 요구사항이 있을 경우 필터를 사용
- 기존 Spring MVC 구조를 떠올려보면 아래와 같은 구조를 그릴 수 있음 (https://jaimemin.tistory.com/1820)

구조를 보면 DispatcherServlet이 호출되기 전에 필터를 거치는 것을 확인할 수 있습니다.
1.1 필터 체인
- 필터는 체인으로 구성되며, 중간에 필터를 자유롭게 추가 및 제거할 수 있음
- 예를 들어 로그를 남기는 필터를 먼저 적용한 후 로그인 여부를 체크하는 필터를 이후에 추가할 수 있음
- ex) HTTP 요청 -> WAS -> 필터 1 -> 필터 2 ->... -> 필터 N -> 디스패처 서블릿 -> 컨트롤러
1.2 필터의 역할
- 필터는 적절하지 않은 요청이 들어올 경우 HTTP 요청이 서블릿까지 도달하지 못하게 하는 역할이 있음
- 따라서, Spring Security를 적용할 때 필터에 적용하기도 함
- 정상적인 요청: HTTP 요청 -> WAS -> 필터1 -> 필터 2 ->... -> 필터 N -> 디스패처 서블릿 -> 컨트롤러
- 잘 못된 요청: HTTP 요청 -> WAS -> 필터1 -> 필터 2 -> 서블릿 호출 X
1.3 필터 인터페이스 살펴보기
/** | |
* A filter is an object that performs filtering tasks on either the request to | |
* a resource (a servlet or static content), or on the response from a resource, | |
* or both. <br> | |
* <br> | |
* Filters perform filtering in the <code>doFilter</code> method. Every Filter | |
* has access to a FilterConfig object from which it can obtain its | |
* initialization parameters, a reference to the ServletContext which it can | |
* use, for example, to load resources needed for filtering tasks. | |
* <p> | |
* Filters are configured in the deployment descriptor of a web application | |
* <p> | |
* Examples that have been identified for this design are<br> | |
* 1) Authentication Filters <br> | |
* 2) Logging and Auditing Filters <br> | |
* 3) Image conversion Filters <br> | |
* 4) Data compression Filters <br> | |
* 5) Encryption Filters <br> | |
* 6) Tokenizing Filters <br> | |
* 7) Filters that trigger resource access events <br> | |
* 8) XSL/T filters <br> | |
* 9) Mime-type chain Filter <br> | |
* | |
* @since Servlet 2.3 | |
*/ | |
public interface Filter { | |
/** | |
* Called by the web container to indicate to a filter that it is being | |
* placed into service. The servlet container calls the init method exactly | |
* once after instantiating the filter. The init method must complete | |
* successfully before the filter is asked to do any filtering work. | |
* <p> | |
* The web container cannot place the filter into service if the init method | |
* either: | |
* <ul> | |
* <li>Throws a ServletException</li> | |
* <li>Does not return within a time period defined by the web | |
* container</li> | |
* </ul> | |
* The default implementation is a NO-OP. | |
* | |
* @param filterConfig The configuration information associated with the | |
* filter instance being initialised | |
* | |
* @throws ServletException if the initialisation fails | |
*/ | |
public default void init(FilterConfig filterConfig) throws ServletException {} | |
/** | |
* The <code>doFilter</code> method of the Filter is called by the container | |
* each time a request/response pair is passed through the chain due to a | |
* client request for a resource at the end of the chain. The FilterChain | |
* passed in to this method allows the Filter to pass on the request and | |
* response to the next entity in the chain. | |
* <p> | |
* A typical implementation of this method would follow the following | |
* pattern:- <br> | |
* 1. Examine the request<br> | |
* 2. Optionally wrap the request object with a custom implementation to | |
* filter content or headers for input filtering <br> | |
* 3. Optionally wrap the response object with a custom implementation to | |
* filter content or headers for output filtering <br> | |
* 4. a) <strong>Either</strong> invoke the next entity in the chain using | |
* the FilterChain object (<code>chain.doFilter()</code>), <br> | |
* 4. b) <strong>or</strong> not pass on the request/response pair to the | |
* next entity in the filter chain to block the request processing<br> | |
* 5. Directly set headers on the response after invocation of the next | |
* entity in the filter chain. | |
* | |
* @param request The request to process | |
* @param response The response associated with the request | |
* @param chain Provides access to the next filter in the chain for this | |
* filter to pass the request and response to for further | |
* processing | |
* | |
* @throws IOException if an I/O error occurs during this filter's | |
* processing of the request | |
* @throws ServletException if the processing fails for any other reason | |
*/ | |
public void doFilter(ServletRequest request, ServletResponse response, | |
FilterChain chain) throws IOException, ServletException; | |
/** | |
* Called by the web container to indicate to a filter that it is being | |
* taken out of service. This method is only called once all threads within | |
* the filter's doFilter method have exited or after a timeout period has | |
* passed. After the web container calls this method, it will not call the | |
* doFilter method again on this instance of the filter. <br> | |
* <br> | |
* | |
* This method gives the filter an opportunity to clean up any resources | |
* that are being held (for example, memory, file handles, threads) and make | |
* sure that any persistent state is synchronized with the filter's current | |
* state in memory. | |
* | |
* The default implementation is a NO-OP. | |
*/ | |
public default void destroy() {} | |
} |
필터 인터페이스
- 인터페이스에는 세 가지 메서드가 있고 init과 destory 메서드 같은 경우 default 키워드가 있기 때문에 필요가 없을 경우 별도로 정의하지 않아도 됨
- 개요에서도 언급했드시 웹 관련 공통사항을 처리하기 때문에 파라미터로 ServletRequest와 ServletResponse 객체가 제공되는 것을 확인할 수 있음
- 스프링 프레임워크는 확장성을 위해 ServletRequest와 ServletResponse 객체로 제공했지만 대부분 HttpServletRequest와 HttpServletResponse를 사용하기 때문에 다운 캐스팅하여 사용하면 됨
메서드 간단 설명
- init() 메서드: 필터 초기화 메서드이며 서블릿 컨테이너가 생성될 때 호출
- doFilter() 메서드: 고객의 요청이 올 때마다 해당 메서드가 호출되며 메인 로직을 이 메서드에 구현하면 됨
- destory() 메서드: 필터 종료 메서드이며 서블릿 컨테이너가 소멸될 때 호출
1.4 로그인 여부 확인하는 필터 예시
- 앞선 게시글에서는 각 컨트롤러마다 로그인 여부를 확인했는데 필터를 적용하면서 반복되는 공통 코드를 한번에 처리할 수 있음
- 로그인 확인 여부 체크하는 필터를 구현한 뒤 @Configuration에 필터를 등록해주면 적용 가능
- 아래 코드 참고
LoginCheckFilter.java
@Slf4j | |
public class LoginCheckFilter implements Filter { | |
private static final String SESSION_ID = "sessionId"; | |
private static final String[] whitelist = {"/", "/login", "/logout", "/css/*", "/*.ico", ...}; | |
@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(SESSION_ID) == null) { | |
log.info("미인증 사용자 요청 {}", requestURI); | |
// 로그인 페이지로 redirect | |
// 로그인 시 다시 해당 페이지로 redirect | |
httpResponse.sendRedirect("/login?redirectURL=" + requestURI); | |
return; | |
} | |
} | |
// 아래 코드를 추가하지 않을 경우 필터 체인 적용이 안되면서 다음 필터로 이어지지 않음 | |
chain.doFilter(request, response); | |
} catch (Exception e) { | |
// 예외 로깅 가능하지만, 톰캣까지 예외를 보내줘야함 | |
throw e; | |
} finally { | |
log.info("인증 체크 필터 종료 {}", requestURI); | |
} | |
} | |
/** | |
* 화이트 리스트의 경우 인증 체크 안함 | |
*/ | |
private boolean isLoginCheckPath(String requestURI) { | |
return !PatternMatchUtils.simpleMatch(whitelist, requestURI); | |
} | |
} |
WebConfig.java
@Configuration | |
public class WebConfig { | |
@Bean | |
public FilterRegistrationBean loginCheckFilter() { | |
FilterRegistrationBean<Filter> filterRegistration = new FilterRegistrationBean<>(); | |
filterRegistration.setFilter(new LoginCheckFilter()); | |
filterRegistration.setOrder(1); // 필터 체인 순서 | |
filterRegistration.addUrlPatterns("/*"); // 모든 요청에 대해 다 필터 적용 | |
return filterRegistration; | |
} | |
@Bean | |
public FilterRegistrationBean exampleFilter() { | |
FilterRegistrationBean<Filter> filterRegistration = new FilterRegistrationBean<>(); | |
filterRegistration.setFilter(new ExampleFilter()); | |
filterRegistration.setOrder(2); // 필터 체인 순서 | |
filterRegistration.addUrlPatterns("/*"); // 모든 요청에 대해 다 필터 적용 | |
return filterRegistration; | |
} | |
} |
코드 부연설명
- LoginCheckFilter 내 whitelist 같은 경우 로그인하지 않아도 접근할 수 있는 자원들 목록
- whitelist에 포함되지 않은 요청 URL들은 로그인이 필수인 상황이므로 만약 로그인이 되어있지 않은 상태에서 요청이 올 경우 필터가 해당 요청을 튕겨내고 login 페이지로 리다이렉트 시켜야 함
- 이때, 기존 요청을 쿼리 파라미터로 redirectURL로 지정함으로써 로그인한 이후에는 기존 요청 페이지로 리다이렉트 될 수 있도록 처리하는 것이 고객 입장에서 편리 (httpResponse.sendRedirect("/login?redirectURL=" + requestURI);)
- 더 이상 필터 체인을 타지 않기 위해 return; 문을 넣는 것이 중요
- WebConfig 내 필터를 등록하기 위해서는 각 필터를 빈으로 등록해줘야 함
- 이후 설명하겠지만 인터셉터의 경우 하나의 메서드 내 여러 인터셉터를 등록할 수 있고 필터의 경우 필터 개수만큼 메서드를 선언하여 빈으로 등록해줘야 함
- 모든 요청에 로그인 필터를 적용한 뒤 whitelist에 포함되었는지 여부는 로그인 필터 내부 로직에서 확인 (인터셉터의 경우 이 같은 처리를 편리하게 적용하기 위해 보다 편한 메서드 제공)
2. 인터셉터
- 필터는 서블릿이 제공하는 기술이지만 스프링 인터셉터는 스프링 MVC가 제공하는 기술
- 웹 관련 공통 관심 사항을 처리한다는 점에서는 필터와 비슷
- 인터셉터는 필터와 달리 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출됨
- 기존 Spring MVC 구조를 떠올려보면 아래와 같은 구조를 그릴 수 있음 (https://jaimemin.tistory.com/1820)

2.1 스프링 인터셉터 체인
- 스프링 인터셉터 또한 체인으로 구성되며 중간에 자유롭게 인터셉터를 추가 및 제거 가능
- 예를 들어 로그를 남기는 인터셉터를 먼저 적용한 후 로그인 여부를 체크하는 인터셉터를 이후에 추가할 수 있음
- ex) HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터 1 -> 인터셉터 2 ->... -> 인터셉터 N -> 컨트롤러
2.2 스프링 인터셉터의 역할
- 스프링 인터셉터 또한 필터처럼 적절하지 않은 요청이 들어올 경우 HTTP 요청이 서블릿까지 도달하지 못하게 하는 역할이 있음
- 따라서, Spring Security를 적용할 때 스프링 인터셉터에 적용하기도 함
- 단, 필터는 서블릿에 도달하기 전에 호출되고 스프링 인터셉터는 서블릿에서 컨트롤러를 호출하기 전에 호출됨
- 정상적인 요청: HTTP 요청 -> WAS -> 필터 -> 디스패처 서블릿 -> 인터셉터 1 -> 인터셉터 2 ->... -> 인터셉터 N -> 컨트롤러
- 잘 못된 요청: HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터 1 -> 인터셉터 2 -> 컨트롤러 호출 안됨
2.3 스프링 인터셉터 인터페이스 살펴보기
public interface HandlerInterceptor { | |
/** | |
* Intercept the execution of a handler. Called after HandlerMapping determined | |
* an appropriate handler object, but before HandlerAdapter invokes the handler. | |
* <p>DispatcherServlet processes a handler in an execution chain, consisting | |
* of any number of interceptors, with the handler itself at the end. | |
* With this method, each interceptor can decide to abort the execution chain, | |
* typically sending an HTTP error or writing a custom response. | |
* <p><strong>Note:</strong> special considerations apply for asynchronous | |
* request processing. For more details see | |
* {@link org.springframework.web.servlet.AsyncHandlerInterceptor}. | |
* <p>The default implementation returns {@code true}. | |
* @param request current HTTP request | |
* @param response current HTTP response | |
* @param handler chosen handler to execute, for type and/or instance evaluation | |
* @return {@code true} if the execution chain should proceed with the | |
* next interceptor or the handler itself. Else, DispatcherServlet assumes | |
* that this interceptor has already dealt with the response itself. | |
* @throws Exception in case of errors | |
*/ | |
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) | |
throws Exception { | |
return true; | |
} | |
/** | |
* Intercept the execution of a handler. Called after HandlerAdapter actually | |
* invoked the handler, but before the DispatcherServlet renders the view. | |
* Can expose additional model objects to the view via the given ModelAndView. | |
* <p>DispatcherServlet processes a handler in an execution chain, consisting | |
* of any number of interceptors, with the handler itself at the end. | |
* With this method, each interceptor can post-process an execution, | |
* getting applied in inverse order of the execution chain. | |
* <p><strong>Note:</strong> special considerations apply for asynchronous | |
* request processing. For more details see | |
* {@link org.springframework.web.servlet.AsyncHandlerInterceptor}. | |
* <p>The default implementation is empty. | |
* @param request current HTTP request | |
* @param response current HTTP response | |
* @param handler the handler (or {@link HandlerMethod}) that started asynchronous | |
* execution, for type and/or instance examination | |
* @param modelAndView the {@code ModelAndView} that the handler returned | |
* (can also be {@code null}) | |
* @throws Exception in case of errors | |
*/ | |
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, | |
@Nullable ModelAndView modelAndView) throws Exception { | |
} | |
/** | |
* Callback after completion of request processing, that is, after rendering | |
* the view. Will be called on any outcome of handler execution, thus allows | |
* for proper resource cleanup. | |
* <p>Note: Will only be called if this interceptor's {@code preHandle} | |
* method has successfully completed and returned {@code true}! | |
* <p>As with the {@code postHandle} method, the method will be invoked on each | |
* interceptor in the chain in reverse order, so the first interceptor will be | |
* the last to be invoked. | |
* <p><strong>Note:</strong> special considerations apply for asynchronous | |
* request processing. For more details see | |
* {@link org.springframework.web.servlet.AsyncHandlerInterceptor}. | |
* <p>The default implementation is empty. | |
* @param request current HTTP request | |
* @param response current HTTP response | |
* @param handler the handler (or {@link HandlerMethod}) that started asynchronous | |
* execution, for type and/or instance examination | |
* @param ex any exception thrown on handler execution, if any; this does not | |
* include exceptions that have been handled through an exception resolver | |
* @throws Exception in case of errors | |
*/ | |
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, | |
@Nullable Exception ex) throws Exception { | |
} | |
} |
스프링 인터셉터 인터페이스
- 인터페이스에는 세 가지 메서드가 있고 필터와 달리 세 가지 메서드 모두 default 키워드가 있음
- 서플릿 필터의 경우 단순하게 doFilter 메서드만 제공되었지만 스프링 인터셉터의 경우 컨트롤러 호출 전에 호출되는 preHandle 메서드, 컨트롤러 호출 후 호출되는 postHandle 메서드, 그리고 요청이 완료된 이후 호출되는 afterCompletion 메서드가 단계적으로 잘 세분화되어 필터보다 많은 기능을 제공
- 서블릿 필터의 경우 단순히 request, response만 제공했지만 스프링 인터셉터는 어떤 컨트롤러(handler)가 호출되었는지에 관한 호출 정보도 받을 수 있음
- 또한, 어떤 ModelAndView가 반환되었는지 응답 정보도 받을 수 있음
- 정리를 하자면 스프링 인터셉터는 필터와 비슷하게 작동하지만 호출 시점이 다르고 인터셉터가 필터보다 더 많은 기능을 제공
메서드 간단 설명
- preHandle() 메서드: 컨트롤러 호출 전, 더 정확히 말하자면 HandlerAdapter 호출 전에 호출
- preHandle의 반환 값이 true일 경우 다음 체인으로 진행이 되고, false일 경우 더 이상 진행이 되지 않으며 핸들러 어댑터 또한 호출이 되지 않음
- postHandle() 메서드: 컨트롤러 호출 후, 더 정확히 말하자면 HandlerAdapter 호출 후에 호출
- afterCompletion() 메서드: 뷰가 렌더링 된 이후에 호출되며 try catch finally 절에서 finally처럼 무조건 호출됨
2.4 스프링 인터셉터에서 예외 발생할 경우
- 컨트롤러에서 예외가 발생할 경우 postHandle 메서드는 호출되지 않음
- 앞서 언급했듯이 afterCompletion 메서드는 무조건 호출되므로 예외 발생 시 파라미터로 어떤 예외가 발생했는지 알 수 있으며 로그로 출력 가능
- 필터의 경우 예외를 로깅하기 위해서는 톰캣까지 예외를 보내줘야 하지만 인터셉터의 경우 바로 로깅 가능
2.5 로그인 여부 확인하는 스프링 인터셉터 예시
- 필터처럼 반복되는 공통 코드를 한 번에 처리할 수 있음
- 로그인 확인 여부 체크하는 필터를 구현한 뒤 @Configuration에 필터를 등록해주면 적용 가능
- 필터와 달리 WebConfig가 WebMvcConfigurer 인터페이스를 구현해야 함
- 아래 코드 참고
LoginCheckInterceptor.java
@Slf4j | |
public class LoginCheckInterceptor implements HandlerInterceptor { | |
private static final String SESSION_ID = "sessionId"; | |
@Override | |
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { | |
String requestURI = request.getRequestURI(); | |
log.info("인증 체크 인터셉터 실행 {}", requestURI); | |
HttpSession session = request.getSession(false); | |
if (session == null | |
|| session.getAttribute(SESSION_ID) == null) { | |
log.info("미인증 사용자 요청"); | |
// 로그인으로 redirect | |
response.sendRedirect("/login?redirectURL=" + requestURI); | |
return false; | |
} | |
return true; | |
} | |
} |
WebConfig.java
@Configuration | |
public class WebConfig implements WebMvcConfigurer { | |
@Override | |
public void addInterceptors(InterceptorRegistry registry) { | |
registry.addInterceptor(new LoginCheckInterceptor()) | |
.order(1) // 인터셉터 체인 순서 | |
.addPathPatterns("/**") // 모든 requestURL에 대해 적용 | |
.excludePathPatterns("/" // 제외하고 싶은 whitelist | |
, "/login" | |
, "/logout" | |
, "/css/**" | |
, "/*.ico" | |
, "/error"); | |
registry.addInterceptor(new ExampleInterceptor()) | |
.order(2) | |
.addPathPatterns("/**") | |
.excludePathPatterns("/css/**" | |
, "/*.ico" | |
, "/error"); | |
} | |
} |
코드 부연설명
- 로그인 확인 여부는 컨트롤러 호출 전에 이루어지므로 preHandle만 구현
- 필터와 마찬가지로 만약 로그인이 되어있지 않은 상태에서 요청이 올 경우 필터가 해당 요청을 튕겨내고 login 페이지로 리다이렉트 시켜야 함
- 이때, 기존 요청을 쿼리 파라미터로 redirectURL로 지정함으로써 로그인한 이후에는 기존 요청 페이지로 리다이렉트 될 수 있도록 처리하는 것이 고객 입장에서 편리 (httpResponse.sendRedirect("/login?redirectURL=" + requestURI);)
- 여기서는 whitelist를 인터셉터 내에서 명시하지 않았다는 것을 주목
- WebConfig 내 인터셉터를 등록하기 위해서는 인터셉터들을 addInterceptors 메서드에서 등록해주면 됨
- 필터와 달리 단 하나의 메서드에서 여러 개 인터셉터 등록 가능
- 필터와 달리 excludePathPatterns 메서드를 지원하여 whitelist를 WebConfig에서 등록 가능
최종 정리
- 필터는 서블릿에서 제공하며 디스패처 서블릿 호출되기 전에 호출됨
- 스프링 인터셉터는 Spring MVC에서 제공되며 컨트롤러, 정확히 말하면 HandlerAdapter 호출 직전에 호출됨
- 이처럼 필터와 인터셉터는 호출 시점이 다르지만 웹 공통 관심 사항을 처리한다는 점은 동일
- 하지만, 스프링 인터셉터가 필터보다 개발자 친화적으로 다양한 기능을 제공하므로 서블릿이 호출되기 직전에 처리해야 하는 로직이 아니라면 스프링 인터셉터를 활용하는 것을 추천
비고
- PathPattern 공식 문서는 아래 링크 참고
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html
PathPattern (Spring Framework 5.3.9 API)
Representation of a parsed path pattern. Includes a chain of path elements for fast matching and accumulates computed state for quick comparison of patterns. PathPattern matches URL paths using the following rules: ? matches one character * matches zero or
docs.spring.io
출처
인프런 스프링 MVC 2편 (김영한 강사님)
'Spring' 카테고리의 다른 글
[SpringBoot] API 예외처리 (1) | 2021.08.08 |
---|---|
[SpringBoot] HTML 예외처리와 예외 페이지 (0) | 2021.08.03 |
[SpringBoot] 쿠키, 세션을 이용한 로그인 처리 (2) | 2021.07.31 |
[SpringBoot] 쿠키, 세션 개념 정리 (0) | 2021.07.29 |
[SpringBoot] Validation 간단 정리 - 2 (Bean Validation) (0) | 2021.07.25 |