filter

Byung Seon Kang·2022년 10월 10일
0

서블릿에 대해

목록 보기
8/9

웹 애플리케이션의 성능을 향상시키기 위해 응답시간, 유저 인터렉션 등등 체크가 필요해진다. 이것들을 어떻게 보고 관리해줄 수 있을까?

필터

  • 필터는 자바의 컴포넌트로, 서블릿과 매우 비슷하다.
    • servlet에 요청을 전달하기 전에, 그리고 서블릿에서 요청을 처리하고나서(하지만 클라이언트에게 응답이 전달되기 전) 필터가 인터셉트가 가능하다.
  • DD에서 어느 request URL pattern에서 어떤 필터를 작동시킬지 정의할 수 있다.
  • DD에서 필터의 순서도 설정이 가능.
    • 여기서 중요한 것은 필터 순서의 dependency는 존재하지만, 프로그래머가 코드의 dependency를 만들지는 않는다.
      • 필터 각각은 자신들의 순서를 모른다!
  • 참고
    servlet3.0이후 DD를 쓸 필요 없이 코드 상에서 설정이 가능하도록 업데이트를 했음(WebApplicationInitializer)

필터의 역할

Request

  • security check
  • request header나 body reformat
  • audit 또는 log

Response

  • response stream 압축
  • response stream의 교체 또는 부착
  • 다른 response 생성

LifeCycle

  • 필터도 서블릿처럼 lifecycle이 존재한다.
  • init(), doFilter(), destory()
    코드 예시
package com.example.filter;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;

import java.io.IOException;

public class BeerRequestFilter implements Filter {

    private FilterConfig fc;

    public void init(FilterConfig config) throws ServletException {
        this.fc = config;
    }

    public void doFilter(ServletRequest req, ServletResponse resp,
                            FilterChain chain) throws ServletException, IOException {

        HttpServletRequest httpReq = (HttpServletRequest) req;
        String name = httpReq.getRemoteUser();

        if(name != null){
            fc.getServletContext().log("User " + name + " is updating");
        }

        chain.doFilter(req, resp); //calling next filter
    }

    //You must implement destroy method but usually it's empty
    public void destroy(){
        //clean up
    }
}

init

  • filter 초기화 할때
    • filter가 불리기 전에 set-up해줌.
    • FilterConfig object의 refererence 저장한다던가 함.

doFilter

  • Container에서 filter가 현재 request에 적용되어야 한다고 판단하면 매번 호출.
    • 실제 필터의 기능을 구현하는 부분
  • doFilter에서 chain.doFilter(req,resp);가 보이는데 이건 뭘까
    • FilterChain의 doFilter()는 filter의 doFilter()와는 조금 다르다.
      • FilterChain의 doFilter는 다음 filter를 호출하거나, 다음 호출할 filter가 없으면 servlet의 service method를 호출하거나 함.
      • 즉, FilterChain은 filter 또는 servlet 둘 다 호출.

Destroy

  • Container가 filter instance를 삭제하기로 결정했을 때 destroy() method를 호출한다.

필터의 동작방식

  • servlet spec은 chain.doFilter(req,res)에 대해 컨테이너에서 어떻게 동작하고 있는지 서술하고 있지 않다.
    • 그럼에도 불구하고 filter chaining의 처리과정을 단일 스택에서의 method call처럼 생각해볼 수 있을 것.
      (그림추가)

Response Filter

  • response를 압축해서 client에게 전달한다고 해보자.
    아래와 같이 생각해볼 수 있을 것
    	public void doFilter(requeest, response, chain) {
      	//this is where request handling would go
          chain.doFilter(request, response);
          
          //압축 여기서
      }
  • 이렇게 생각하면 문제가 생긴다.
    • 이미 doFilter를 통해 서블릿에서 응답 작성하고 client에게 보낸 뒤에 어떻게 압축을 시켜서 전달하는가?
      • 너무 늦다.
  • 따라서 다른 방식이 필요

해결방안 : implement Response

  • HttpServletResponse의 custom implementation을 만들고 chain.doFilter()의 parameter로 custom된 객체를 전달시켜 응답으로 사용하도록 한다.
  • 여기서 Decorator pattern을 사용
    • wrapper로 감싸서 새로운 기능을 추가시켜준다.

좀 더 구체적으로

  • HttpServletResponse 객체를 그냥 implement하게 되면 상당히 많은 메소드를 구현해야 한다.
    • 그래서 어느정도 구현한 것들을 제공해줌.
      ServletRequestWrapper
      HttpServletRequestWrapper
      ServletResponseWrapper
      HttpServletResponseWrapper

코드 예시

압축 필터

package com.example.filter;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.zip.GZIPOutputStream;

public class CompressionFilter implements Filter {

    private ServletContext ctx;
    private FilterConfig cfg;

    public void init(FilterConfig cfg) throws ServletException {
        this.cfg = cfg;
        ctx = cfg.getServletContext();
        ctx.log(cfg.getFilterName() + " initialized.");
    }

    public void doFilter(ServletRequest req,
                         ServletResponse resp,
                         FilterChain fc) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;

        String valid_encodings = request.getHeader("Accept-Encoding");
        if ( valid_encodings.indexOf("gzip")>-1) {

            CompressionResponseWrapper wrappedResp = new CompressionResponseWrapper(response);
            wrappedResp.setHeader("Content-Encoding", "gzip");

            fc.doFilter(request, wrappedResp);

            GZIPOutputStream gzos = wrappedResp.getGZIPOutputStream();
            gzos.finish();

            ctx.log(cfg.getFilterName() + ": finished the request.");

        }else {
            ctx.log(cfg.getFilterName() + ": no encoding performed.");
            fc.doFilter(request, response);
        }
    }

    public void destory(){
        cfg = null;
        ctx = null;
    }
}

ResponseWrapper(압축 위한 response 커스텀)

package com.example.filter;

import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.zip.GZIPOutputStream;

public class CompressionResponseWrapper extends HttpServletResponseWrapper {

    private GZIPServletOutputStream servletGZipOs = null;

    private PrintWriter pw = null;

    private Object streamUsed = null;
    public CompressionResponseWrapper(HttpServletResponse response) {
        super(response);
    }
    public void setContentLength(int len) {}

    public GZIPOutputStream getGZIPOutputStream() {
        return this.servletGZipOs.internalGzipOS;
    }

    //access to a decorated servlet output stream.
    public ServletOutputStream getOutputStream() throws IOException {
        if((streamUsed != null) && (streamUsed != pw)) {
            throw new IllegalStateException();
        }

        if(servletGZipOs == null){
            servletGZipOs = new GZIPServletOutputStream(getResponse().getOutputStream());
            streamUsed = servletGZipOs;
        }

        return servletGZipOs;
    }

    public PrintWriter getWriter() throws IOException {

        if((streamUsed != null) && (streamUsed != servletGZipOs)){
            throw new IllegalStateException();
        }

        if( pw == null) {
            servletGZipOs
                    = new GZIPServletOutputStream(getResponse().getOutputStream());

            OutputStreamWriter outputStreamWriter
                    = new OutputStreamWriter(servletGZipOs,
                    getResponse().getCharacterEncoding());
            pw = new PrintWriter(outputStreamWriter);
            streamUsed = pw;
        }
        return pw;
    }


}

OutputStream custom

package com.example.filter;

import jakarta.servlet.ServletOutputStream;

import java.io.IOException;
import java.util.zip.GZIPOutputStream;

public class GZIPServletOutputStream extends ServletOutputStream {

    GZIPOutputStream internalGzipOS;

    //decorator constructor
    GZIPServletOutputStream(ServletOutputStream sos) throws IOException {
        this.internalGzipOS = new GZIPOutputStream(sos);
    }

    //write method implements compression decoration.
    //this method delegate write() call to the gZIP compression stream.
    //기존 ServletOutputStream을 wrapping
    //궁극적으로 TCP network output stream을 wrapping한 것.
    public void write(int param) throws IOException {
        internalGzipOS.write(param);
    }
}
profile
왜 필요한지 질문하기

0개의 댓글