이 방식은 자바 웹 앱에서 뷰 컴포넌트(JSP, HTML 등)의 관계를 추출하기 위해 사용될 수 있다. 추출할 수 있는 관계로는 대표적으로 Redirect, Forward, Include 관계가 있다. 해당 내용은 아래의 논문을 토대로 제작된 형태임을 밝힌다.
[안우현, 오재원, & 김태공. (2017). 자바 웹 앱에서 서블릿 필터와 래퍼를 이용한 컴포넌트 협력 과정 자동 추출 기법. 정보처리학회논문지. 소프트웨어 및 데이터 공학, 6(7), 329-336.]
먼저, 필터는 말 그대로 들어온 무언가를 여과하기 위해 있는 장치이다. 그래서 사용자의 요청에 의해 여러 웹 컴포넌트가 실행된다고 할 때, 컴포넌트가 실행되기 직전에 어떤 처리를 할 수 있다. 본 논문에서는 사용자가 미화를 입력하더라도 원화로 변경하고 싶을 때, 어떤 컴포넌트 X가 수행되기 이전에 입력 메시지 객체를 스캔하여 달러 단위의 숫자를 원화로 변경하는 작업을 필터에서 수행할 수 있다고 한다.
다음으로, 래퍼는 원본 입력 메시지 객체를 임의로 수정하여 새롭게 감싸는 것이다. 그래서 위와 같은 예시에서 원본 입력 메시지 객체에 담긴 달러를 원화로 수정하여 변경된 입력 메시지 래퍼 객체를 사용할 수 있다. 그러면 이후에 수행되는 모든 컴포넌트에서는 원화 값이 담긴 입력 메시지 객체를 받아서 어떠한 작업을 처리하게 된다.
위의 그림에서는 사전 작업을 하는 필터를 예시로 들고 있지만 후속 작업을 하는 필터도 존재한다. 기본 request가 들어왔을 때 사전 작업 필터에서 어떤 처리를 하고, request를 wrapper로 변경하여 WebComponent에 전달한다.
위의 내용을 바탕으로 자바에서는 Filter Class와 Wrapper Class를 이용하여 구현할 수 있다!!
글쓴이의 경우 Forward, Include 관계를 추출하고자 하였기에 4가지의 클래스를 생성하였다.
1. ForwardFilter
2. IncludeFilter
3. GenericFilter
4. RequestWrapper
5. LogWriter
그리고 마지막으로 이런 필터가 작동할 수 있도록, web.xml에서 필터를 특정 url에 매핑하였다.
간단하게 하나의 그림으로 나타내면 위의 그림처럼 나타낼 수 있다. 해당 과정은 LoginServlet에서 Forward가 이루어질 때의 예시이다. 로그인 버튼을 눌렀을 때 GenericFilter가 실행되고 Wrapper를 생성하면서 어떤 Servlet이 호출되는지 기록한다. 그리고 Wrapper를 Servlet에게 전달한다. Servlet은 어떤 로직을 처리하고 다시 다른 컴포넌트로 Forward한다. 이때, ForwardFilter가 수행되고 Wrapper 담겨있던 이전에 호출된 컴포넌트인 LoginServlet정보와 다음에 호출될 어떤 웹 컴포넌트 정보를 이용하여 로그를 찍는다.
GenericFilter에서는 LogStart와 LogEnd를 기록하여 하나의 요청에 대한 로그를 구분짓는다.
※ Include역시 동일하나 조금은 다른 점이 존재한다. 해당 내용과 관련해서는 아래에서 따로 설명하도록 한다.
public class RequestWrapper extends HttpServletRequestWrapper {
String includePath;
String forwardCaller;
public RequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public RequestDispatcher getRequestDispatcher(String path) {
this.includePath = path;
return super.getRequestDispatcher(path);
}
public String getIncludePath() {
return includePath;
}
public void setForwardCaller(String caller) {
this.forwardCaller = caller;
}
public String getForwardCaller() {
return this.forwardCaller;
}
}
includePath는 Include시 어떤 녀석을 Include했는지를 담는다. 이를 위해서 RequestDispatcher 메소드를 재정의하였다. Include를 할 때에는 컴포넌트에서 RequestDispatcher 메소드가 실행되는데 해당 메소드를 재정의하여서 includePath에 해당 패스를 저장하고 실제 RequestDispatcher가 실행되도록 해놓았다. 누가 나를 Forward로 호출하였는지를 가지고 있는 forwardCaller 변수가 존재하고 해당 변수에 대한 setter와 getter가 존재한다.
public class GenericFilter implements Filter {
LogWriter lw;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
lw = new LogWriter();
lw.StartLog();
HttpServletRequest httpReq = (HttpServletRequest) servletRequest;
//만든 wrapper을 넘겨준다.
RequestWrapper requestWrapper = new RequestWrapper((HttpServletRequest)servletRequest);
requestWrapper.setForwardCaller(httpReq.getServletPath());
filterChain.doFilter(requestWrapper, servletResponse);
lw.EndLog();
}
@Override
public void destroy() {
}
}
모든 컴포넌트가 수행되기 이전에 실행되는 기본적인 필터이다. 해당 필터에서는 하나의 리퀘스트에 대한 싸이클을 표시하기 위해 로그에서 start와 end를 기록한다. 해당 필터가 실행될 때 wrapper를 생성하고 해당 wrapper에 getServletPath()를 통해 Forward를 호출한 녀석을 담는다. 필터체인이 모두 완료되었을때, 리퀘스트에 대한 싸이클이 종료됨을 파악하고 로그를 마친다.
여기서 filterChain은 다음 필터가 존재한다면 해당 필터로 request를 넘겨주는 코드이다.
public class ForwardFilter implements Filter{
String dispatcherType = "Forward";
LogWriter lw;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
lw = new LogWriter();
//전처리
HttpServletRequest httpReq = (HttpServletRequest) servletRequest;
RequestWrapper rw = (RequestWrapper)servletRequest;
String startPoint = rw.getForwardCaller();
String destination = httpReq.getServletPath();
lw.LogWrite("Start", startPoint, destination, dispatcherType);
filterChain.doFilter(servletRequest, servletResponse);
lw.LogWrite("End", startPoint, destination, dispatcherType);
//후처리
}
@Override
public void destroy() {
}
}
httpReq에 ServletRequest를 담고 RequestWrapper에 request에 담겨있는 래퍼 객체를 가지고 온다. 래퍼 객체는 해당 필터가 실행되기 이전에 어떤 컴포넌트가 수행되었는지를 담고 있다. 그리고 destination에 현재 어떤 컴포넌트에 있는지를 담는다. 그리고 해당 내용을 로그에 남긴다.
public class IncludeFilter implements Filter {
String dispatcherType = "Include";
LogWriter lw;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
lw = new LogWriter();
//전처리
HttpServletRequest httpReq = (HttpServletRequest) servletRequest;
RequestWrapper rw = (RequestWrapper)servletRequest;
String startPoint = httpReq.getServletPath();
String destination = rw.getIncludePath();
lw.LogWrite("Start", startPoint, destination, dispatcherType);
filterChain.doFilter(servletRequest, servletResponse);
//후처리
lw.LogWrite("End", startPoint, destination, dispatcherType);
}
@Override
public void destroy() {
}
}
ForwardFilter와 동일하나 getForwardCaller를 호출하는 것이 아니라 getIncludePath 메소드를 호출한다.
public class LogWriter {
public File txtFile;
public FileWriter jspLogWriter = null;
public LogWriter() {
this.txtFile = new File("D:\\filterDetectedJsp.txt");
//change your dir
}
synchronized public void StartLog() {
String msg = "<Trace Start>\n\n";
try {
jspLogWriter = new FileWriter(txtFile, true);
jspLogWriter.write(msg);
jspLogWriter.flush();
jspLogWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
synchronized public void EndLog() {
String msg = "<Trace End>\n\n";
try {
jspLogWriter = new FileWriter(txtFile, true);
jspLogWriter.write(msg);
jspLogWriter.flush();
jspLogWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
synchronized public void LogWrite(String des,String startPoint, String Destination, String dispathcerType) {
String msg = des + " : " + startPoint + " -> " + Destination + "[ " + dispathcerType + " ]\n";
try {
jspLogWriter = new FileWriter(txtFile, true);
jspLogWriter.write(msg);
jspLogWriter.flush();
jspLogWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
기본적으로 로그를 기록하는 클래스이다..
<filter>
<filter-name>GenericFilter</filter-name>
<filter-class>filter.GenericFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>GenericFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>ForwardFilter</filter-name>
<filter-class>filter.ForwardFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ForwardFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
<filter>
<filter-name>IncludeFilter</filter-name>
<filter-class>filter.IncludeFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>IncludeFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>INCLUDE</dispatcher>
</filter-mapping>
다음과 같이 필터 매핑을 해준다. 모든 url에 매핑될 수 있도록 /*로 설정한다.
본 논문에서는 다음과 같이 설명한다.
Forward의 경우 Java API(HttpServletRequest의 getServletPath())를 이용하여 사전 필터 이후에 실행할 웹 컴포넌트를 식별할 수 있다. 하지만 Include의 경우에는 똑같은 방식으로 컴포넌트를 식별하게 되면 include되는 컴포넌트가 실별되는 것이 아니라 include하는 컴포넌트가 식별된다. 그래서 include 호출이 실행될 때 include 대상을 지정하는 Java API(Servlet Request의 getRequestDispatcher())가 존재하는 것을 이용하게 된다.
이러한 특성 때문에 Forward와 Include의 필터를 달리 사용한다.
Redirect의 경우 Java API(ServletResponse의 sendRedirect())를 래퍼에서 재정의하면 식별가능하다.
차례대로 Include와 Forward 로그 예시이다.
논문을 이해하고 바로 기술을 적용시키는데에는 굉장히 어려움이 있었다. 랩을 다니면서 처음으로 실험에 관련한 프로그래밍을 진행한거고, 또 성공적으로 잘 수행되서 좋았다. 실제로 실험을 진행할 때 내가 만든 필터 코드를 이용해서 웹 컴포넌트 간의 관계를 얻기도 하여서 뿌듯하였다!!