서블릿 필터

김나쁜 Kimbad·2022년 11월 7일
0

프로젝트

목록 보기
6/9

서블릿 필터란?

서블릿 필터(Servlet Filter)란 Client로부터 Server로 요청이 들어오기전에 서블릿을 거쳐서 필터링하는 것을 서블릿 필터라고 한다.

Servlet과 거의 동일하지만 기능이 Filtering 기능을 하기 때문에 이렇게 이름이 지어졌다고 한다.

사용자 인증이나 로깅과 같은 기능들은 모든 서블릿이나 JSP가 공통적으로 필요로 한다.
이러한 공통적인 기능들을 서블릿이 호출되기 전에 수행(전처리)되게 하고 싶거나
서블릿이 호출 되고 난 뒤에 수행(후처리) 하고 싶으면 공통적인 기능들을 서블릿 필터로 구현하면 된다.

Filter로 구현하면 좋은 기능들

  • 인증 필터
  • 로깅 및 감시(Audit) 필터
  • 이미지 변환 및 데이터 압축 필터
  • 암호화 필터
  • XML 컨텐츠를 변형하면 XSLT 필터
  • URL 및 기타 정보들을 캐싱하는 필터

Filter 인터페이스

필터를 설정하는 FilterConfig 객체, FilterChain 객체와 Filter 객체가 필요하다.

이 중에서 FilterConfig 인터페이스랑 FilterChain 인터페이스를 상속받은 클래스들은 웹컨테이너가 구현해준다. Filter를 구현하는데 필요한 건 사용자 정의 필터 클래스는 javax.servlet.Filter 인터페이스를 구현한다. Filter 인터페이스는 init(), doFilter() , destroy() 메소드가 있다.

  • init(FilterConfig config)
    서블릿 컨테이너가 필터 인스턴스를 초기화 하기 위해서 호출하는 메서드
  • doFilter(ServletRequest res, ServletResponse res, FilterChain chain)
    필터에서 구현해야 하는 로직을 작성하는 메서드
  • destroy() : void
    필터 인스턴스를 종료시키기 전에 호출하는 메서드

Filter의 라이프 사이클

필터는 서블릿과 비슷한 라이프 사이클을 가지며, 생성, 초기화, 필터, 종료의 4단계로 이루어진다.
또한 서블릿 컨테이너는 필터 객체가 초기화 파라미터에 접근하는데 사용하는 환경설정 객체(FilterConfig)의 레퍼런스를 제공한다. 서블릿 컨테이너가 필터의 init() 메서드를 호출하면 필터 인터페이스는 바로 요청을 처리할 수 있는 상태가 된다.

서블릿이 service() 메서드를 이용해서 요청을 처리한 것 처럼 필터는 doFilter() 메서드를 통해서 요청을 처리한다. 모든 요청에 대한 처리가 끝나면 destroy() 메서드가 호출되면서 필터는 비활성 상태로 변경된다.

FilterChain

필터는 연속적인 작용을 수행한다. 필터 객체가 수행해야 할 부분인 doFilter() 메서드의 인자로 전달되는것이 FilterChain 객체다. FilterChain 객체는 필터의 수행과정을 연속적으로 하기 위한 방법이다. 웹 컨테이너가 FilterConfig 객체와 함께 FilterChain 인터페이스를 구현한 객체를 생성한다.

doFilter() 메서드

가장 핵심인 Filtering이 이루어지는 메서드이다.

public void doFilter(ServletRequest request, 
					ServletResponse response,
                    FilterChain chain) throws IOException, ServletException {
	// 전처리
    chain.doFilter(request, response);
    // 후처리
}

필터는 한번만 수행되지 않고, 요청을 받을때 수행 되고 chain.doFilter()를 통해 다음 부분으로 넘겨준다. 그 다음 모든 부분이 수행 되면 다시 필터로 완전한 응답 객체와 함께 제어권이 넘어오게 된다.

따라서 chain.doFilter()doFilter() 메서드 내에 없다면 서블릿의 수행 결과를 알 수 없다. 즉, chain.doFilter() 메서드를 사용하여 다음 단계인 진짜 서블릿을 수행한 후 결과를 다시 받는 것이다.

Request/Response 로깅 필터

Slf4j를 통해 이미 로깅을 하고 있지만, 좋아보이는 예제가 있어 사용해보았다.
요청/응답 및 헤더 등 각 정보를 로깅할 수 있는 필터로
API 호출, 응답 등 유용하게 사용할 수 있을 것 같다.

@Slf4j
@Component
public class RequestFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);

        long start = System.currentTimeMillis();
        chain.doFilter(requestWrapper, responseWrapper);
        long end = System.currentTimeMillis();

        log.info("\n" +
                        "{} - {} \n" +
                        "Status Code : {}, Access Time : {}\n" +
                        "Headers : {}\n" +
                        "Request : {}\n" +
                        "Response : {}\n",
                ((HttpServletRequest) request).getMethod(),
                ((HttpServletRequest) request).getRequestURI(),
                responseWrapper.getStatus(),
                (end - start) / 1000.0,
                getHeaders((HttpServletRequest) request),
                getRequestBody(requestWrapper),
                getResponseBody(responseWrapper));
    }

    private Map getHeaders(HttpServletRequest request) {
        Map headerMap = new HashMap<>();

        Enumeration headerArray = request.getHeaderNames();
        while (headerArray.hasMoreElements()) {
            String headerName = (String) headerArray.nextElement();
            headerMap.put(headerName, request.getHeader(headerName));
        }
        return headerMap;
    }

    private String getRequestBody(ContentCachingRequestWrapper request) {
        ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (wrapper != null) {
            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) {
                try {
                    return new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
                } catch (UnsupportedEncodingException e) {
                    return " - ";
                }
            }
        }
        return " - ";
    }

    private String getResponseBody(final HttpServletResponse response) throws IOException {
        String payload = null;
        ContentCachingResponseWrapper wrapper =
                WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) {
                payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
                wrapper.copyBodyToResponse();
            }
        }
        return null == payload ? " - " : payload;
    }

}

아래와 같이 로그가 남는다.

POST - /test/testById 
Status Code : 500, Access Time : 0.035
Headers : {content-length=29, host=localhost:8020, content-type=application/json, 
connection=keep-alive, cache-control=no-cache, accept-encoding=gzip, deflate,
br, user-agent=PostmanRuntime/7.29.2, accept=*/*}
Request : {"test_id" : "test33"}
Response : {"message":"Doesn't Exist.","code":"1001","status":"INTERNAL_SERVER_ERROR"}

다만 API 요청이 아닌, 페이지로 이동 시 Response단에 해당 페이지의 모든 내용(스크립트를 포함한)이 들어가게 되므로 이 부분은 약간 수정해주어야 겠다.

profile
Bad Language

0개의 댓글