Spring - (14) : 서블릿 필터 && 스프링 인터셉터

­이승환·2021년 12월 10일
0

spring

목록 보기
12/26

필터와 인터셉터


지금까지 포스팅을 통해서 스프링의 동작방식에 대한 이해와 스프링부트를 이용한 조작방법에 대해서 간단하게 소개해봤다. 예를 들어 로그인 서비스를 위한 서버를 만든다고 가정해보자. 당연하게도 다음 페이지에 넘어가기 전에 로그인 검증부터 진행해야 할 것이다. 이를 위해 api를 하나 더 추가해서 회원임을 확인 할 수 있다. 하지만 Restful한 api 서버의 경우 stateless 한 특징을 가지고 있다. 즉, 사용자가 현재 로그인 세션이 만료가 되었는지, 혹은 토큰 발행이 되었는지 매번 검증해 주고, 통과하면 api 를 통한 비즈니스 로직을 확인해 볼 수 있다.

그럼 이런 검증은 어디서 진행하면 좋을까? 정답은 필터와 인터셉터를 활용하는 것이다.

즉, 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() {}
}
  • 구현 클래스
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() {}
}
  • 스프링 빈 등록
@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() 메서드에서 추가적인 로직을 구현하여 걸러줘야 한다는 번거로움이 존재한다.

스프링 인터셉터

스프링 인터셉터는 서블릿 필터보다 편리하고, 다양한 기능을 지원해주는 기능이라고 생각하면 쉽다.

스프링 인터셉터는 스프링 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 {
	}
}

인터셉터를 추가하고 싶으면 해당 인터페이스를 구현해서 등록하면, 서블릿 컨테이너가 인터셉터를 싱글톤 객체로 생성 및 관리해준다.

서블릿 필터는 request, response만 제공하였지만, 스프링 인터셉터는 request, response뿐만 아니라 어떤 컨트롤러(Handler)가 호출되는지, 어떤 ModelAndView가 반환되는지에 대한 정보를 제공해준다. 위에서 언급한 바와 같이 3단계로 나누어서 실행시킬 수 있기 때문이다.

스프링 인터셉터에서 예외가 발생하면 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
    }
}
  • 빈에 등록
@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를 쉽게 지정해줄 수 있다.

서블릿 필터는 request, response 객체를 변경할 수 있는 기능이 있지만 실제로 잘 사용하지 않기 때문에 웬만하면 인터셉터를 사용하는 것이 낫다.

profile
Mechanical & Computer Science

0개의 댓글