[Spring] 필터와 인터셉터
로그인해야 게시판에 접근할 수 있다는 요구 사항이 있을 때, 로그인 여부를 확인하는 로직은 여러 컨트롤러에서 공통적으로 필요하다.
위와 같이 애플리케이션의 여러 로직에서 공통으로 관심이 있는 사항을 공통 관심사라고 한다.
스프링은 공통 관심사를 처리하여 중복 코드를 제거할 수 있도록 다음과 같은 기능을 제공한다.
- 필터
- 인터셉터
- AOP
이번 시간에는 이 3가지 중에서 필터와 인터셉터에 대해서 알아보려고 한다.
필터와 인터셉터는 HttpServletRequest를 제공함으로써 웹과 관련된 공통 관심사를 처리할 때 유용하다.
서블릿 필터
필터는 서블릿이 지원하는 기능으로 Dispatcher Servlet에 요청이 전달되기 전 또는 처리한 후에 추가 작업을 수행할 수 있도록 한다. 즉 스프링 컨테이너가 아닌 서블릿 컨테이너에 의해 관리되고 필터 체인을 구성하여 여러 필터를 적용할 수 있다.
필터가 적용되면 요청 흐름은 다음과 같이 진행된다.
HTTP 요청 → WAS → 필터1 → 필터2 → 필터3 → 서블릿 → 컨트롤러
해당 필터에서 적절하지 않은 요청이라고 판단되면 해당 필터에서 요청을 중단시킬 수 있다.
필터를 추가하기 위해선 Filter 인터페이스를 구현해야 한다. 구현한 필터를 등록하면 서블릿 컨테이너에 의해 싱글톤으로 생성, 관리된다.
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {
}
void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
default void destroy() {
}
}
- init() : 필터를 초기화하기 위한 메서드로 서블릿 컨테이너가 생성될 때 1회 호출.
- doFilter() : 요청이 올 때 마다 해당 메서드가 호출되며 필터의 로직을 구현하면 된다.
- destroy() : 필터를 제거하기 위한 메서드로 서블릿 컨테이너가 종료될 때 1회 호출.
다음은 로그인 여부를 확인하는 공통 관심사를 처리하는 필터의 예이다.
/members인 url을 제외하고 나머지는 작성한 필터가 동작하도록 한다. 세션을 확인 후 인증된 사용자가 아니라면 401 코드를 반환 후 return을 통해 해당 필터에서 요청이 종료되도록 한다.
public class LoginCheckFilter implements Filter {
private static final String[] whitelist = {"/members"};
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String requestURI = request.getRequestURI();
HttpServletResponse response = (HttpServletResponse) servletResponse;
try {
if (isLoginCheckPath(requestURI)) {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
}
// servletRequest, servletResponse가 아닌 httpServletRequest, httpServletResponse 넣어주는 것이 가능
filterChain.doFilter(servletRequest, servletResponse);
} catch (Exception e) {
throw e;
}
}
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}
}
작성한 필터는 스프링 부트의 경우 FilterRegistrationBean에 등록하면 된다.
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean<Filter> loginFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
스프링 인터셉터
스프링 인터셉터 역시 웹에 관련된 요청, 응답에 추가적인 작업을 처리할 수 있다. 차이가 있다면 필터는 서블릿 컨테이너에서 동작하지만 인터셉터는 스프링 MVC가 제공하는 기술이다.
따라서 인터셉터를 사용하면 요청 흐름은 다음과 같다.
HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터1 → 스프링 인터셉터2 → 컨트롤러
인터셉터의 경우 컨트롤러가 호출되기 직전에 호출되며, 마찬가지로 체인으로 구성되고 서블릿에서 URL 패턴을 적용한 것과는 다르게 더 정밀한 설정이 가능하다.
스프링 인터셉터를 사용하려면 HandlerInterceptor 인터페이스 구현하고 등록한다.
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 {
}
}
- preHandle() : 핸들러 어댑터가 호출되기 전에 호출
- 응답이 true면 다음으로 진행, false면 더는 진행하지 않는다.(컨트롤러를 호출하지 않음)
- postHandle() : 핸들러 어댑터 호출 후에 호출
- afterCompletion() : 뷰가 렌더링 된 이후 호출
만약 컨트롤러에서 예외가 발생한다면?
preHandle() 호출 → 핸들러 어댑터에서 컨트롤러를 찾음 → 컨트롤러 하위에서 예외 발생 → postHandle()은 호출되지 않고 afterCompletion()는 항상 호출
즉 afterCompletion() 메서드는 예외와 상관없이 항상 호출되므로 요청 처리 중 사용한 리소스 반환 시에 적합하다.
위에서 작성한 로그인 여부를 확인하는 인터셉터를 구현해보도록 하겠다.
로그인 인증은 요청이 컨트롤러에 들어오기 전에 확인하면 되므로 preHandle() 메서드만 구현하였다.
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
return true;
}
}
작성한 인터셉터는 WebMvcConfigurer가 제공하는 addInterceptors() 메서드를 사용해서 등록 가능
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginCheckInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/members");
}
}
필터 | 인터셉터 | |
관리 주체 | 서블릿 컨테이너 | 스프링 컨테이너 |
Request, Response 객체 조작 여부 | 가능 | 불가능 |
스프링의 예외 처리 방식 (@RestControllerAdvice, @ExceptionHandler) 처리 여부 | 처리 X | 처리 O |
구현 방식 | Filter 구현 | HandlerInterceptor 구현 |
참고
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의 | 김영한 - 인프런
김영한 | 웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습
www.inflearn.com