Filter

Filter : Web Application에서 관리되는 영역, Spring Boot Framework에서 Client로 오는 요청/ 응답에 대하여 최초/최종단계의 위치에 존재하며, 이를 통해서 요청/응답의 정보를 변경하거나 Spring에 의해 데이터가 변환되기 전의 순수한 Client의 요청/응답 값을 확인 할 수 있다.

유일하게 ServletRequest, ServletResponse의 객체를 변환 할 수 있다.

주로 request/response의 Loggin용도로 활용하거나, 인증과 관련된 Logic들을 해당 Filter에서 처리한다.

Filter의 전역적 사용

Filter 인터페이스를 상속받은 GlobalFilter를 하나 만들자.

@Slf4j
@Component
public class GlobalFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //전처리
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        String url = httpServletRequest.getRequestURI();
        BufferedReader br = httpServletRequest.getReader();

        br.lines().forEach(line -> {
            log.info("url : {} , line : {}", url, line);
        });

        chain.doFilter(request, response); // 기준

        //후처리

    }
}

chain.doFilter()를 기준으로 그 이전에 작성한 코드는 전처리, 그 이후에 작성한 코드는 후처리로 적용 된다. 이전에 배웠던 AOP의 @Arround를 활용할때와 비슷한 것 같다.

일단 전처리쪽 코드만 작성해서 실행하고 요청을 보내면, 일단 에러가 발생한다…!
일단 로그찍힌것을 보면 json이 어떻게 들어오는지 확인할 수 있다.

그 이유는 BufferedReader의 특성때문인데, 가운데 있는 br.lines()를 돌면서 전부 읽어버려서 커서가 맨 끝으로 가 있는데 그상태에서 스프링 body를 읽으려고 했지만 읽을 수 없는 상태가 되어 버렸기 때문이다.

이를 방지하기 위해서 다양한 방법이 있었는데, 스프링에서는 다음과 같은 방법을 제공한다.

@Slf4j
@Component
public class GlobalFilter implements Filter {

    @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);

        String url = httpServletRequest.getRequestURI();

        /** 기준 */
        chain.doFilter(request, response);

        //후처리
        //req
        String reqContent = String.valueOf(httpServletRequest.getContentAsByteArray());
        log.info("request url : {}, request body : {}", url, reqContent);

        //res
        String resContent = String.valueOf(httpServletResponse.getContentAsByteArray());
        int httpStatusCode = httpServletResponse.getStatus();

        httpServletResponse.copyBodyToResponse(); // body를 한번 읽어버렸기 때문에 복사해서 넘겨줘야한다.

        log.info("response status : {}, response body : {}", httpStatusCode, resContent)
    }
}

ContentCachingRequestWrapper, ContentCachingResponseWrapper라는 타입형을 제공하여 요청의 내용을 모두 읽어도 계속해서 읽을 수 있도록 해준다. 원리는 요청내용을 cache하여 다시 읽을 수 있도록 한다.

ContentCachingWrapper 특성상 chain.doFilter 후에 내용이 적히고 그 전에는 길이만 설정되기 때문에 chain.doFilter 후에 값을 받아서 로그를 찍어야한다.

그리고 마지막에 response를 주기 전에도 body를 이미 읽어버려서, copyBodyToResponse메소드를 통해 body를 다시 복사해서 응답을 내려줘야 한다. 그렇지 않으면 클라이언트에서 빈 body를 받게된다.

Filter의 지역적 사용

특정 리퀘스트에 대해서만 필터를 걸려고 할 때에, 두가지 정도의 방법이 있는데 그 중 쉬운 방법을 소개하고 있 다.

일단 springBootApplication클래스에 가서 @ServletComponentScan 이라는 어노테이션을 붙여준다.

@SpringBootApplication
@ServletComponentScan //필터 지역적 사용을 위해 붙여줌
public class FilterAndInterceptorApplication {
    public static void main(String[] args) {
        SpringApplication.run(FilterAndInterceptorApplication.class, args);
    }
}

그리고 GlobalFilter를 스프링이 관리하기 위해 붙여주었던 어노테이션인 @Component를 지우고, @WebFilter라는 어노테이션으로 바꿔준다.

@Slf4j
//@Component
@WebFilter(urlPatterns = "/api/user/**")
public class GlobalFilter implements Filter {
  //생략
}

그러면 WebFilter어노테이션에서 지정한 urlPattern 에 해당하는 url에 대해서만 필터를 적용하게 된다.