본문 바로가기

기술 블로그

필터와 인터셉터란?

안녕하세요.

이번 글에서는 필터와 인터셉터에 대한 내용을 다루고자 합니다.

'필터와 인터셉터를 내가 과연 제대로 알고 있을까?'와 같은 반성을 하게 된 개인적인 사건(?)이 있었고, 이에 대한 고민을 풀고 필터와 인터셉터에 대해서 Deep-Dive하여 정리해보고자 글을 작성하게 되었습니다. 

 

DispatcherServlet? 필터? 인터셉터?

필터와 인터셉터에 대해서 최초로 접하게 되었던 기술은 Spring MVC 였습니다. 제가 알고 있었던 간단한 내용은 아래와 같았습니다. 

  • 필터 : DispatcherServlet 앞단에서 HTTP 요청 및 응답을 처리하기 위한 오브젝트
  • 인터셉터 : DispatcherServlet이 HTTP 요청에 맞는 Handler를 호출하기 이전에 가로채어 특정 작업을 실행하는 오브젝트

필터와 인터셉터 모두 어찌되었던 HTTP 요청을 가로채어 어플리케이션 단에서 무언가 처리를 하겠다는 것인데,

그렇다면 필터와 인터셉터를 구분하여 어떻게 상황에 맞게 적절히 사용할 것인지에 대한 기준을 세우는 것이 필요했습니다. 이를 위해 필터와 인터셉터에 대해 분석하였습니다.

 

인터셉터 Deep-Dive

인터셉터는 스프링에서 등장한 개념으로 Spring MVC 2.0에서 최초로 사용되었다고 합니다. 그러니 Spring MVC에서의 인터셉터는 어떻게 사용되고 있는지 파악하고자 했습니다.

DispatcherServlet에서 Handler 및 관련 인터셉터를 어떻게 저장하고 있는지 확인하기 위해 해당 클래스를 뜯어 보았습니다.

public class DispatcherServlet extends FrameworkServlet {
	// ...생략
    protected void onRefresh(ApplicationContext context) {
        this.initStrategies(context);
    }

    protected void initStrategies(ApplicationContext context) {
    	// ...생략
        this.initHandlerMappings(context);
        // ...생략
    }
    
    private void initHandlerMappings(ApplicationContext context) {
        this.handlerMappings = null;
        if (this.detectAllHandlerMappings) {
        // ApplicationContext가 초기화 된 후 등록된 모든 HandlerMapping Bean을 불러옴
            Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
            if (!matchingBeans.isEmpty()) {
            	// Handler Mappings를 DispatcherServlet 내 필드로 저장함
                this.handlerMappings = new ArrayList(matchingBeans.values());
                AnnotationAwareOrderComparator.sort(this.handlerMappings);
            }
        } else {
            try {
                HandlerMapping hm = (HandlerMapping)context.getBean("handlerMapping", HandlerMapping.class);
                this.handlerMappings = Collections.singletonList(hm);
            } catch (NoSuchBeanDefinitionException var4) {
            }
        }
        // ... 이하 생략
    }
}

 

 위 DispatcherServlet 소스를 확인해본 결과, 아래와 같이 요약할 수 있었습니다.

  • DispatcherServlet이 초기화 되는 시점에 HandlerMapping Bean을 DispatcherServlet 내 필드로 등록한다.

그렇다면 HandlerMapping은 무엇일까요? HandlerMapping은 인터페이스로 아래와 같이 선언되어 있었습니다.

public interface HandlerMapping {
    @Nullable
    HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

 

HandlerMapping 구현체들의 추상 클래스인 AbstractHandlerMapping의 getHandler 메소드는 아래와 같습니다.

public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport implements HandlerMapping, Ordered, BeanNameAware {
 // ... 중략
    @Nullable
    public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        Object handler = this.getHandlerInternal(request);
        if (handler == null) {
            handler = this.getDefaultHandler();
        }

        if (handler == null) {
            return null;
        } else {
        // ...
            // Handler에 맞는 HandlerExecutionChain을 찾음
            HandlerExecutionChain executionChain = this.getHandlerExecutionChain(handler, request);
        // ...
            return executionChain;
        }
    }
    
// ... 중략

    protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
        HandlerExecutionChain var10000;
        if (handler instanceof HandlerExecutionChain handlerExecutionChain) {
            var10000 = handlerExecutionChain;
        } else {
            var10000 = new HandlerExecutionChain(handler);
        }

        HandlerExecutionChain chain = var10000;
        Iterator var7 = this.adaptedInterceptors.iterator();

        while(var7.hasNext()) {
            HandlerInterceptor interceptor = (HandlerInterceptor)var7.next();
            if (interceptor instanceof MappedInterceptor mappedInterceptor) {
                if (mappedInterceptor.matches(request)) {
                // request에 맞는 Interceptor를 HandlerExecutionChain에 등록함
                    chain.addInterceptor(mappedInterceptor.getInterceptor());
                }
            } else {
                chain.addInterceptor(interceptor);
            }
        }

        return chain;
    }
}

 

위 HandlerMapping 추상 클래스 내 메소드들을 종합하여 정리해보면 아래와 같았습니다.

  • Bean으로 등록된 HandlerMapping 구현체는 getHandler 메소드를 통해 HandlerExecutionChain을 반환한다.
  • HandlerExecutionChain은 Handler와 HandlerMapping 내 Interceptor List 2가지를 필드로 갖는다.

즉, DispatcherServlet의 '적절한 Handler 및 인터셉터 호출'에 대한 비밀은 HandlerExecutionChain이 모두 담당하고 있었습니다. DispatcherServlet에서 HTTP 요청을 처리하는 코드를 보면 아래와 같이 정확하게 HandlerExecutionChain을 활용합니다.

public class DispatcherServlet extends FrameworkServlet {

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

        try {
            try {
                ModelAndView mv = null;
                Exception dispatchException = null;

                try {
                    processedRequest = this.checkMultipart(request);
                    multipartRequestParsed = processedRequest != request;
                 // HandlerExecutionChain을 가져옴
                    mappedHandler = this.getHandler(processedRequest);
                    if (mappedHandler == null) {
                        this.noHandlerFound(processedRequest, response);
                        return;
                    }

                    HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                    // ... 이하 생략

        }
    }
}

 

결론은 이렇습니다. DispatcherServlet이 초기화 되는 시점에, DispatcherServlet은 Bean으로 등록된 HandlerMapping을 필드로 갖고, HandlerMapping는 Handler와 인터셉트를 필드로 갖는 HandlerExecutionChain를 반환하는 getHandler 메소드가 존재합니다. 해당 메소드를 통해 DispatcherServlet은 클라이언트의 요청에 맞는 Handler와 인터셉터를 불러올 수 있었습니다.

 

그렇다면 인터셉터는 왜 스프링 빈으로 등록되지 않아도 괜찮을까요? 그 이유는 '스프링 빈으로 관리될 필요가 없기 때문' 이라고 생각했습니다. 스프링 빈은 개인적으로 싱글 톤 인스턴스 및 인스턴스 간 Dependency Injection이 가장 큰 핵심이라고 생각합니다. 하지만, 인터셉터는 이러한 기준으로 보았을 때 스프링 빈으로 관리될 이유가 없다고 판단했습니다. 그렇기 때문에 인터셉터는 WebMvcConfigurer 내에서 인터셉터 레지스트리에만 등록되고 빈으로는 등록되지 않는다고 생각했습니다.

 

 그러므로, 스프링 MVC에서의 인터셉터의 동작 원리를 보았을 때, '인터셉터란 무엇인가?'에 대한 개인적인 결론은 아래와 정의하였습니다.

  • Java 진영에서는 스프링에서 최초 등장한 개념이다.
  • 스프링 빈으로 등록된 객체가 특정 메소드를 실행하기 이전 및 이후에 특정 전후처리를 위해서 사용된다.
  • 인터셉터 동작 방식을 보면 프록시 패턴과 AOP와 유사한 느낌이 어느정도 있다.

물론, AOP 및 프록시 패턴과 달리 인터셉터에서는 ServletRequest/Response 객체를 직접 얻을 수 있습니다.

 

 

필터 Deep-Dive

Java에서 Spring이 사용되기 이전에도 필터를 사용했습니다. 예를 들어, 어노테이션 기반으로는 @WebFilter, 또는 web.xml 등을 통해서도 필터 설정이 가능했습니다. 이처럼, 필터란 스프링 컨테이너에서 관리되는 것이 아닌 서블릿 컨테이너(tomcat 등)에서 관리되는 기능입니다. 

 

그렇다면 필터에서는 스프링 빈을 사용할 수 없는 것일까요? 아닙니다. 아래와 같이 스프링 빈으로 등록하여 사용할 수 있습니다.

@Configuration
public class WebConfig {
    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new LogFilter());
        filterFilterRegistrationBean.setOrder(1);
        filterFilterRegistrationBean.addUrlPatterns("/*");
        return filterFilterRegistrationBean;
    }
 }

 

 

필터란 서블릿 컨테이너에서 관리되고 빈은 스프링 컨테이너에 등록 된다는 점에서 차이가 있지만, 두가지가 함께 필터 등록을 위해 사용될 수 있다는 점이 다소 모순적이기도 합니다. 그래서, 서블릿 컨테이너에서도 스프링 빈을 사용할 수 있도록 하는 방법이 무엇인지 확인했습니다.

 

SpringApplication이 시작되는 시점에 refreshContext(context) 메소드가 아래와 같이 호출됨을 확인했습니다.

public class SpringApplication {
	public ConfigurableApplicationContext run(String... args) {
    	// ...
   	this.refreshContext(context);
        // ...
    }
    
    private void refreshContext(ConfigurableApplicationContext context) {
        if (this.registerShutdownHook) {
            shutdownHook.registerApplicationContext(context);
        }
        this.refresh(context);
    }
    
    protected void refresh(ConfigurableApplicationContext applicationContext) {
        applicationContext.refresh();
    }
}

 

ConfiguratiableApplicationContext 인터페이스의 구현체 ServletWebServerApplicationContext의 refresh 메소드는 아래와 같았습니다. 정확히 동일한 소스는 아니지만 설명을 위해 클래스 상속관계 및 예외 처리 등의 불필요 소스를 제거하였습니다.

public class ServletWebServerApplicationContext extends GenericWebApplicationContext implements ConfigurableWebServerApplicationContext {
    
    public final void refresh() throws BeansException, IllegalStateException {
        try {
            this.createWebServer();
        } catch (Throwable var2) {
            throw new ApplicationContextException("Unable to start web server", var2);
        }
    }
    
    private void createWebServer() {
        WebServer webServer = this.webServer;
        ServletContext servletContext = this.getServletContext();
        try {
        	this.getSelfInitializer().onStartup(servletContext);
        } catch (ServletException var5) {
            throw new ApplicationContextException("Cannot initialize servlet context", var5);
        }
        this.initPropertySources();
    }
    
    private ServletContextInitializer getSelfInitializer() {
        return this::selfInitialize;
    }

    private void selfInitialize(ServletContext servletContext) throws ServletException {
        this.prepareWebApplicationContext(servletContext);
        this.registerApplicationScope(servletContext);
        WebApplicationContextUtils.registerEnvironmentBeans(this.getBeanFactory(), servletContext);
        Iterator var2 = this.getServletContextInitializerBeans().iterator();

        while(var2.hasNext()) {
            ServletContextInitializer beans = (ServletContextInitializer)var2.next();
            beans.onStartup(servletContext);
        }

    }

}

 

 

SpringApplication이 실행되는 시점에 Bean으로 등록된 ServletContextInitializer 구현체들을 startup 하는 것이 핵심이었습니다.

 

ServletContextInitializer의 대표적인 구현체 중 하나는 처음에 언급했던 FilterRegistrationBean입니다. FilterRegistrationBean는 상속 관계를 갖는 클래스로, 부모 클래스의 onStartup 메소드를 실행하여 Bean으로 등록된 자신을 서블릿의 필터로 등록하는 구조였습니다.

 

DelegatingFilterProxyRegistrationBean 또한 ServerletContextInitializer 인터페이스의 구현체로 FilterRegistrationBean과 유사한 상속 관계를 갖는 클래스임을 확인했습니다. DelegatingFilterProxyRegistrationBean은 FilterRegistrationBean과 마찬가지로 부모 클래스의 onStartup 메소드를 통해 서블릿의 필터로 등록됨을 확인하였습니다.

 

FilterRegistrationBean과 DelegatingFilterProxyRegistrationBean 모두 스프링 어플리케이션이 실행되는 시점에 서블릿 컨테이너의 필터로 등록된다는 점에서 공통점이 있습니다. 그러나, 두 클래스의 주된 차이점은 아래와 같습니다.

  • FilterRegistrationBean : 서블릿 컨테이너에 실제로 등록되어 필터로 동작하는 객체
  • DelegatingFilterProxyRegistrationBean : DelegatingFilterProxy를 서블릿 컨테이너에 등록하고, 필터 처리를 스프링 컨테이너에 위임함

 

그렇다면, 굳이 FilterRegistrationBean과 DelegatingFilterProxyRegistrationBean을 나누어 사용할 이유가 있었을까요? 그 이유는 스프링 부트와 관련이 있습니다. 스프링 부트에서는 SpringApplication.run에서 살펴볼 수 있듯 서블릿 컨테이너를 스프링 컨텍스트에서 관리할 수 있었습니다. 그러나, 스프링 부트를 사용하지 않을 시, 스프링 컨텍스트와 서블릿 컨테이너는 서로 영향을 줄 수 없습니다. 그러므로, 스프링 부트를 사용하지 않는 서블릿 컨테이너는 FilterProxy를 등록하여 런타임에 ApplicationContext로부터 Bean으로 등록된 필터를 조회하여 사용할 수 있도록 만든 구조라고 판단했습니다.

 

기존 레거시 시스템에서 FilterProxy의 철학에 맞추어 설계된 스프링 빈 필터들은 DelegatingFilterProxyRegistrationBean를 통해 기존 방식을 그대로 이어갈 수 있다는 장점이 있습니다. 물론, 새로이 스프링 부트를 사용하는 시스템에서는 굳이 FilterProxy를 사용할 이유는 더 이상 없습니다. 

 

마지막으로, Filter에 대한 개인적인 정리는 아래와 같습니다.

  • Filter는 스프링 컨테이너가 아닌 서블릿 컨테이너에 등록된다. 스프링이 등장하기 이전부터 존재하던 개념이다.
  • 스프링 부트는 ServletContextInitializer의 구현체인 Filter를 Spring Bean으로 등록하여 서블릿 컨테이너에 등록할 수 있다.
  • 어떤 Filter를 사용할지는 시스템의 특성에 맞추어 선택하면 된다.

 

그래서... 필터와 인터셉터란?

필터는 서블릿 컨테이너의 기능이고, 인터셉터는 특정 기능이라기 보다는 디자인 패턴에 가까운 추상적인 개념이라는 생각이 들었습니다.

 

필터는 서블릿 컨테이너에 등록되는 개념으로 CORS 설정, 로그인, HTTP 헤더 체크 등 클라이언트의 요청에 대한 전역적인 처리를 위해 사용되는 기능입니다. 즉, ServletRequest/Response에 대한 전역적인 처리가 필요할 때 사용하면 됩니다. 물론, Filter를 Bean으로 등록하여 서블릿 컨테이너에 직접 등록할지, 프록시를 사용할지 등 어떤 필터를 선택할지는 시스템의 상황에 맞추어 선택하면 됩니다. 

 

인터셉터는 ServletRequest/Response 관련 핵심 비즈니스 로직의 전후 처리를 담당하기 위한 개념입니다. 예를 들어, 스프링 MVC에서는 Controller 메소드(Handler)의 전후 처리를 담당하기 위해 사용됩니다. 그러므로, 클라이언트 주요 처리를 위한 객체는 스프링 빈으로 관리하되, 인터셉터가 다른 객체로의 Dependency Injection이 필요하지 않다면 굳이 스프링 빈으로 관리될 필요가 없습니다.

 

 

 

구체적인 소스를 분석하는 과정에서 필터와 인터셉터에 Deep-Dive하여 매우 흥미로웠습니다.

마치겠습니다. 감사합니다.

 

글에 대한 피드백은 언제나 환영합니다.

 

'기술 블로그' 카테고리의 다른 글

@Transactional Deep Dive  (1) 2025.02.19
Java Annotation  (2) 2025.02.18
Java ObjectMapper (Feat. RedisTemplate)  (2) 2025.02.13
스프링 Kafka Consumer Deep Dive - 1  (1) 2025.02.05
Spring Security에서의 Filter  (0) 2024.12.17