Spring and Spring Boot/Spring Security

Spring Security Series (3) 필터 체인 더 깊게 들어가기 (FilterChain, Filter, ServletFilterChain, Servlet Filter, Servlet Container, HttpServletRequest, ServletRequest, Wrapper Class)

마이트너 2025. 3. 17. 23:51




각 구성요소의 시점 별로 재설명합니다. 따라서 일부 설명이 중복되는 점 양해바랍니다. 전체적인 프로세스 즉, 스프링 시큐리티 프레임워크의 동작원리를 알고 싶다면 동작원리 관점에서 기록한 여기를 참고해주세요.
FilterChain과 자주 혼동하는 SpringFilterChain이 궁금하다면 여기에 기록해두었으니 참고하시길 바랍니다.
파란 밑줄 글씨는 링크, 주황 밑줄 글씨는 단순한 강조, 초록 밑줄 글씨는 뇌피셜입니다.

 

 

 

➡️ 자바가 제공하는 인터페이스 : FilterChain

 

서블릿 API의 일부분으로 Java Servlet API에서 정의된 인터페이스이다. FilterChain은 단일 HTTP 요청을 처리하는 레이어로, Servlet Container에서 필터를 구성하고 관리하는 방식을 사용한다. FilterChain을 통해 HTTP 요청이 필터(Servlet Filter)들을 통과하게 되는데 이를 필터링이라고도 부른다.

 

 

 

 FilterChain vs ServletFitlerChain

  • FilterChain : 서블릿 필터 체인의 동작을 정의하는 인터페이스로, 여러 필터가 순차적으로 요청을 처리할 수 있도록 하는 기본적인 개념이라고 생각할 수 있다.
  • ServletFilterChain : Servlet API에서 제공하는 FilterChain 인터페이스의 구현체다. 즉, 실제로 일하는 것은 ServletFilterChain다.

 

 

 FilterChain vs SecurityFilterChain

 : FilterChain에 대해 학습할 때 가장 혼란스러워 하는 부분이 Spring Security의 SecurityFilterChain인데, 이 부분에 대해 먼저 설명해 혼란을 없애고 (Servlet)FilterChain에 대해 자세히 알아보려고 한다. 아래 각각의 설명을 보면 알겠지만 둘은 다른 인터페이스다. 다만 이것들이 결국 Servlet Container라는 동작 공간(서블릿 실행 환경)에서 하나의 흐름으로 연결되어 통합되어 사용되는 인터페이스이며, SecurityFilterChain는 Servlet Container의 기본 필터체인인 FilterChain에 삽입되어 함께 동작한다. 그렇기 때문에 둘 사이에는 포함관계가 있다는 것이다.

  • FilterChain : 일단 분명히 말해두지만 FilterChain은 Spring Security Framework에 포함되지 않는다. 웹 애플리케이션의 모든 필터를 포함할 수 있으며, 보안 필터 외에도 다양한 기능을 수행하는 것이 특징이다. 우리가 주로 Spring Security를 다룰 때 FilterChain을 살펴보기 때문에 혼동하는 경우가 많은데, 이는 Java EE(Jakarta EE)에서 정의된 서블릿 필터 체인이다. 즉, 자바가 제공하는 인터페이스라는 점을 기억하자.
  • SecurityFilterChain : 반면 SecurityFilterChain은 이름에서도 알 수 있듯이 Spring Security Framework에서 제공하는 인터페이스다. 이는 FilterChain보다 특수하고 작은 규모의 필터들만을 제공한다. 오직 Spring Security의 보안 필터만을 포함하여 요청과 응답의 보안 처리를 담당한다. 둘다 요청과 응답을 필터링한다는 점은 같지만 제공해주는 곳이 다르고 다른 인터페이스므로 혼동하지 말자. 



 FilterChain의 동작 시기

 : FilterChian이 동작하는 시기는 DispatcherServlet을 기준으로 한다. 하나씩 살펴보자. 

  • 첫 번째로 클라이언트 요청이 Dispatcher Servlet을 도달하기 전에 FilterChain이 요청을 가로채 필터링한다. 이를 전처리라고 부른다. Dispatcher Servlet은 물론이고 다른 Servlet을 썼을 때도 마찬가지이다. 
  • 두 번째 시기는 다음과 같다. 여러 동작에 걸쳐 클라이언트 요청에 대한 결과물을 서버에서 클라이언트로 보내게 되는데, 전처리 때와 마찬가지로 이번에도 DispatcherServlet이 클라이언트로 응답을 보내기 전에 가로챈다. 이렇게 FilterChain이 DispatcherServlet의 응답을 가로채는 것을 후처리라고 부른다.

 

 

✅ FilterChain의 구조와 구성요소 : Servlet Filter

 : FilterChain은 여러 개의 필터가 연쇄적으로 순서대로 엮어있는 체인과 같은 구조를 가지고 있는데, 여러 Fiter 객체가 순서대로 실행되며 HTTP 요청 및 응답이 처리된다. 이러한 FilterChain을 구성하는 각 필터를 Servlet Filter라고 부르며, Servlet Filter는 Filter 인터페이스를 implements 받는다. Filter 인터페이스의 메서드인 doFilter() 메서드를 이용해 요청을 가로채고 처리한 후 다음 필터로 요청을 전달할 수 있으며, 이 덕분에 FilterChain의 연쇄적인 필터링 작업이 가능한 것이다. 더 자세한 내용은 맨 아래 ➡️ 필터 체인의 구성요소 더 알아보기 에서 살펴보자.

 

 

 

 FilterChain 관리 : Servlet Container (서블릿 컨테이너)

 : 오해하지 말아야 할 점은 FilterChain은 직접 구현되는 것이 아니라, ServletContainer에 의해 관리되는 객체 즉, 인터페이스라는 점이다. 본인이 스스로 동작하는 게 아니라 Servlet Container가 필터를 설정하고 요청이 들어왔을 때 필터를 순차적으로 호출하는 등, Servlet Container가 FilterChain 객체를 통해 필터링 작업을 조정하는 것이다. 간편하게 생각하면 주체가 Servlet Container에 있다고 이해할 수 있다. 이는 FilterChain이 인터페이스이며 Servlet API의 일부로 정의되어 있다는 사실을 통해서도 확인할 수 있다. 각 필터는 web.xml에 정의하거나 FilterRegistrationBean을 사용하여 ServletContainer에 등록되며, 필터가 등록될 때 Servlet Container에 의해 FilterChain가 자동으로 생성된다. 이 설정 파일에서 필터의 순서와 URL 패턴을 정의하여 필터 체인을 설정할 수 있다. 참고로 Java EE 6부터는 @WebFilter 어노테이션을 사용하여 보다 직관적으로 필터를 정의하고 필터 매핑을 코드로 처리할 수 있다. 여기서 기억해야 할 점은 Servlet Container는 FilterChain을 관리하며 필터와 서블릿을 실행하는 역할을 한다는 사실이다.

 

 

 ServletFilter와 Spring Bean을 연결하는 역할 : DelegatingFilterProxy

  • 자바에서 제공하고 서블릿 컨테이너에서 생성 및 관리되는 ServletFilterChain 내부의 각 필터(ServletFilter)
  • 스프링 시큐리티에서 제공하며 스프링 컨테이너에 등록되고 생성 및 관리되는 SecurityFilterChain이라는 이름을 가진 스프링 빈(Spring Bean)

생각해보면 이상하다. 위의 두 개는 서로 실행되는 위치 자체가 다른데 어떻게 연결되는지 의문이 생길지도 모른다. 그 의문의 해결점은 DelagatingFilterProxy에 있다. DelagatingFilterProxy란, Spring Framework에서 제공하는 클래스로 주로 웹 애플리케이션 보안에서 사용되며, 서블릿 필터와 스프링 빈을 연결하는 역할을 하는 클래스이다. 구체적으로 설명하자면, 서블릿 필터로 등록된 필터의 로직을 Spring Application Context에서 관리되는 빈으로 위임(delegation)하는 역할을 하는 것이다. 이렇게 함으로써 DelagatingFilterProxy는 필터에서 스프링의 다양한 기능(예: 트랜잭션 관리, AOP, 보안 등)을 쉽게 사용할 수 있게 해준다. 일반적으로 스프링 시큐리티와 같은 보안 설정에서 특히 이 클래스를 자주 사용한다. 이 말을 통해, 이 클래스를 스프링 시큐리티에서만 사용하는 것은 아니지만 우리가 현재 알고자하는 스프링 시큐리티에서 꽤나 중요한 역할을 해주고 있다는 것을 알 수 있다. 스프링 시큐리티에서의 예를 들어 보면, (Spring)SecurityFilterChain이라는 이름의 필터 빈을 Application Context에서 관리하게 하고, DelegatingFilterProxy가 해당 빈으로 요청 처리를 위임하는 경우 (자세한 설명은 동작원리에서 보자.)를 들 수 있겠다. 이렇게 되면 생기는 일련의 과정을 맛보기를 살펴보자.

  • (Servlet)FilterChain의 ServletFilter는 DelagatingFilterProxy 클래스를 사용하여 (Spring) Application Context에서 Spring Bean인 SecurityFilterChain을 찾아 요청을 위임한다.
  • 여기서 DelagatingFitlerProxy는 ServletFilter와 마찬가지로 Filter 인터페이스의 구현체가 되는 것이며, 덕분에 서블릿 컨테이너(Servlet Container)의 생명주기와 스프링 컨테이너(Spring Application Context)가 연결될 수 있는 것이다.
  • 그 후 Spring Bean(SecurityFilterChain)에서 구현한 ServletFilter를 이용해 본인이 잘 하는(스프링 시큐리티에서 제공하는) 보안 필터링 요청처리를 수행함으로써, (Servlet)FilterChain의 다음 ServletFilter로 넘어갈 수 있게 된다.

 


➡️ FilterChain의 주요 기능 네 가지 살펴보기

 

 FilterChain의 기능 첫 번째 : 요청 및 응답 수정

  • 각 필터는 downstream 필터와 Dispatcher Servlet을 사용하여  ServletRequest를 래핑함으로써, 요청 파라미터를 추가 및 수정하거나 입력 스트림을 읽어 변경할 수 있다.
  • 각 필터는 downstream 필터와 Dispatcher Servlet을 사용하여 ServletResponse를 래핑하여 응답의 내용을 수정하거나 응답 헤더를 추가 및 변경할 수도 있다. 각각 요청 수정(Request Wrapping), 응답 수정(Response Wrapping)이라 부른다.
  • 요청 및 응답을 수정하는 방법으로는 HttpServletRequestWrapper, HttpServletResponseWrapper 클래스를 재정의(overriding)하여 요청 및 응답 수정한 커스텀 클래스를 만들 수 있다. 또는 각 필터 안에서 HttpServletRequestWrapper, HttpServletResponseWrapper 클래스를 직접 사용하여 요청을 수정하고 응답할 수도 있다. 이는 예제 코드로 직접 살펴보자.
예제 코드들은 다소 긴 관계로 접은 글을 클릭하면 볼 수 있게 만들었습니다.
더보기
더보기

EX. HttpServletRequestWrapper 클래스를 재정의(overriding)하여 요청 수정한 커스텀 클래스

public class MyRequestWrapper extends HttpServletRequestWrapper {
    public MyRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    @Override
    public String getParameter(String name) {
        // 요청 파라미터를 수정할 수 있습니다.
        if ("param".equals(name)) {
            return "modifiedValue";
        }
        return super.getParameter(name);
    }
}

 

EX. HttpServletResponseWrapper 클래스를 재정의(overriding)하여 응답 수정한 커스텀 클래스

public class MyResponseWrapper extends HttpServletResponseWrapper {
    private PrintWriter writer;

    public MyResponseWrapper(HttpServletResponse response) {
        super(response);
        writer = new PrintWriter(new StringWriter());
    }

    @Override
    public PrintWriter getWriter() {
        return writer;
    }

    public String getModifiedContent() {
        // 응답의 내용을 수정할 수 있습니다.
        return writer.toString().replace("original", "modified");
    }
}

 

EX. 필터 안에서 HttpServletRequestWrapper, HttpServletResponseWrapper 클래스를 직접 사용하여 요청을 수정하고 응답하는 예제

public class MyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 요청을 래핑하여 수정
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletRequestWrapper wrappedRequest = new HttpServletRequestWrapper(httpRequest) {
            @Override
            public String getParameter(String name) {
                if ("username".equals(name)) {
                    return "filteredUser";
                }
                return super.getParameter(name);
            }
        };

        // 응답을 래핑하여 수정
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        HttpServletResponseWrapper wrappedResponse = new HttpServletResponseWrapper(httpResponse) {
            private StringWriter writer = new StringWriter();

            @Override
            public PrintWriter getWriter() {
                return new PrintWriter(writer);
            }

            public String getModifiedContent() {
                return writer.toString().replace("Hello", "Hi");
            }
        };

        // 요청을 다음 필터나 서블릿으로 전달
        chain.doFilter(wrappedRequest, wrappedResponse);

        // 응답 내용을 수정
        String modifiedContent = wrappedResponse.getModifiedContent();
        response.getWriter().write(modifiedContent);
    }
}

 

 

 FilterChain의 기능 두 번째 : 보안 뿐만 아니라 다양한 기능 제공

  • 인증 및 권한 부여 : 요청이 서블릿으로 전달되기 전에 인증을 수행하거나 사용자 권한을 검사한다. 예를 들어, 특정 URL 패턴에 대해 인증된 사용자만 접근할 수 있도록 제한할 수 있다.
  • 세션 관리 : 세션이 유효한지 확인하고, 세션이 만료된 경우 적절한 응답을 생성할 수 있다
  • 요청 및 응답 로깅 : 요청과 응답에 대한 정보를 로깅하여 디버깅, 모니터링 및 감사 목적으로 사용할 수 있다. 이는 시스템의 상태를 추적하고, 문제를 진단하는 데 유용하다.
  • 성능 모니터링 : 요청 처리 시간과 같은 성능 지표를 수집하고 기록하여 애플리케이션의 성능을 분석할 수 있다.
  • 응답 데이터 압축 : 서버에서 클라이언트로 전송되는 응답 데이터를 압축하여 네트워크 대역폭을 절약하고 응답 속도를 개선할 수 있다. 일반적으로 GZIP 압축을 사용한다고하니 참고하자.
  • 데이터 전송 효율성 향상 : 압축을 통해 데이터의 크기를 줄여서 클라이언트와 서버 간의 데이터 전송 효율성을 높일 수 있다.
  • 응답 캐싱 : 자주 사용되는 응답을 캐시하여 서버의 부하를 줄이고 응답 시간을 단축할 수 있다. 캐싱을 통해 동일한 요청에 대해 서버가 매번 처리하지 않도록 할 수 있다.
  • 정적 자원 캐싱 : 정적 자원의 캐시를 설정하여 클라이언트가 자원을 다시 요청할 때 서버 부하를 줄일 수 있습니다.
  • 문자 인코딩 설정 : 요청과 응답의 문자 인코딩을 설정하여 다양한 문자셋을 처리할 수 있다.
  • 데이터 변환 : 요청 본문이나 응답 본문에서 데이터 변환을 수행하여 특정 포맷으로 변환하거나, 요청을 처리하기 전에 데이터를 준비할 수 있다.
각 기능별 예제코드입니다. 마찬가지로 다소 긴 관계로 접은 글을 클릭하면 볼 수 있게 만들었습니다.
더보기
더보기

EX. 인증필터

public class AuthenticationFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        // 사용자 인증 확인
        HttpSession session = httpRequest.getSession(false);
        if (session == null || session.getAttribute("user") == null) {
            httpResponse.sendRedirect("/login");
            return;
        }

        // 인증된 사용자일 경우 다음 필터 또는 서블릿으로 요청 전달
        chain.doFilter(request, response);
    }
}

 

EX. 로깅 필터

public class LoggingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        long startTime = System.currentTimeMillis();
        
        chain.doFilter(request, response);
        
        long duration = System.currentTimeMillis() - startTime;
        System.out.println("Request URI: " + httpRequest.getRequestURI() + " Duration: " + duration + " ms");
    }
}

 

 EX. 데이터 압축 필터

public class CompressionFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        CompressionResponseWrapper wrappedResponse = new CompressionResponseWrapper(httpResponse);
        
        chain.doFilter(request, wrappedResponse);
        
        wrappedResponse.finishResponse();
    }
}

// CompressionResponseWrapper 클래스는 응답을 압축하는 역할을 합니다.

 

EX. 캐싱 필터

public class CachingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        // 캐시 헤더 설정
        httpResponse.setHeader("Cache-Control", "max-age=3600");
        
        chain.doFilter(request, response);
    }
}

 

EX. 인코딩 필터

public class EncodingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        
        chain.doFilter(request, response);
    }
}

 

 

 FilterChain의 기능 세 번째 : 필터의 요청 전파 제어

 : 필터의 순서를 설정하여 요청이 필터 체인을 통과하는 순서를 제어할 수 있다. 각 필터(Servlet Filter)는 나머지 체인(필터) 또는 서블릿으로의 요청 전파를 제어하거나 거부할 수도 있으며, 이를 FilterChain의 체인 형식의 연쇄 구조를 막는다고 해서 체인 거부(Request Interception)이라고 부른다. 예를 들어, 인증이 필요한 요청에서 인증되지 않은 사용자가 접근할 경우, 필터는 요청을 거부하고 오류 페이지를 직접 생성할 수 있는데 이런 경우가 체인 거부에 해당된다. 순서 관리하는 방법 두 가지를 살펴보자

  • Filter 타입의 @Beans에 @Order를 붙이는 방법 : 각 필터에 @Order 애노테이션을 붙여 실행 순서를 지정할 수 있다. 숫자가 작은 필터일수록 먼저 실행된다. 이 방법은 Spring의 기본적인 순서 관리 기능을 사용하여 필터들이 정의된 순서에 따라 실행되도록 보장해준다.
@Component
@Order(1)
public class FirstFilter implements Filter {
    @Override
    public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 필터 처리 로직
        chain.doFilter(request, response);  // 다음 필터로 넘김
    }
}

 

  • Filter 타입의 @Beans에 Ordered 인터페이스를 구현하는 방법 : 필터가 Ordered 인터페이스를 구현하면 getOrder() 메서드를 통해 필터의 순서를 정의할 수 있다. 이 방법 또한 Spring의 기본적인 순서 관리 기능을 사용하여 필터들이 정의된 순서에 따라 실행되도록 보장해준다.
@Component
public class SecondFilter implements Filter, Ordered {
    @Override
    public int getOrder() {
        return 2;
    }

    @Override
    public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 필터 처리 로직
        chain.doFilter(request, response);
    }
}

 

  • FilterRegistrationBean을 사용하는 방법 : FilterRegistrationBean은 setOrder() 메서드를 제공하여 필터의 순서를 지정할 수 있게 해준다. 이 방법은 필터를 Bean으로 등록하는 방식에서 자주 사용된다. FilterRegistrationBean을 사용하면 특정 URL 패턴에 대해서만 필터를 적용하거나 여러 필터를 유연하게 등록할 수 있다는 장점이 있다. 특히 순서 지정 기능을 통해 복잡한 필터 체인 관리가 가능해진다.
@Bean
public FilterRegistrationBean<CustomFilter> customFilter() {
    FilterRegistrationBean<CustomFilter> registrationBean = new FilterRegistrationBean<>();
    registrationBean.setFilter(new CustomFilter());
    registrationBean.addUrlPatterns("/api/*");
    registrationBean.setOrder(1); // 순서를 지정
    return registrationBean;
}

 

 

 

 FilterChain의 기능 네 번째 : 요청 처리 재사용

 : FilterChain은 일반적인 웹 애플리케이션에서 여러 요청 처리 작업을 재사용할 수 있도록 구성할 수 있게끔 만들어준다. 필터를 사용하여 공통된 요청 처리 로직을 모듈화하고, 이를 필터 체인에서 재사용할 수 있다는 의미다. 예를 들어, 인증, 로깅, 압축, 캐싱 등과 같은 공통 작업을 필터로 구현하고, 이를 여러 서블릿에 대해 재사용할 수 있다. 또는 중앙 집중식 관리 방식을 사용할 수도 있다. 해석하면, 요청 처리와 관련된 공통적인 로직을 필터로 구현하면 애플리케이션 전체에서 일관성 있게 해당 로직을 적용할 수 있는데, 이를 통해 요청 처리의 중복을 줄이고 유지보수를 용이하게 만들 수 있다는 것이다.

 

 


➡️필터 체인의 구성요소 더 알아보기 : implements Filter, Request and Response, Wrapper Class

필터 체인 구조를 사용하며 각 필터들이 요청을 가로채고 수정하거나 추가적인 처리를 할 수 있도록 하는 구조를 가진다. FitlerChain의 내부 구성은 필터의 순서는 설정에 따라 결정되며, 앞서 말했듯이 내부를 구성하는 각 필터는 Filter 인터페이스를 implements 받아 정의한다. FilterChain은 이 Filter 인터페이스를 사용하여 역할을 수행하는 것이다. Filter 인터페이스는 자바에서 제공하는 인터페이스(javax.servlet.Filter)로, 요청과 응답을 가로채어 처리하는 기능을 각 필터에 제공해준다. 

 

 

 

 FitlerChain을 구성하는 각 필터에 제공하는 기능 : Filter interface's methods

 

  • doFilter(ServletRequest request, ServletResponse response, FilterChain chain) : 필터 인터페이스의 핵심 메서드로, 필터가 요청을 처리하고 다음 필터 또는 서블릿으로 요청을 전달하는 역할을 한다. 이때, Request 파라미터를 이용하여 클라이언트 요청을 일차적으로 필터링하며, RequestWrapper 클래스를 사용해 클라리언트 변경을 요청한다는 사실은 참고하자.

 

  • init(FilterConfig filterConfig) : 매개변수인 FilterConfig은 필터에 대한 초기화 정보를 제공하는 객체로, 필터의 이름, 초기화 파라미터 및 서블릿 컨테이너에 대한 접근을 제공한다. FilterConfig 객체를 통해 필터에 설정된 초기화 파라미터를 읽어올 수 있으며, 데이터베이스 연결, 파일 읽기, 기타 리소스 초기화 등 필터가 요청을 처리하기 위해 필요한 리소스를 로드하는 역할을 수행하는 메서드이다. 또한 필터에 필요한 설정을 수행하기도 한다. 실제 코드 상에서 이 메서드를 사용할 때 주로 throws ServletException와 함께 사용하니 참고하자.

 

  • destroy( ) : 필터가 서블릿 컨테이너에서 제거될 때 호출되는 메서드이다. 필터가 더 이상 필요하지 않을 때, 필터가 사용했던 자원이나 리소스를 정리하거나 필터의 상태를 저장하는 데에 사용된다. 필터의 수명 주기에서 마지막 단계에 해당하며, 필터가 작업을 마치고 메모리에서 제거되기 전에 호출된다.
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

public class FirstFilter implements Filter {

    private String initParam;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 필터 초기화 파라미터 읽기
        initParam = filterConfig.getInitParameter("myParam");
        // 추가 초기화 작업
        System.out.println("Filter initialized with parameter: " + initParam);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 요청 처리 전
        System.out.println("Before filter");

        // 다음 필터 또는 서블릿으로 요청 전달
        chain.doFilter(request, response);

        // 요청 처리 후
        System.out.println("After filter");
    }

    @Override
    public void destroy() {
        // 필터가 제거될 때 리소스 정리 작업
        System.out.println("Filter destroyed");
    }
}
@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<FirstFilter> addfirstFilter()  {
        FilterRegistrationBean<FirstFilter> registrationBean = new FilterRegistrationBean<>(new FirstFilter());
        registrationBean.setOrder(1);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean<SecondFilter> addsecondFilter()  {
        FilterRegistrationBean<SecondFilter> registrationBean = new FilterRegistrationBean<>(new SecondFilter());
        registrationBean.setOrder(2);
        return registrationBean;
    }

}

 

 

 

 FilterChain을 거치는 요청과 응답 : ServletRequest/Response, HttpServletRequest/Response

 : HttpServletRequest, HttpServletResponse 인터페이스는 웹 애플리케이션에서 요청과 응답을 처리하는 기본적인 역할을 수행한다. 이 두 개의 객체는 우리가 자주 봐왔던 클라이언트 요청과 응답에 해당한다.

 

클라이언트 요청은 HttpServletRequest 객체에 담겨 FilterChain에 전송되며, FilterChain을 거쳐 필터링된 결과는 Diapatcher Servlet을 지나 Spring Framework 등에서 처리 과정을 거친 후, 그 처리 결과를 HttpServletResponse 객체에 담아 클라이언트에게 반환한다.

 

HttpServletRequest과 HttpServletResponse는 한 마디로 데이터 송수신에서 큰 역할을 하고 있다. 구체적으로 설명하자면, FilterChain에서 각 필터가 doFilter( ) 메서드를 사용하여 다음 필터로 요청과 응답을 전달할 때, 요청과 응답을 추가로 수정하는 등의 작업을 수행하곤 한다. 또한, 등록된 필터들이 순차적으로 호출될 때, 각 필터가 요청을 가로채고 응답을 처리하는 과정에서 사용된다. 이들이 필터에서 어떻게 사용되는지 살펴보자.

 

  • HttpServletRequest
    • 필터에서의 사용 예시
      • 요청 파라미터 검증 : 요청 파라미터를 검사하여 유효성을 확인하거나 수정할 수 있다.
      • 헤더 추가 : 요청 헤더를 읽거나 변경할 수 있다.
      • 인증 정보 : 클라이언트의 인증 정보를 확인하고, 인증된 사용자인지 검증할 수 있다.
  • HttpServletResponse
    • 필터에서의 사용 예시
      • 응답 헤더 설정 : 응답 헤더를 추가하거나 수정할 수 있다.
      • 응답 본문 수정 : 응답 본문을 수정하거나 추가 데이터를 삽입할 수 있다.
      • 응답 상태 코드 : 특정 조건에 따라 응답 상태 코드를 설정할 수 있다.

 

CF. ServletRequest and ServletResponse

 : HttpServletRequest와 ServletRequest는 서블릿 API에서 요청을 처리하는 두 가지 주요 인터페이스다. 이들 간의 차이점을 이해하는 것은 서블릿 기반의 웹 애플리케이션을 개발할 때 중요한 부분이라고 한다. 


 ServletRequest : 기본 인터페이스로 서블릿 요청을 처리하기 위한 기본 인터페이스다. 요청의 일반적인 속성 및 메서드를 정의한다. 예를 들어, 요청의 입력 스트림을 가져오는 getInputStream(), 요청의 문자 인코딩을 설정하거나 가져오는 getCharacterEncoding(), 요청의 URI 및 프로토콜 정보 등을 가져오는 메서드를 제공한다. 모든 서블릿 요청에서 사용할 수 있으며, HTTP 요청 외에도 Java RMI 또는 JMS와 같은 다른 프로토콜에 대한 요청을 처리할 수 있다.


 HttpServletRequest : ServletRequest의 하위 인터페이스로 서브 인터페이스라고 부른다. 서브 인터페이스(sub-interface)란, 다른 인터페이스를 확장하는 인터페이스를 말한다. 즉, HttpServletRequest는 ServletRequest를 확장하여 HTTP 요청과 관련된 추가적인 메서드를 제공한 객체라는 것이다. 예를 들어, 요청의 HTTP 메서드(GET, POST 등)를 가져오는 getMethod(), 요청의 URL과 쿼리 파라미터를 처리하는 getRequestURL() 및 getQueryString(), 세션 객체를 가져오는 getSession(), 클라이언트의 IP 주소를 가져오는 getRemoteAddr() 등을 제공한다. 때문에 주로 HTTP 프로토콜에 특화된 요청을 처리할 때 사용하며, 웹 애플리케이션에서 HTTP 요청을 처리할 때는 HttpServletRequest를 사용하곤 한다.
그렇다면 FilterChain에서 사용되는 객체는 무엇일까?

💡 ServletRequest와 ServletResponse다. 하지만 실제로는 HttpServletRequest와 HttpServletResponse 객체가 FilterChain에서 처리된다. 이게 무슨 말인지 황당할텐데.. 실제 코드를 보면 실제로 그러하다. 
@WebFilter("/*")
public class CorsLoggingFilter implements jakarta.servlet.Filter {

(생략)

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // CORS 요청 및 응답 헤더를 로그에 기록
        System.out.println("CORS Request Origin: " + httpRequest.getHeader("Origin"));
        System.out.println("CORS Request Method: " + httpRequest.getMethod());
        System.out.println("CORS Response Headers: ");
        httpResponse.getHeaderNames().forEach(headerName ->
                                                      System.out.println(headerName + ": " + httpResponse.getHeader(headerName))
        );

        // 체인 필터링 계속 진행
        chain.doFilter(request, response);
    }

(생략)

}​​



💡 잠시 생각해보자. 모든 작업이 이루어지는 공간이자 전체적으로 관리를 담당하고 있는 곳은 Servlet Container다. 그리고 FilterChain의 각 필터(Servlet Filter)를 만들어내는 것도 Servlet Container이다. ServletRequest는 Servlet Container가 요청을 필터(Servlet Filter)에 전달할 때 사용하는 기본 인터페이스이다. 그렇다면 FilterChain을 구성하는 Servlet Filter는 ServletRequest와 ServletResponse에 의해 요청이 처리될 수 밖에 없는 것이다.


💡 FilterChian 껍데기에서 ServletRequest를 사용하는 것을 제외하고, 그 외부와 내부에서는 모두 HttpServletReqeust가 사용된다는 사실을 알게 되었다. 거꾸로 생각하면 FilterChain이 Servlet(Dispatcher Servlet 포함)의 영향력 아래 관리되고 있다는 사실을 다시한번 확인할 수 있다. 

❓그럼 HttpServletRequest는 왜 나온 걸까? ServletRequest만으로도 충분한 것 아닌가? 라는 생각이 드는데. 실제 코드를 봐서 알겠지만 외부와 맞닿아 있는 파라미터는 Servlet Request를, 외부와 무관한 FilterChain 내부에서는 HttpServletRequest로 타입 캐스팅(type casting)을 하고 있다.


💡 일단 이런 타입 캐스팅이 문제가 되진 않을까 생각하게 되는데, 이는 HttpServletRequest가 ServletRequest를 확장한 서블릿 요청의 더 구체적인 타입이기 때문에 위화감이 없는 타입 캐스팅이 가능하다. 만약 전혀 무관한 클래스 간의 타입 캐스팅이었다면 ClassCastException이 발생할 수도 있었다. 아무튼 이러한 캐스팅을 통해 HTTP 요청에 대한 더 구체적인 정보를 얻을 수 있는 것이다.
❓그래도 아직 좀 의아한 점이 남아있었는데.. HttpServletRequest를 사용하면 ServletRequest에서 제공하는 기능도 모두 사용할 수 있다. 거기에 HTTP 기능까지 추가된 HttpServletRequest를 직접 사용하는 것이 유연성과 호환성을 지키는 데 더 적합할 수 있다. 그럼에도 불구하고 FilterChain에서 파라미터로 ServletRequest는 무엇일까?


💡ServletRequest와 ServletResponse는 서블릿 API의 기본 인터페이스로, 모든 서블릿과 필터가 이들 인터페이스를 기반으로 작업하도록 설계되어있다. 필터의 설계가 이들 기본 인터페이스를 사용하는 것은 서블릿 API의 일관성과 호환성을 유지하는 데 중요한 부분이기 때문에 그런 것이라고 한다. 이번 의문의 결론은 '서블릿 API의 설계 철학과 일관성'이었다.

이번 궁금증 모두 해결 !

 

 

 

 

✅ FilterChain이 이용하는 래퍼 클래스

 : FilterChain을 구성하는 각 필터가 클라이언트의 요청을 변경하고, 클라이언트로 반환되는 응답을 변경할 수 있도록 도와주는 클래스이다. 이는 필터가 필터로서 역할을 할 수 있게 해준다. 직접 정의할 경우 HttpServletRequestWrapper와 HttpServletReponseWrapper 클래스를 상속받아 정의할 수 있다. 이들은 각각 HttpServletRequest, HttpServletResponse 인터페이스를 구현한 추상클래스이며, 이를 상속받아 기본 요청 및 응답 객체를 래핑하여 필터나 서블릿이 요청과 응답을 직접 변경할 수 있도록 해준다. 

 

래퍼 클래스를 직접 정의하여 필터나 서블릿의 동작을 용도에 맞게 개선하는 것은 필수적이진 않지만, 이를 통해 보다 유연하고 강력한 요청 및 응답 처리가 가능해진다고 하니 참고용으로 적어두었다.

 

  • 요청 데이터 변경 : 요청 파라미터를 변경하거나 추가할 때 사용할 수 잇다. 예를 들어, 요청 파라미터를 수정하여 필터에서 조건에 따라 요청을 변경할 수 있다.
  • 요청 헤더 추가/수정 : 요청 헤더를 추가하거나 수정할 때 사용할 수 있다.

 

래퍼 클래스를 직접 정의하는 예시를 보고 싶다면 더보기를 클릭하세요.
더보기
더보기
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.HashMap;
import java.util.Map;

public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private Map<String, String[]> additionalParameters;

    public CustomHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
        additionalParameters = new HashMap<>();
    }

    @Override
    public String getParameter(String name) {
        String[] values = getParameterMap().get(name);
        return (values != null && values.length > 0) ? values[0] : null;
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> map = new HashMap<>(super.getParameterMap());
        map.putAll(additionalParameters);
        return map;
    }

    public void addParameter(String name, String value) {
        additionalParameters.put(name, new String[]{value});
    }
}

 


➡️ 참고자료

[공식문서]

https://docs.spring.io/spring-security/reference/servlet/architecture.html

 

[교재]
스프링 부트3 백엔드 개발자 되기: 자바 

스프링 시큐리티 인 액션

 

[블로그]

https://jun-itworld.tistory.com/28

https://oh-sh-2134.tistory.com/114

https://velog.io/@gwichanlee/Filter-FilterChain

 

 


728x90