권한이 있는 사용자만 특정 URI에 접속할 수 있어야 하는 경우가 발생할 수 있다.
예를 들어 로그인된 사용자만 게시판에 글을 작성할 수 있다면, 로그인되지 않은 사용자는 게시판 작성 URI에 접속해서는 안된다.
즉, URI마다 특정 조건을 걸어서 권한이 없는 사용자를 필터링할 수 있어야 하는데 이는 서블릿 필터 or 스프링 인터셉터를 활용하면 해결할 수 있다.
공통 관심사로 분류할 수 있기 때문에 AOP를 활용해도 되지만 웹과 관련된 공통 관심사는 HTTP 헤더나 URI 정보들이 필요하기 때문에 HttpServletRequest를 제공하는 이들을 사용하는 것이 좋다.
서블릿 필터
서블릿 필터는 서블릿에서 지원하는 기능으로 서블릿 이전에 필터가 적용된다. 따라서 필터에서 적절하지 않은 요청이라 판단되면 서블릿을 호출하지 않는다.
- HTTP 요청 → WAS → 필터 → 서블릿 → 컨트롤러
또한, 여러 가지 조건의 필터를 체인 형태로 구성하여 연속적으로 처리할 수 있다.
- HTTP 요청 → WAS → 필터 A → 필터 B → 필터 C → 서블릿 → 컨트롤러
서블릿 필터는 인터페이스로 구현되어 있으며 3가지의 메서드를 가진다.
- init() : 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출
- doFilter() : 필터 로직을 수행하는 메서드, 실제로 고객의 요청이 발생하면 구현된 로직을 수행하면서 필터링
- destroy() : 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출
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() {}
}
필터를 추가하고 싶으면 해당 인터페이스를 구현해서 등록하면, 서블릿 컨테이너가 필터를 싱글톤 객체로 생성 및 관리해준다.
또한, 서블릿 필터는 예외가 발생하면 Web Application에서 처리해야 한다.
그렇다면 로그인 유무를 판단해주는 필터를 직접 구현해보자. Filter 인터페이스를 구현하는 LoginFilter 클래스를 구현해준다.
public class LoginFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request; //HTTP를 사용하기 때문에 다운캐스팅
HttpSession session = httpRequest.getSession(false);
if (로그인되지 않은 사용자라면) return; //다음 필터나 서블릿을 호출 X
chain.doFilter(request, response); //다음 필터나 서블릿을 호출 O
}
@Override
public void destroy() {}
}
다음으로 구현한 LoginFilter 클래스를 서블릿 컨테이너에 등록해주자. 스프링 부트에서는 FilterRegistrationBean을 사용하면 서블릿 컨테이너에 등록할 수 있다.
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginFilter()); //LoginFilter 등록
filterRegistrationBean.setOrder(1); //필터 우선순위(필터 체인), 낮을수록 우선순위 높음
filterRegistrationBean.addUrlPatterns("/*"); //필터 적용 URI 패턴, 다수 패턴 지정 가능
return filterRegistrationBean;
}
}
다만, 서블릿 필터는 필터를 적용하고 싶은 URI는 addUrlPatterns() 메서드를 통해 적용할 수 있지만 필터를 적용하고 싶지 않은 URI이 있다면 doFilter() 메서드에서 추가적인 로직을 구현하여 걸러줘야 한다는 번거로움이 존재한다.
서블릿 필터는 chain.doFilter(request, reponse) 메서드를 호출할 때 request와 response를 다른 객체로 바꿀 수 있다. 이는 스프링 인터셉터에서는 지원하지 않는 기능이다.
스프링 인터셉터
스프링 인터셉터는 서블릿 필터보다 편리하고, 다양한 기능을 지원해주는 기능이라고 생각하면 쉽다.
스프링 인터셉터는 스프링 MVC가 지원하는 기능으로 서블릿 필터는 서블릿 이전에 필터가 적용되었더라면 스프링 인터셉터는 컨트롤러 이전에 적용된다.
- HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러
즉, 둘의 차이 중 하나는 필터링이 적용되는 시점이다.
또한, 스프링 인터셉터 역시 마찬가지로 여러 가지 조건의 인터셉터를 체인 형태로 구성하여 연속적으로 처리할 수 있다.
- HTTP 요청 → WAS → 필터 → 서블릿 → 인터셉터 A → 인터셉터 B → 인터셉터 C → 컨트롤러
스프링 인터셉터도 인터페이스로 구현되어 있으며 3가지의 메서드를 가진다.
- preHandle() : 핸들러 어댑터 호출 전에 호출되는 메서드
- postHandle() : 핸들러 어댑터 호출 후에 호출되는 메서드
- afterCompletion() : 뷰가 렌더링된 이후에 호출되는 메서드
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 {
}
}
실제로 요청이 발생하면 각 메서드의 호출 흐름은 다음과 같다.
인터셉터를 추가하고 싶으면 해당 인터페이스를 구현해서 등록하면, 서블릿 컨테이너가 인터셉터를 싱글톤 객체로 생성 및 관리해준다.
서블릿 필터는 단순히 doFilter() 메서드에서 의미 있는 로직을 수행하였지만, 스프링 인터셉터는 3단계로 조금 더 세분화하여 로직을 수행할 수 있다.
또한, 서블릿 필터는 request, response만 제공하였지만, 스프링 인터셉터는 request, response뿐만 아니라 어떤 컨트롤러(Handler)가 호출되는지, 어떤 ModelAndView가 반환되는지에 대한 정보를 제공해준다.
스프링 인터셉터에서 예외가 발생하면 postHandle()은 호출되지 않지만, afterCompletion()은 항상 호출된다. 따라서 예외 여부에 상관없이 반드시 수행해야 하는 로직이 있다면 afterCompletion()에 구현해주면 된다.
또한, 스프링 인터셉터는 서블릿 내에서 동작하기 때문에 예외가 발생하면 WAS까지 올려 보내서 처리할 필요 없이 @ControllerAdvice에서 @ExceptionHandler를 통해 예외를 처리를 할 수 있는 장점이 있다.
그렇다면 로그인 유무를 판단해주는 스프링 인터셉터를 직접 구현해보자. HandlerInterceptor 인터페이스를 구현하는 LoginInterceptor 클래스를 구현해준다.
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (로그인되지 않은 사용자라면) return false;
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//do Something
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//do Something
}
}
다음으로 구현한 LoginInterceptor 클래스를 서블릿 컨테이너에 등록해주자. 스프링 인터셉터는 스프링 MVC에서 지원하는 기능이기 때문에 WebMvcConfigurer의 addInterceptors() 메서드를 통해 등록해준다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()) //LoginInterceptor 등록
.order(1) //인터셉터 우선순위(인터셉터 체인), 낮을수록 우선순위 높음
.addPathPatterns("/**") //인터셉터 적용 URI 패턴
.excludePathPatterns("/css/**", "/*.ico", "/error"); //인터셉터 적용 제외할 URI 패턴
}
}
스프링 인터셉터는 서블릿 필터와 달리 excludePathPatterns() 메서드를 통해 제외할 URI를 쉽게 지정해줄 수 있다.
스프링 인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공하는 친구라고 생각하면 된다.
서블릿 필터는 request, response 객체를 변경할 수 있는 기능이 있지만 실제로 잘 사용하지 않기 때문에 웬만하면 인터셉터를 사용하는 것이 낫다.
[ Reference ]
· https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
'IT 개인 공부 > Spring' 카테고리의 다른 글
[Spring] AOP가 적용되지 않은 메서드에서 AOP가 적용된 메서드를 호출하면 AOP가 정상적으로 동작할까? (0) | 2021.09.11 |
---|---|
[Spring] AOP란? (0) | 2021.09.09 |
[Spring] HTTP 메시지 컨버터 (0) | 2021.09.03 |
[Spring] 빈 스코프(Scope) 종류 (0) | 2021.08.23 |
[Spring] 싱글톤 컨테이너 : CGLIB (0) | 2021.08.22 |
댓글