티스토리 뷰

728x90
반응형

웹 서비스를 개발한다고 했을 때, 기능마다 공통으로 가지고 있는 로직이 있다. 예를 들어, 상품을 등록, 수정, 삭제하는 기능들은 검증된 사용자만 사용할 수 있는 기능들이기 때문에 사용자를 인증하는 로직이 공통으로 들어가야 한다. 이렇게 애플리케이션 여러 로직에서 공통으로 관심이 있는 있는 것을 공통 관심사(cross-cutting concern) 라고 한다.

이러한 공통 관심사를 해결하는 대표적인 기술로 스프링의 AOP가 있는데, 웹 애플리케이션이라면 서블릿 필터스프링 인터셉터가 더 좋은 대안이 된다.

서블릿 필터(Servlet Filter)

서블릿 필터는 J2EE 표준 스펙 기술로, HTTP 요청과 응답을 필터링하거나 수정할 수 있는 메커니즘을 제공한다. 주의할 점은, 스프링 컨테이너에서 동작하는게 아니라 톰캣(Tomcat)같은 서블릿 컨테이너에서 동작하기 때문에 다음과 같은 요청 흐름을 가지게 된다.

  • HTTP Request → 서블릿 컨테이너(WAS) → 필터 → 서블릿(DispatcherServlet) → 컨트롤러

서블릿 필터를 사용하기 위해서는 jakarta.servlet.Filter 인터페이스를 구현해야 한다.

Filter 인터페이스 메서드

인터페이스를 코드로 보면 다음과 같다.

package jakarta.servlet;

import java.io.IOException;

public interface Filter {

    default void init(FilterConfig filterConfig) throws ServletException {
    }

    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;

    default void destroy() {
    }
}

코드를 보면, 구현해야할 메서드가 총 3개인데, init 메서드와 destroy 메서드는 default 메서드라 필수로 구현할 필요는 없다.

  • init : 필터 초기화 메서드로 서블릿 컨테이너가 생성될 때 한 번 호출한다.
  • doFilter : 필터의 ULR 패턴에 맞는 클라이언트 요청이 올 때 마다 호출되며, 요청이 디스패처 서블릿에 전달 되기 전에 실행된다. 필터의 실질적인 로직을 여기에 구현하면 된다.
  • destroy : 필터 종료 메서드로, 서블릿 컨테이너가 종료될 때 한 번 호출된다. 사용한 리소스를 정리할 때 사용된다.

가장 중요한 doFilter 파라미터를 보면, ServletRequest, ServletResponse, FilterChain 총 3개가 있다. ServletRequest, ServletResponse 객체를 사용해서 HTTP와 관련된 편의 기능을 쉽게 사용할 수 있다.

가장 중요한 FilterChain 객체는 서블릿 컨테이너가 호출할 필터들을 가지고 있는데, doFilter 의 로직이 정상적으로 수행됐다면, 마지막에 chain.doFilter 메서드를 호출해야만 다음 필터로 넘어갈 수 있다. 만약 현재 필터가 마지막 필터라면, 컨트롤러를 호출하게 된다.

public class LoginFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {

        // 로그인 인증 관련 로직...

        chain.doFilter(servletRequest, servletResponse);  // 호출하지 않으면 다음 필터 또는 컨트롤러가 호출 X
    }
}

필터 등록 방법

필터를 등록하는 방법은 여러가지가 있지만, 스프링 부트를 사용한다면 FilterRegistrationBean 를 사용해 등록하는 방법이 가장 좋다.

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LoginFilter());    // 필터 등록
        filterRegistrationBean.setOrder(1);                     // 순서 지정
        filterRegistrationBean.addUrlPatterns("/*");            // 적용할 URL 패턴
        return filterRegistrationBean;
    }
}

스프링 인터셉터(Spring Interceptor)

스프링 인터셉터는 필터와 다르게 스프링 MVC가 제공하는 기술이다. 때문에 스프링 컨테이너 안에 있는 DispatcherServlet 에 의해 동작된다.

  • HTTP Request → 서블릿 컨테이너(WAS) → 필터 → 서블릿(DispatcherServlet) → 인터셉터 → 컨트롤러

인터셉터 역시 URL 패턴에 맞는 모든 컨트롤러가 호출되기 전에 동작하기 때문에 공통 관심사를 분리하기 좋고, 여러 개의 인터셉터를 체인 형태로 묶을 수 있으며, 순서도 지정할 수 있다. 다만, 필터보다 편리하고 정교한 기능들을 지원한다.

구현할 인터페이스는 org.springframework.web.servlet.HandlerInterceptor 다.

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 를 반환하면, 다음 인터셉터 혹은 컨트롤러가 정상 호출된다.
  • postHandle : 컨트롤러가 호출된 후 호출된다. ModelAndView 파라미터를 사용해 뷰를 렌더링하는데 필요한 데이터를 넘겨줄 수 있다.
  • afterCompletion : 뷰가 렌더링 된 이후에 호출된다. 예외가 발생하면 Exception 파라미터에 예외 정보가 담겨 호출된다.

위와 같이 인터셉터는 필터와 다르게 컨트롤러 호출 전(preHandle), 호
출 후(postHandle), 요청 완료 이후(afterCompletion)와 같이 단계적으로 잘 세분화 되어 있다.

추가로, handler 파라미터는 컨트롤러에서 사용한 핸들러 매핑 방식에 대한 정보를 담고 있는데, @RequestMapping 을 사용했다면 HandlerMethod 타입의 핸들러 정보가 담긴다.

인터셉터 등록 방법

WebMvcConfigurer 가 제공하는 addInterceptors() 를 사용해서 인터셉터를 등록할 수 있다. 이를 위해 WebConfig 클래스가 WebMvcConfigurer 를 구현하도록 한다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor())                    // 인터셉터 등록
                .order(1)                                               // 순서 지정
                .addPathPatterns("/**")                                 // 적용할 URL 패턴
                .excludePathPatterns("/css/**", "/*.ico", "/error");    // 제외할 URL 패턴(화이트리스트)
    }
}

필터 vs 인터셉터

두 기술의 차이를 간단하게 표로 정리해 봤다.

특징 서블릿 필터 스프링 인터셉터
기술 스택 서블릿 표준 스프링 MVC
적용 범위 서블릿 컨테이너 전역 (정적 자원 포함) 스프링 컨트롤러에 매핑된 요청만 처리
적용 시점 요청이 서블릿으로 전달되기 전, 응답이 클라이언트로 전달되기 전 DispatcherServlet과 컨트롤러 사이
설정 방법 FilterRagistrationBean WebMvcConfigurer에서 addInterceptors() 메서드 사용
스프링 통합 여부 스프링과 독립적 스프링 컨텍스트와 긴밀히 통합
대상 모든 HTTP 요청 및 응답 스프링 컨트롤러와 관련된 요청
정적 리소스 처리 가능 (CSS, JS 등 포함) 처리하지 않음
전형적인 사용 사례 보안, 로깅, 데이터 압축 인증, 공통 데이터 추가, 요청 전후 처리 로직

스프링을 사용하는 개발자라면, 코드 작성이나 디버깅 시에도 스프링 기술 지원을 받을 수 있기 때문에, 대부분의 경우 스프링 인터셉터를 사용하는 편이 더 나을 수 있다.

다만, 보안, 로깅, CORS, 압축같은 서비스 전역으로 사용되는 로직이나, 정적 리소스에도 공통 로직을 적용하고 싶으면 필터를 사용하는 편이 낫다. 예를 들어, Spring Security는 내부적으로 필터를 사용해 인증/인가를 구현하고 있다.

추가로, AOP를 사용할 수도 있다는 생각이 들 수 있다. 하지만 웹 애플리케이션의 경우, 파라미터로 ServletRequest, ServletResponse 객체가 넘어오기 때문에 HTTP 관련 로직을 훨씬 편리하게 작성할 수 있다. 또한, 스프링 컨트롤러는 ArgumentResolver 덕분에 다양한 파라미터를 받을 수 있는데, 이를 AOP에서 처리하기에는 다소 어려움이 있다.

결론은, 전역으로 적용되는 보안, 로깅같은 경우가 아니면, 웹 애플리케이션의 공통 관심사는 스프링 인터셉터를 사용하는 게 속 편할 수 있다.

References

728x90
반응형
댓글