Spring

Spring 필터와 인터셉터, 차이점부터 활용법까지 완벽 가이드

Written by 개발자서동우 · 1 min read >
스프링 필터와 인터셉터

안녕하세요! Devloo 입니다. 🙂 Spring 프레임워크를 사용하면서 개발자들은 필터와 인터셉터에 대해 자주 듣게 됩니다. 이 두 가지 개념은 웹 애플리케이션 개발에서 중요한 역할을 하며, 목적과 기능이 다소 유사해 혼란을 일으킬 수 있습니다. 필터와 인터셉터 모두 권한 검사, 로그 처리, 데이터 압축/해제 등 다양한 기능을 수행할 수 있지만, 이들을 어떻게 구분하고 활용해야 하는지 명확히 이해하는 것이 중요합니다.

스프링 필터와 인터셉터
사진: UnsplashIonela Mat

이 글에서는 필터와 인터셉터의 기원과 설계 철학을 살펴보고, 두 개념의 차이점과 유사점을 명확히 구분해 보겠습니다. 이를 통해 필터와 인터셉터를 적절히 활용하는 방법을 이해하고, 실제 개발 시 혼란을 줄이는 데 도움이 되고자 합니다. 필터와 인터셉터의 기본 개념부터 Spring에서의 사용법, 그리고 일반적인 사용 시나리오까지 체계적으로 설명드리겠습니다. 자 그럼 시작해볼까요 🙂 ?

이 설명은 SpringBoot 2.7.5 버전을 기준으로 작성되었습니다.

필터(Filter): 외부에서 도입된 개념

1. 기본 개념

소스 코드를 자세히 살펴보면, 필터의 개념이 실제로 서블릿 사양을 준수하는 서블릿에서 도입된 외부 개념임을 알 수 있습니다. 필터 클래스의 전체 이름은 다음과 같습니다:

javax.servlet.Filter

필터는 Tomcat과 같은 웹 컨테이너에서 서블릿 관련 처리를 위해 사용되며, 원래 Spring의 도구가 아닙니다. 이 사실은 Spring의 필터와 인터셉터가 왜 유사한 기능을 하는지 이해하는 데 도움이 됩니다.

서로 다른 시스템을 위해 각기 다른 저자들이 만들었기 때문에 비슷한 아이디어와 접근 방식을 취하게 된 것은 이해할 만합니다. 결국, 위대한 생각은 비슷하게 모이는 법이니까요.

이후 Spring은 Tomcat 컨테이너의 처리 로직을 도입하여 두 가지 유사한 개념을 동일한 애플리케이션 컨텍스트(Application Context)에 두었습니다. (Spring은 이들을 통합하지 않고 호환되도록 만들었을 뿐입니다.) 이로 인해 개발자들이 혼란스러워하는 것도 이해할 수 있습니다.

필터의 역할을 더 잘 이해하기 위해, 공식 설명을 소개하겠습니다.

필터는 자원(서블릿 또는 정적 콘텐츠)에 대한 요청이나 자원에서 반환되는 응답, 또는 두 가지 모두에 대해 필터링 작업을 수행하는 객체입니다.

이 정의에서 두 가지 유용한 정보를 얻을 수 있습니다:

  • 실행 시점: 필터는 자원에 대한 요청이 처리되기 전과 자원에서 응답이 반환되기 전에 실행됩니다.
  • 실행 내용: 필터는 필터링 작업을 수행하며, 필터링 조건은 자원에 대한 요청이나 자원에서 반환되는 응답에 따라 결정됩니다.

이 정보를 바탕으로, Tomcat의 서블릿 컨테이너 구조와 결합하여 필터 실행의 다음과 같은 프로세스 흐름도를 도출할 수 있습니다:

필터 실행 프로세스
필터 실행 프로세스

실제 개발 시나리오에서는 자원 요청의 전처리나 자원 응답의 후처리가 단일 유형의 필터링 작업에만 국한되지 않을 수 있습니다.

따라서 Tomcat은 여러 유형의 필터가 요청이나 응답을 처리해야 하는 상황을 다루기 위해 책임 연쇄 패턴을 설계에 사용합니다.

이 개념은 앞서 언급한 흐름도에서도 반영됩니다. 체인 구조는 선형 데이터 구조이므로 필터 작업의 실제 과정에서 고유한 실행 순서가 있습니다. 이는 커스텀 필터를 구현할 때 필터 간의 의존성 역전이 없도록 해야 함을 의미합니다.

물론, 필터 간에 의존성이 없다면 실행 순서는 큰 문제가 되지 않습니다. Tomcat은 org.apache.catalina.core.ApplicationFilterChain을 사용하여 앞서 언급한 책임 연쇄 패턴을 구현합니다. 이를 이해하기 위해 일부 코드를 살펴보겠습니다:

public final class ApplicationFilterChain implements FilterChain {

    public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
        if (Globals.IS_SECURITY_ENABLED) {
            final ServletRequest req = request;
            final ServletResponse res = response;
            try {
                java.security.AccessController.doPrivileged(
                        (java.security.PrivilegedExceptionAction<Void>) () -> {
                            internalDoFilter(req, res);
                            return null;
                        }
                );
            } catch (PrivilegedActionException pe) {
                // 예외 처리
            }
        } else {
            internalDoFilter(request, response);
        }
    }

    private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
        if (pos < n) {
            ApplicationFilterConfig filterConfig = filters[pos++];
            try {
                Filter filter = filterConfig.getFilter();
                if (Globals.IS_SECURITY_ENABLED) {
                    // 보안 설정이 활성화된 경우
                } else {
                    filter.doFilter(request, response, this);
                }
            } catch (IOException | ServletException | RuntimeException e) {
                throw e;
            } catch (Throwable e) {
                // 예외 처리
            }
            return;
        }

        try {
            servlet.service(request, response);
        } catch (IOException | ServletException | RuntimeException e) {
            throw e;
        } catch (Throwable e) {
            // 예외 처리
        } finally {
            // 최종 정리 작업
        }
    }
}

위 코드에서 알 수 있듯이, Tomcat은 pos 포인터를 사용하여 필터 체인 내에서 필터의 실행 위치를 추적합니다. 체인의 모든 필터가 실행되고 통과된 후에야 요청과 응답 객체가 서블릿 인스턴스로 제출되어 서비스 처리가 이루어집니다.

이 시점에서는 특정 핸들러가 개입하지 않기 때문에, 필터는 특정 핸들러 클래스의 요청/응답을 세분화하여 처리할 수 없고, 전체 서블릿 인스턴스 수준에서 모호하게 처리할 수밖에 없습니다.

또한, 위 코드에서 확인할 수 있는 또 다른 문제는 자원 요청에 대한 필터링 처리만 있고, 자원 응답에 대한 필터링 처리는 없다는 점입니다.

실제로 자원 응답에 대한 필터링 처리는 각 필터의 doFilter 메서드 내부에 숨겨져 있습니다. 커스텀 필터를 구현할 때는 아래와 같은 로직을 따라야 자원 응답을 처리할 수 있습니다:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    // 전처리 작업
    chain.doFilter(request, response);  // 체인 구조를 유지하기 위한 필수 호출
    // 후처리 작업
}

이것을 ApplicationFilterChaininternalDoFilter 메서드와 결합해 보면, 푸시와 팝 로직이 내포되어 있다는 것을 알 수 있습니다(본질적으로 메서드 스택입니다). 자원 요청의 전처리는 푸시 과정이며, 모든 전처리 필터가 스택에 푸시된 후에 servlet.service(request, response)가 실행됩니다.

서블릿 서비스 처리가 완료된 후에는 팝 과정이 시작되며, 마지막 필터(위 코드의 마지막 줄에 위치한)의 후처리 로직이 순서대로 실행되고 메서드에서 빠져나갑니다.

이 로직은 초보자에게 친숙하지 않을 수 있습니다. 필터는 인터페이스일 뿐이므로 추상 클래스처럼 템플릿 메서드를 제공할 수 없어, 좋은 예제 참고 없이 소스 코드만 보고 사용하기 어려울 수 있습니다.

2. Spring에서의 사용 방법

여기서 언급하는 것은 Spring이 아니라 실제로는 Spring Boot에서의 사용 방법입니다. Spring Boot에서 커스텀 필터를 구현하려면, 필터를 Spring 컨테이너에 주입하는 로직을 추가해야 합니다. Spring Boot는 이를 수행하는 두 가지 방법을 제공합니다:

  1. 커스텀 필터에 @Component 어노테이션을 사용하는 방법
  2. 커스텀 필터에 @WebFilter 어노테이션을 사용하고, 시작 클래스에 @ServletComponentScan 어노테이션을 사용하는 방법

두 번째 방법을 더 추천하는 이유는 Spring이 원래 Tomcat 처리에는 없는 추가 기능, 즉 URL 매칭 기능을 제공하기 때문입니다.

@WebFilter 애노테이션의 urlPattern 필드를 활용하면, Spring은 필터 처리를 더 세밀하게 할 수 있어 개발자가 더 유연하게 사용할 수 있습니다. 또한, 필터 주입 순서를 결정하기 위해 Spring이 제공하는 @Order 애노테이션을 사용하여 필터의 순서를 지정할 수 있습니다. 아래는 코드 예시입니다:

@Component
@Order(1)
public class TransactionFilter implements Filter {

    @Override
    public void doFilter(
      ServletRequest request, 
      ServletResponse response, 
      FilterChain chain) throws ServletException {
 
        HttpServletRequest req = (HttpServletRequest) request;
        LOG.info(
          "Starting a transaction for req : {}", 
          req.getRequestURI());
 
        chain.doFilter(request, response);
        LOG.info(
          "Committing a transaction for req : {}", 
          req.getRequestURI());
    }
    // other methods 
}

@Component
@Order(2)
public class RequestResponseLoggingFilter implements Filter {

    @Override
    public void doFilter(
      ServletRequest request, 
      ServletResponse response, 
      FilterChain chain) throws ServletException {
 
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        LOG.info(
          "Logging Request  {} : {}", req.getMethod(), 
          req.getRequestURI());
        chain.doFilter(request, response);
        LOG.info(
          "Logging Response :{}", 
          res.getContentType());
    }
    // other methods
}

인터셉터(Interceptor): Spring의 고유 기능

1. 기본 개념

필터에 대해 살펴본 후, 이제 인터셉터로 눈을 돌려보겠습니다. 인터셉터는 원래 Spring에서 비롯된 개념으로, 해당 인터페이스 클래스는 HandlerInterceptor입니다 (비동기 인터셉터 인터페이스 클래스도 있지만, 여기서는 다루지 않겠습니다. 관심 있는 분들은 소스 코드를 참고하시기 바랍니다 🙂 ).

관련 소스 코드를 검토해보면, Filter가 단순히 doFilter 메서드만 제공하는 것과 달리, HandlerInterceptor는 실행 시점과 관련된 세 가지 메서드를 명확히 제공합니다:

  • preHandle: 해당 핸들러가 실행되기 전에 전처리를 수행합니다.
  • postHandle: 해당 핸들러의 요청 처리가 끝난 후, ModelAndView 객체가 렌더링되기 전에 후처리를 수행합니다.
  • afterCompletion: ModelAndView 객체가 렌더링된 후, 응답이 반환되기 전에 결과 후처리를 수행합니다.

단순히 doFilter 메서드만 제공하는 Filter 클래스와 비교했을 때, HandlerInterceptor의 메서드 정의는 더 구체적이고 사용하기 쉽습니다. 소스 코드를 읽거나 사용 예제를 참고하지 않아도 커스텀 인터셉터를 구현하는 방법을 대략적으로 알 수 있습니다.

org.springframework.web.servlet.DispatcherServlet#doDispatch의 소스 코드를 결합하여 다음과 같은 흐름도를 그릴 수 있습니다.

DispatcherServlet 프로세스 흐름도
DispatcherServlet 프로세스 흐름도

인터셉터의 실행 로직은 모두 서블릿 인스턴스 내에 포함되어 있습니다. 앞서 설명한 필터의 실행 과정과 결합해 보면, 필터는 샌드위치 비스킷의 두 쿠키처럼 서블릿과 인터셉터를 감싸고 있으며, 인터셉터는 필터의 전처리 후와 후처리 전에 실행됨을 알 수 있습니다.

또한, 소스 코드를 분석하는 과정에서 Spring이 인터셉터를 사용할 때도 책임 연쇄 패턴을 사용한다는 것을 발견할 수 있습니다. 이 패턴은 서로 다른 작업과 로직을 순차적으로 실행해야 하는 상황에서 매우 유용합니다.

Spring은 인터셉터를 설계할 때 실행 단계별로 명확히 정의된 메서드를 제공하므로, 인터셉터의 실제 실행은 필터와 같은 푸시 앤 팝 방식을 사용하지 않습니다.

2. Spring에서의 사용 방법

Spring Boot에서 인터셉터를 사용하려면, HandlerInterceptor 인터페이스를 구현하는 것 외에도 Spring의 웹 설정에 명시적으로 등록해야 합니다. 설정 방법은 다음과 같습니다:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new DemoInterceptor())
                .addPathPatterns("/api/*")
                .excludePathPatterns("/api/ok");
    }
}

위 코드에서 알 수 있듯이, Spring은 필터와 동일한 경로 매칭 기능을 제공하여 커스텀 인터셉터를 사용할 수 있게 합니다. 이 기능을 통해 인터셉터는 핸들러의 요청과 응답을 더 세밀하게 처리할 수 있습니다. (이 기능은 필터와 겹치지만, Spring 내부적으로 제공하는 기능입니다.)


일반적인 사용 시나리오

사실 이 글의 시작 부분에서 두 개념의 일부 기능을 이미 소개했습니다. 여기서 다시 간략히 요약해 보겠습니다.

위의 분석을 통해 필터와 인터셉터를 설계한 사람들이 요청의 전처리와 응답의 후처리를 비즈니스 코드와 분리하여, 개발자들이 이를 확장하고 구현할 수 있는 일반적인 처리 로직으로 제공하고자 했다는 것을 알 수 있습니다. 이러한 아이디어는 AOP의 개념과도 비슷한 면이 있습니다 (정말로, 위대한 생각은 비슷하게 모이기 마련입니다).

실제 개발 시나리오에서 맞춤형 필터나 인터셉터는 주로 다음과 같은 작업을 수행하는 데 사용됩니다:

  • 사용자 로그인 검증
  • 권한 검사
  • 로그 인터셉션
  • 데이터 압축/해제
  • 암호화/복호화

여기서는 각 시나리오의 코딩 구현을 모두 다루지는 않겠지만, 관심 있는 분들은 검색을 통해 학습할 수 있습니다.

조언을 하나 드리자면, 위에 언급된 시나리오가 많아 보일 수 있지만, 본질적으로는 요청 파라미터나 응답 결과를 처리하는 것입니다. 이를 이해하면, 이러한 시나리오를 설계하고 구현하는 것이 비교적 쉬워질 것입니다.

마무리

면밀히 분석해 보면 필터와 인터셉터 간에 근본적인 차이가 없다는 것을 알 수 있습니다. 도구로서 이들이 제공하는 기능은 기본적으로 동일합니다.

다만 주의할 점은 실행 시점의 차이입니다 (하나는 서블릿 실행 전후에 작동하고, 다른 하나는 서블릿 실행 중에 작동합니다). 다른 측면에서는 큰 차이가 없습니다. 따라서 실제 개발 및 사용 시에는 둘 중 하나를 선택하는 데 크게 신경 쓸 필요가 없습니다.

이번 시간에는 스프링의 필터와 인터셉터에 대해 자세히 알아보는 시간을 갖았습니다.

끝까지 읽어주셔서 정말 감사합니다 !!, 궁금하신 사항이 있으시면 댓글 꼭 남겨주세요 🙂

Written by 개발자서동우
안녕하세요! 저는 기술 분야에서 활동 중인 개발자 서동우입니다. 명품 플랫폼 (주)트렌비의 창업 멤버이자 CTO로 활동했으며, AI 기술회사 (주)헤드리스의 공동 창업자이자 CTO로서 역할을 수행했습니다. 다양한 스타트업에서 일하며 회사의 성장과 더불어 비즈니스 상황에 맞는 기술 선택, 개발팀 구성 및 문화 정착에 깊은 경험을 쌓았습니다. 개발 관련 고민은 언제든지 편하게 연락주세요 :) https://linktr.ee/dannyseo Profile

Leave a Reply

Your email address will not be published. Required fields are marked *