Spring

스프링 부트 필터의 동작 구조

개발하는지호 2024. 10. 21. 04:05

전반적인 흐름

 

1. 클라이언트 요청이 발생하면, 해당 요청은 서블릿 컨테이너(Tomcat, Jetty 등)에 전달된다.

2. 서블릿 컨테이너는 요청을 처리하기 전에 등록된 <<필터 체인(filter chain)>> 을 통해 요청을 필터링한다.

3. 필터 체인에 등록된 필터들은 등록된 순서대로 요청을 가로채고, 각 필터는 필요에 따라 요청을 수정하거나, 특정 조건에서 요청을 차단할 수도 있다.

4. 모든 필터를 통과한 후에야, 서블릿 또는 스프링 MVC에 요청이 도달한다.

 

 

 

스프링에서 필터를 구현하는 방식

스프링에서 필터는 두 가지 방식으로 사용할 수 있다.

 

1. 서블릿 필터 (Servlet Filter)

  • javax.servlet.Filter 인터페이스를 구현하는 필터
  • 스프링 부트에서 <<FilterRegistrationBean>> 을 사용하여 필터를 등록할 수 있다.

2. 스프링 시큐리티 필터 (Spring Security Filter)

  • 인증 및 인가를 처리하기 위한 스프링 시큐리티의 필터 체인
  • 여러 개의 시큐리티 필터들이 요청을 검증한 후 요청이 처리된다.

 

 

필터 사용 예시 (서블릿 필터)

1. 기본 필터 구현

 

1.1 LoggingFilter.java

package com.example.demo.filter;

import javax.servlet.*;
import java.io.IOException;

public class LoggingFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("LoggingFilter initialized");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("LoggingFilter: Request is being filtered");
        chain.doFilter(request, response);
        System.out.println("LoggingFilter: Response is being filtered");
    }

    @Override
    public void destroy() {
        System.out.println("LoggingFilter destroyed");
    }
}

 

 

1.2 AuthenticationFilter.java

package com.test.test1.filter;

import com.test.test1.wrapper.CharResponseWrapper;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;

public class AuthenticationFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("AuthnticationFilter initialized");
    }


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 요청 데이터를 읽음
        String requestData = request.getParameter("body");
        System.out.println("Request data: " + requestData); // 요청 데이터 출력 (예: 클라이언트가 보낸 'body' 파라미터)

        // 응답을 수정하기 위해 response를 래핑, 이렇게 하면 버퍼에 저장이 되고 실제 데이터를 수정할 수 있다.
        CharResponseWrapper wrappedResponse = new CharResponseWrapper((HttpServletResponse) response);

        // 다음 필터 또는 서블릿(컨트롤러) 호출 -> 최종 필터까지 가서 최종 필터의 응답값을 변형하면서 최초 여기 까지 옴
        chain.doFilter(request, wrappedResponse);

        // 원래 응답 데이터를 가져옴
        String originalResponseContent = wrappedResponse.getCapturedResponse();
        System.out.println("Original Response: " + originalResponseContent);

        // 응답 데이터를 수정 (예: 응답 본문에 요청 데이터를 추가)
        String modifiedResponseContent = originalResponseContent + "\nModified response with request data: " + requestData;

        byte[] responseBytes = modifiedResponseContent.getBytes("UTF-8");
        // content-length는 바이트 수를 요구한다. 영어, 띄어쓰기는 전부 1바이트지만, 한글은 개당 3바이트이기 때문에 단순 글자 길이로 했을 경우 전부 못 읽어 낸다.
        // UTF-8은 참고하자면 특정 언어를 컴퓨터 언어 비트인 저급언어로 변형하는 즉, 인코딩 방식이다.
        response.setContentLength(responseBytes.length);
        System.out.println(responseBytes.length); // -> 한글로 인해서 영어로만 되어 있다면 52바이트가 56바이트가 된다.


        // 수정된 응답을 클라이언트에게 전송

        //문자열을 직접 담아서 보내고, 이 문자열은 내부적으로 바이트 배열로 변환되어 전송한다. 그러면 클라이언트는 이 바이트 배열을 다시 문자열로 디코딩하여 사용자가
        //볼 수 있는 형태로 표시

        // 최종 필터까지 간 뒤, 역순으로 돌아오면서 response 데이터가 변형이 일어날텐데, 이때 버퍼에 저장해두면서 변형하고 최종 변형한 뒤, 버퍼에서 클라이언트로 응답
        response.getWriter().write(modifiedResponseContent);

//        response.getOutputStream().write(responseBytes); -> 이거는 바로 바이트 단위로 전송

    }

    @Override
    public void destroy() {
        System.out.println("AuthenticationFilter destroyed");
    }


}

 

 

위 코드 처럼 응답 데이터를 가져와서 탐지 등을 하거나 변조를 한 뒤 응답값으로 보낼 수 있다.

 

 

2. 필터 등록 

 

여러 필터를 등록하려면 <<FilterRegistrationBean>> 을 사용하여 각각의 필터를 설정해야 한다.

 

package com.example.demo.config;

import com.example.demo.filter.LoggingFilter;
import com.example.demo.filter.AuthenticationFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<LoggingFilter> loggingFilter() {
        FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new LoggingFilter());
        registrationBean.addUrlPatterns("/api/*");  // 필터가 적용될 URL 패턴
        registrationBean.setOrder(1);  // 필터 실행 순서 설정
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean<AuthenticationFilter> authenticationFilter() {
        FilterRegistrationBean<AuthenticationFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new AuthenticationFilter());
        registrationBean.addUrlPatterns("/api/*");  // 필터가 적용될 URL 패턴
        registrationBean.setOrder(2);  // 필터 실행 순서 설정
        return registrationBean;
    }
}

 

 

3. 필터 실행 순서

setOrder(int order) :  필터의 실행 순서를 설정한다. 숫자가 작을수록 먼저 실행된다. 위의 예에서는 LoggingFilter가 먼저 실행되고, 그 다음에 AuthenticationFilter 가 실행된다.

 

 

3.5 Controller 설정

package com.test.test1.controller;


import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    
    @GetMapping("/api/hello")
    public String hello() {
        return "Hello, World";
    }
}

 

4. 애플리케이션 실행

이제 애플리케이션을 실행하고 /api/ 로 시작하는 요청을 보내면, 등록된 필터가 차례로 실행된다.

 

위의 Controller를 사용해서 Postman에 url인 http://localhost:8080/api/hello 를 요청하면 해당 필터를 정해준 순서대로 거친다.

 

LoggingFilter initialized
AuthenticationFilter initialized
LoggingFilter: Request is being filtered
AuthenticationFilter: Checking authentication
AuthenticationFilter: Response is being filtered
LoggingFilter: Response is being filtered

 

 

5. 전반적인 흐름

 

클라이언트 -> 필터 -> 컨트롤러 -> 필터 -> 클라이언트


 

요즘 많이 접하고 있는 필터 부분 한 번 정리해 보았다. 이번에는 Spring Boot를 사용할 때다 보니 Gradle를 활용한 필터 구조였다.

 

다음 번에는 전통적인 필터 적용 방식인 web.xml를 활용해서 해보려고 한다.