Spring Filter

이종찬·2023년 2월 14일
1

📖 Filter?

Spring Boot에서 Filter는 Web Application에서 관리되는 영역으로 커스텀 코드로 작성되어 들어오는 HTTP 요청을 처리할 수 있는 Spring Boot 프레임워크의 기능입니다. Client로 부터 오는 요청/응답에 대해 최초/최종 단계의 위치에 존재합니다. 이를 통해 요청/응답의 정보를 변경하거나, Spring에 의해 데이터가 변환되기 전의 순수한 클라이언트의 요청/응답 값을 확인할 수 있습니다. 유일하게 ServletRequest, ServletResponse의 객체 변환 가능

Spring Boot의 Filter는 쉽게 정의하고 사용가능하며 로깅 및 보안과 같은 일반적인 작업에 사용할 수 있는 Filter를 제공합니다.

Spring MVC request life cycle

Spring MVC request life cycle

이미지 출처 : https://nesoy.github.io/articles/2019-02/Spring-request-lifecycle-part-1

Request가 처음 들어왔을 때 처음으로 Filter를 거치고 DispathcerServlet -> Interceptor -> AOP동작이 되는 것을 확인할 수 있습니다. 즉, Filter, Interceptor, AOP를 다 실행하게 된다면 위의 그림과 같은 순서로 실행됩니다.


🤔 사용해야 하는 이유는?

1.Logging

필터는 들어오는 request, response를 기록하는데 사용할 수 있어 디버깅 및 모니터링에 사용할 수 있습니다.

2.보안

필터는 request 진위 여부, 사용자에게 리소스 엑세스에 필요한 권한이 있는지 등과 같은 보안 검사를 수행하는데 사용될 수 있습니다. 또한 세션 생성, 유지, 무효화와 같은 사용자 세션을 관리하는데 사용할 수도 있습니다.

3.유효성 검사

request에서 데이터가 올바른 형식인지 확인하는 유효성 검사하는데 필터를 사용할 수 도 있습니다.

4.캐싱

자주 엑세스하는 리소스에 대한 응답을 캐시하여 서버의 부하를 줄여 이용자의 응답 시간을 단축할 수도 있습니다.

5. request, response 조작

헤더 추가, 응답 데이터 변환 및 조작하는데 필터를 사용할 수 있습니다.


👨‍💻 구현

dto, controller

@Data //getter + setter + toString
@NoArgsConstructor //default constructor
@AllArgsConstructor // all constructor

class User {
    private String name;
    private String email;
    private int age;
}

@Slf4j // log 사용 가능
@RestController
@RequestMapping("/api")
public class ApiController {

    @PostMapping("/user")
    public User user(@RequestBody User user) {
        log.info("user : {} ", user); //{}와 매칭이 가능하며 {}안에 객체가 들어간다.
        return user;
    }
}

GlobalFilter

@Slf4j
@Component
public class GlobalFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		log.info("전처리");
        chain.doFilter(request, response);
        log.info("후처리");
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
        log.info("init filter {}", filterConfig);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
        log.info("destroy filter");
    }
}

Filter에 주요 메서드는 init, doFilter, destroy로 구성되어있습니다.

  • init : 필터 인스턴스 초기화시 최초 한번만 실행됩니다.
  • doFilter : 클라이언트의 요청/응답을 처리시 사용되는 메서드 입니다. 대부분의 로직이 doFilter에 들어갑니다.
  • destroy : 필터 인스턴스가 제거될 때 실행됩니다.

말 그대로 Global하게 사용하기 위해 @Component를 사용하였습니다. doFilter실행하기 전에 코드는 전처리 구간, 이후는 후처리 구간으로 나뉩니다. 실제로 해당 메서드를 실행하면 다음과 같은 결과가 나옵니다.

com.example.filter.filter.GlobalFilter : 전처리
c.e.filter.controller.ApiController : user : User(name=a1w1, email=aaaaa, age=10)
com.example.filter.filter.GlobalFilter : 후처리

doFilter를 기준으로 나뉘는 것을 확인할 수 있습니다. Filter를 사용하면서 여러가지로 기능을 구현할 있지만 간단하게 데이터를 조작해보겠습니다.

RefactFilter

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

        //길이만 초기화 , 전처리과정 내용을 담고 있지는 않다.
        ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest) request);
        ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse) response);

        //doFilter 를 통해 실제 Spring 내부로 들어가야 content 담긴다.
        //후처리는 항상 doFilter 이후에 처리 해야한다.

        String url = httpServletRequest.getRequestURI();
        String reqContent = new String(httpServletRequest.getContentAsByteArray());
        log.info("request url : {}, requestBody : {}", url, reqContent);

        chain.doFilter(httpServletRequest, httpServletResponse);

        User user = new User("After", "user@naver.com", 123123);
        String stringUser = mapper.writeValueAsString(user);
        httpServletResponse.reset();
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().write(stringUser);

        String resContent = new String(httpServletResponse.getContentAsByteArray());

        int httpStatusCode = httpServletResponse.getStatus();
        log.info("response status : {}, responseBody : {}", httpStatusCode, resContent);

        //ContentAsByte를 써서 리턴할 값을 다 뺀 상태이기 때문에 복사하여 반환해주어야한다.
        httpServletResponse.copyBodyToResponse();
    }

실행결과

com.example.filter.filter.GlobalFilter : request url : /api/user, requestBody :
c.e.filter.controller.ApiController : user : User(name=a1w1, email=aaaaa, age=10)
com.example.filter.filter.GlobalFilter : response status : 200, responseBody : {"name":"After","email":"user@naver.com","age":123123}

이전과 같은 request를 보낸것을 메서드에서 확인이 가능합니다. 후처리로 response값을 바꿔 보냈기 때문에 다른 값이 출력되는 것을 확인 할 수 있습니다.

만약 특정한 경우에만 지정하고 싶은 경우는 어떻게 해야 할까요?

@WebFilter(urlPatterns = "/api/") @Compoent를 제외하고 적용시키면 됩니다. 의미는 해당 URI로 호출되는 것들에 대해서만 Filter를 적용하겠다는 의미입니다. 특정한 URI에 해당되는 만큼 Application에 @ServletComponentScan 어노테이션을 사용합니다.

그렇다면 같은 조건의 필터가 만약 2개 이상이라면 작동이 어떻게 될까요?

TempFilter

@Slf4j
@WebFilter(urlPatterns = "/api/*")
public class TempFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
	...
}

해당 메서드는 값을 이전과 다르게 변경하지 않고 그대로 리턴하게 됩니다.

실행 결과

com.example.filter.filter.TempFilter : request url : /api/user, requestBody :
com.example.filter.filter.GlobalFilter : request url : /api/user, requestBody :
c.e.filter.controller.ApiController : user : User(name=a1w1, email=aaaaa, age=10)
com.example.filter.filter.GlobalFilter : response status : 200, responseBody : {"name":"After","email":"user@naver.com","age":123123}
com.example.filter.filter.TempFilter : response status : 200, responseBody : {"name":"After","email":"user@naver.com","age":123123}

범위를 지정한 TempFilter가 먼저 실행되고 끝나는 것을 알 수 있다. 전역적으로 지정한 것보다는 구체적인 범위의 것을 먼저 실행하는 것을 알 수 있습니다.

✅ 요약

  • Filter는 Spring에서 제공하는 기능으로 요청/응답에 최초/최후 단계에 위치합니다.
  • 유일하게 ServletRequest, ServletResponse의 객체 변환 가능하며 캐싱,데이터 조작, 유효성 검사등의 기능을 할 수 있습니다.
  • init,doFilter,destroy 메서드로 이루어져있으며 doFilter에서 전처리,후처리를 합니다.
  • @Component를 사용하여, 전역적으로 처리가 가능하며, Filter에 @WebFilter, Application에 @ServletComponentScan를 사용하여 부분적으로 사용할 수 있습니다.
profile
왜? 라는 질문이 사라질 때까지

0개의 댓글