Spring에서는 공통적으로 여러 작업을 처리하고 중복된 코드를 제거 및 재가공하는 많은 기능을 가지고 있다.
그 중 필터와 인터셉터 기능이 있는데 이 둘의 공통점으로 Controller에 도달하기 전 처리되는 기능이지만, 역할이 다르므로 이 둘의 정의와 차이를 알아보고자 한다.
Nest에서도 이와 유사하게 필터도 있고, 인터셉터도 있고 미들웨어도 존재하지만 Nest에서 동작되는 원리는 다음에 알아보려 한다.
필터는 말 그대로 요청과 응답을 거른 뒤에 정제하는 역할을 한다. J2EE 표준 스펙 기능으로 디스패처 서블릿(Dispatcher Servlet)에 요청이 전달되기 전/후에 url 패턴에 맞는 모든 요청에 대해 부가 작업을 처리할 수 있는 기능을 제공한다. 즉, 스프링 컨테이너가 아닌 톰캣과 같은 웹 컨테이너에 의해 관리가 되는 것이고, 스프링 범위 밖에서 처리되는 것이다.
디스패처 서블릿은 스프링의 가장 앞단에 존재하는 웹에서 이뤄지는 컨트롤러이므로, 필터는 스프링 범위 밖에서 처리가 되는 것이다.
이전 게시글에 Dispatcher Servlet에 대해 이야기를 했었는데 먼저 Dispatcher Servlet에 대해 간단히 알고 보는 것이 좋다. 여기 링크요!
즉, 스프링 컨테이너가 아닌 톰캣과 같은 웹 컨테이너(서블릿 컨테이너)에 의해 관리가 되는 것이다.
필터를 사용하기 위해서는 javax.servlet의 Filter 인터페이스를 구현(implements) 해야 하며, 다음과 같은 메소드를 가진다.
public interface Filter {
/* 필터 객체를 초기화하고 서비스에 추가하기 위한 메소드
*/
public default void init(FilterConfig filterConfig) throws ServletException {}
/* HTTP 요청이 디스패처 서블릿으로 전달되기 전에 웹 컨테이너에 의해 실행되는 메소드
*/
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
/* 필터 객체를 제거하고 사용하는 자원을 반환하기 위한 메소드
*/
public default void destroy() {}
}
init은 웹 컨테이너가 init()을 호출하여 필터 객체를 초기화하고 서비스에 추가하기 위한 메소드이다. 초기화가 이뤄지면 doFilter를 통해 처리가 이뤄진다.
url-pattern에 맞는 모든 HTTP 요청이 디스패처 서블릿으로 전달되기 전에 웹 컨테이너에 의해 실행되는 메소드이다.
doFilter의 파라미터로 FilterChain이 있는데, FilterChain의 doFilter를 통해 다음 대상으로 요청을 전달할 수 있게 된다. chain.doFilter()로 전/후에 우리가 필요한 처리 과정을 넣어줌으로써 원하는 처리를 진행할 수 있다.
필터의 중요 역할인 보안 작업, 이미지/동영상과 같은 인코딩 등 Spring 컨텍스트와 분리되는 웹 컨테이너에서 이뤄지는 역할로 이뤄진다.
처리된 필터 객체를 웹 컨테이너가 1회 destroy()를 호출하여 필터 객체를 종료되는 것을 확인하여 후처리로 doFilter에 의해 처리되지 않는다.
인터셉터는 말 그대로 가로채는 녀석이다.
그렇다면 뭘 가로챌까? 프론트엔드에서 받는 요청을 가로채는 것이 아닌 Dispatcher Servlet이 Controller를 호출하기 전/후에 인터셉터가 가로채는 요청과 응답을 참조하거나 가공할 수 있는 기능을 제공한다.
이번엔 필터와 달리 웹 컨테이너가 아닌 Spring 내부 컨텍스트에서 처리가 되고, Spring MVC에서 제공되는 기능이다.
디스패처 서블릿이 핸들러 매핑을 통해 컨트롤러를 찾도록 요청하는데, 그 결과로 실행 체인(HandlerExecutionChain)을 돌려준다.
여기서 1개 이상의 인터셉터가 등록되어 있다면 순차적으로 인터셉터들을 거쳐 컨트롤러가 실행되도록 하고, 인터셉터가 없다면 바로 컨트롤러를 실행한다.
실제로 Interceptor가 직접 Controller로 요청을 위임하는 것은 아니다
인터셉터를 추가하기 위해서는 org.springframework.web.servlet의 HandlerInterceptor 인터페이스를 구현(implements)해야 하며, 이는 다음의 3가지 메소드를 가지고 있다.
public interface HandlerInterceptor {
/* Controller가 호출되기 전에 실행
*/
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
/* Controller가 호출된 후에 실행
*/
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 {
}
}
preHandle 메소드는 컨트롤러 이전에 처리해야 하는 전처리 작업이나 요청 정보를 가공하거나 추가하는 경우에 사용할 수 있다.
preHandle의 3번째 파라미터로 Object 타입의 handler 파라미터는 핸들러 매핑이 찾아준 컨트롤러 빈에 매핑되는 HandlerMethod라는 새로운 타입의 객체로써, @RequestMapping이 붙은 메소드의 정보를 추상화한 객체이다. 현재 실행되는 컨트롤러를 파악하거니, 추가적인 메소드를 실행하는 등의 작업이 가능합니다.
또한 preHandle의 반환 타입은 boolean인데 반환값이 true이면 다음 단계로 진행이 되지만, false라면 작업을 중단하여 이후의 작업(다음 인터셉터 또는 컨트롤러)은 진행되지 않는다.
나는 AccessToken이 없거나 인증 객체가 없을 시 401 에러로 응답하도록 설계를 한 경험이 있다.
그러나 서비스에 따라 다르겠지만 Token을 디코드 했을 시, 권한 데이터를 확인해 권한 체크도 아마 들어가는 게 낫지 않을까 싶다.
✍🏻 만약 현재 실행되는 컨트롤러와 메소드의 정보를 파악하고 싶다면?
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
log.info("Bean: {}", handlerMethod.getBean());
log.info("Method: {}", method);
return true;
}
위와 같이 handler를 HandlerMethod 타입으로 캐스팅한 후 원래의 메소드와 객체(빈)를 확인할 수 있습니다.
코드를 실행하면 순서대로 현재 실행되는 Controller와 메소드가 출력됩니다.
postHandle 메소드는 컨트롤러 이후에 처리해야 하는 후처리 작업이 있을 때 사용할 수 있다. 이 메소드는 컨트롤러가 반환하는 ModelAndView 타입의 정보가 제공되는데, 최근에는 JSON 형태로 데이터를 제공하는 RestAPI 기반의 컨트롤러(@RestController)를 만들면서 자주 사용되지 않는다.
나도 아직 안사용해봄... 그러나 응답 전 View인지, 데이터인지 체크하여 그거에 따른 후 처리가 필요하면 그거에 따른 처리도 해보고 싶다.
또한 컨트롤러 하위 계층에서 작업을 진행하다가 중간에 예외가 발생하면 postHandle은 호출되지 않는다.
afterCompletion 메소드는 이름 그대로 모든 뷰에서 최종 결과를 생성하는 일을 포함해 모든 작업이 완료된 후에 실행된다. 요청 처리 중에 사용한 리소스를 반환할 때 사용하기에 적합하다. postHandler과 달리 컨트롤러 하위 계층에서 작업을 진행하다가 중간에 예외가 발생하더라도 afterCompletion은 반드시 호출된다.
필터는 Request와 Response를 조작할 수 있지만, 인터셉터는 조작할 수 없다.
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 다른 request와 response를 넣어줄 수 있음
chain.doFilter(request, response);
}
}
필터가 다음 필터를 호출하기 위해서는 필터 체이닝(다음 필터 호출)을 해줘야 한다. 이때 request, response 객체를 넘겨주므로 우리가 원하는 request, response 객체를 넣어줄 수 있다.
하지만 인터셉터는 처리 과정이 필터와 다르다.
public class MyInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception { // Request, Response를 교체할 수 없고 boolean 값만 반환 가능
return true;
}
}
디스패처 서블릿이 여러 인터셉터 목록을 가지고 있고, 순차적으로 실행시킨다.
그리고 true를 반환하면 다음 인터셉터가 실행되거나 컨트롤러로 요청이 전달되며, false가 반환되면 요청이 중단된다. 그러므로 다른 request, response 객체를 넘겨줄 수 없다.
- 보안 및 인증/인가 관련 작업
- 모든 요청에 대한 로깅 또는 검사
- 이미지/데이터 압축 및 문자열 인코딩
- Spring과 분리되어야 하는 기능
필터는 기본적으로 Spring과 무관하게 전역적으로 처리해야 하는 작업들을 처리할 수 있다. 필터는 인터셉터보다 앞단에서 동작하기 때문에 보안 검사(XSS 방어 등)를 하여 올바른 요청이 아닐 경우 차단할 수 있다.
그러면 Spring 컨테이너까지 요청이 전달되지 못하고 차단되므로 안전성을 더욱 높일 수 있다. 또한, 필터는 이미지나 데이터의 압축, 문자열 인코딩과 같이 웹 어플리케이션에 전반적으로 사용되는 기능을 구현하기에 적당하다.
- 세부적인 보안 및 인증/인가 공통 작업
- API 호출에 대한 로깅 또는 검사
- Controller로 넘겨주는 정보(데이터)의 가공
인터셉터에서는 클라이언트의 요청과 관련되어 전역적으로 처리해야 하는 작업들을 처리할 수 있다.
대표적으로 세부적으로 적용해야 하는 인증이나 인가와 같이 예를 들어 특정 그룹의 사용자는 어떤 기능을 사용하지 못하는 경우가 있는데, 이러한 작업들은 컨트롤러로 넘어가기 전에 검사해야 하므로 인터셉터가 처리하기에 적합하다.
또한 인터셉터는 필터와 다르게 HttpServletRequest나 HttpServletResponse 등과 같은 객체를 제공받으므로 객체 자체를 조작할 수는 없다. 대신 해당 객체가 내부적으로 갖는 값은 조작할 수 있으므로 컨트롤러로 넘겨주기 위한 정보를 가공하기에 용이하다.
예를 들어 AccessToken 토큰 정보를 파싱하거나 컨트롤러에서 다루기 쉽게 사용자의 정보를 가공할 수 있는 것이다. 그 외에도 여러 목적으로 API 호출에 대한 정보들을 로깅해야 하는 상황에 HttpServletRequest나 HttpServletResponse를 제공해주는 인터셉터는 클라이언트의 IP나 요청 정보들을 기록하기에 용이하다.
두고두고 잘 기억하면 사용자 행동 패턴이나 요청, 응답 데이터를 DataDog이나 RedShift 등 기록하여 서비스 고도화에 많은 인사이트를 얻을 수 있을 거 같다.
필터와 인터셉터 모두 비즈니스 로직과 분리되어 특정 요구사항(보안, 인증, 인코딩 등)을 만족시켜야 할 때 적용한다.
대표적으로 필터(Filter)를 인증과 인가에 사용하는 도구로는 SpringSecurity가 있다.
SpringSecurity의 특징 중 하나는 Spring MVC에 종속적이지 않다는 것인데, 이러한 이유로는 필터 기반으로 인증/인가 처리를 하기 때문이다.
필터(Filter) 는 특정 요청과 컨트롤러에 관계없이 전역적으로 처리해야 하는 작업이나 웹 어플리케이션에 전반적으로 사용되는 기능을 구현할 때 적용하고,
인터셉터(Interceptor) 는 클라이언트의 요청과 관련된 작업에 대해 추가적인 요구사항을 만족해야 할 때 적용한다.