JSP/Servlet - 필터, 리스너!

Filter 필터

요청과 응답데이터를 필터링하여 제어, 변경하는 역할을 하는 것을 필터라 한다.

요청에서 로그인 체크, 요청명령을 확인해 요청 경로를 제어하고
응답에서 필터를 통해 암호화 작업, 로그처리 등을 할 수 있다.

image28

보통 필터를 사용해 처리하는 작업은 다음과 같다.

  • 사용자 인증
  • 캐싱 필터
  • 자원접근에 대한 로그처리
  • 응답 데이터 변환 (HTML변환, 응답헤더 변환, 데이터 암호화 등)
  • 공통 기능 수행 (Context Path설정, Download경로설정, Encoding설정 등)

필터의 핵심 클래스

  1. javax.servlet.Filter 인터페이스
    클라이언트와 최종 자원 사이에 위치하는 필터를 나타내는 객체가 구현해야 하는 인터페이스

  2. javax.servlet.ServletRequestWrapper 클래스
    필터가 요청을 변경한 결과를 저장하는 래퍼

  3. javax.servlet.ServletResponseWrapper 클래스
    필터가 응답을 변경하기 위해 사용하는 저장하는 래퍼

필터 역할을 3가지 타입을 잘 알아야 한다.

Filter를 구현함으로써 필터의 기능을 수행하고 ServletRequestWrapper, ServletResponseWrapper을 통해 요청, 응답 데이터를 변경할 수 있다.

1. Filter 인터페이스

Filter에서 구현해야 하는 메서드는 총 3가지이다.

  1. public void init(FilterConfig filterConfig) throws ServletException
    필터 초기화시 호출.

  2. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws java.io.IOException, ServletException
    필터가 기능을 수행하는 메서드, 요청을 변경, 제어할 수 있고 다음 필터로 처리를 전담 가능하다.

  3. public void destroy()
    필터가 웹 콘테이너에서 삭제될 때 호출된다.

필터는 웹 서버가 시작 시 init()메서드가 호출되기 때문에 초기 설정(인코딩 타입, 암호화 알고리즘 등)을 등록하기 좋다.

doFilter()메서드에선 실질적인 필터의 기능을 수행한다.

필터클래스를 구현하는 객체 형태는 다음과 같다.

public class MyFirstFilter implements Filter{

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		//필터 초기화 작업
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		// 1. request 파라미터를 이용한 요청 필터 작업 수행

		// 2. 체인의 다음 필터 처리
		chain.doFilter(request, response);
		
		// 3. reponse 를 이용한 요청 필터링 작업 수행
	}

	@Override
	public void destroy() {
		// 주로 필터가 사용한 자원 반납
	}
}

수상한 점은 매개변수로 받은 FilterChain chainchain.doFilter(request, response)메서드…

사실 필터는 그림처럼 여러개 올 수 있으며 이를 필터 체인이라 한다.

image29

chain객체를 통해 web.xml에 설정된 filter-mapping에 걸린 필터들을 엮는다.

web.xml에 필터 매핑

서블릿을 url과 패밍할 때 클라이언트가 특정 url를 서버에 요청하면 서버는 web.xml에 먼저 가서 url-mapping에 따라 서블릿 객체를 응답했다.

필터 매핑도 서블릿 매핑과 똑같다.

web.xml에서 url에 해당하는 필터 객체를 매핑하고 요청필터를 거친후 서블릿 객체에 연결된다.
연결된 서블릿 객체는 포워딩하거나 리다이렉트 시킬것이고 이 응답 데이터는 응답필터를 다시 거쳐 클라이언트에게 반환된다.

chain.doFilter이전의 코드가 요청필터 역할을 하고
chain.doFilter이후의 코드가 응답필터 역학을 하게 된다.

클라이언트가 요청한 데이터를 초기설정한 encoding타입으로 받고 html페이지를 만들어 주도록 하는 필터를 제작하자.
먼저 web.xml에서 필터와 url을 매핑시켜주자.

<filter>
	<filter-name>encodingFile</filter-name>
	<filter-class>com.filter.CharacterEncodingFilter</filter-class>
	<init-param>
		<param-name>encoding</param-name>
		<param-value>utf-8</param-value>
	</init-param>
</filter>
<filter-mapping>
	<filter-name>encodingFile</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

/* 모든 요청은 응답 서블릿으로 가기전에 이 필터를 거쳐가게 된다.
CharacterEncodingFilter이라는 필터객체는 init-param태그에 설정된 타입으로 request객체가 디코딩 할 수 있게, resoonse객체가 인코딩 할 수 있게 도와주는 필터역할을 해줄 것 이다.

public class CharacterEncodingFilter implements Filter{
	private String encoding;
	@Override
	public void destroy() {
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		System.out.println("CharterEncodingFilter doFilter");
		request.setCharacterEncoding(encoding);
		response.setContentType("text/html; charset="+encoding); 
		chain.doFilter(request, response);
	}

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		encoding = filterConfig.getInitParameter("encoding");
		if(encoding == null)
			encoding = "utf-8";
	}
}

init()FilterConfig객체로 web.xml에서 설정한 정보를 가져올 수 있다.

매소드 리턴타입 설명
getFilterName() String web.xml에 등록한 필터명을 리넡하는 메서드
getInitParameter(String name) String web.xml필터 설정시 넘겨주는 초기화 설정값 <init-pram>의 값을 반환, 없다면 null을 반환하는 메서드
getInitParameterNames() Enumeration<String> web.xml필터 설정시 넘겨주는 초기화 설정값을 Enumeration으로 반환하는 메서드
getServletContext() ServletContext 필터가 속해있는 웹 어플리케이션의 ServletContext(application객체)를 반환하는 메서드

annotation을 사용한 필터 매핑

필터매핑을 web.xml이 아닌 어노테이션으로 url패턴과 매핑 가능하다.

매핑객체는 수동으로 implements Filter을 통해 오버라이딩 해도 되지만 이클립스에서 우클릭 new > Filter 클릭으로 추가 가능하다.

image30

image31

Filter mapping을 설정하면 아래와 같은 어노테이션이 추가된다.

@WebFilter(dispatcherTypes= {DispatcherType.REQUEST}, { "/TestFilter", "/Test/*" })

@WebFilter어노테이션에 dispatcherTypes속성을 추가하였는데 뜻은 아래와 같다.

dispatcherType이 가질 수 있는 값은 REQUEST, INCLUDE, FORWARD, ERROR 가 있는데 다음 상황에서 필터를 적용하라는 의미이다.

REQUEST : url을 통해 들어올 경우.
INCLUDE : include() 를 통해(<jsp:include ..>) 를 통해 들어올 경우.
FORWARD : forward() 를 통해(<jsp:forward ..>) 를 통해 들어올 경우.
ERROR : <%@ page errorPage="..." %>를 통해 에러페이지로 이동할 경우.

물론 어노데이션으로만 설정할수 있는 정보가 아니다.
web.xml에서 <dispatcher>REQUEST</dispatcher>, <dispatcher>FORWARD</dispatcher>와 같이 dispatcher태그로 설정 가능하다.

필터 순서는 web.xml에 정의한 순서이며 안타깝게도 어노테이션으로 매핑할 경우 순서를 지정하지 못한다. 위에서 보았던 필터 체인 그림 순서처럼 응답 필터는 역순으로 일어난다.

필터를 통해 url매핑이 아닌 서블릿 명을 통해 요청, 응답을 필터링 할 수 도 있다. url범위가 너무 넓어 특정 서블릿만 선택하기 힘들다면 서블릿 명을 통해 필터링 하도록 하자.
어노테이션에선 servletNames으로, web.xml에선 <servlet-name>태그로 설정 가능하다.

2. ServletRequestWrapper, ServletResponseWrapper 클래스

필터의 역할 중 제일 중요한 것이 요청데이터를 변경해서 서블릿 객체에 넘겨주고
서블릿의 응답데이터를 변경해서 클라이언트에게 넘겨주는 것이다.

이런 변경 요청 데이터, 응답데이터를 변경하려면 ServletRequestWrapper, ServletResponseWrapper사용이 필수이다.

예로 클라이언트가 xml페이지를 요청하면
필터를 통해 xml페이지를 html페이지로 변경시켜 반환하도록 하자.

xml Extensible Markup Language 라는 태그를 쓰는 언어중 하나이다. html DOM객체처럼 Tree형태의 구조를 가지고 있다.

먼저 xml페이지를 하나 작성하자.

<?xml version="1.0" encoding="UTF-8"?>
<%@ page language="java" contentType="text/xml; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page trimDirectiveWhitespaces="true" %>
<list>
	<book>
		<title>스프링4 프로그래밍 입문</title>
		<author>최범균</author>
		<price>25,000</price>
	</book>
	<book>
		<title>객체 지향과 디자인 패턴</title>
		<author>최범균</author>
		<price>25,000</price>
	</book>
</list>

처음 쓰는 xml이다….

<?xml...>태그가 가장 첫 행으로 와야하고 디렉티브의 contentType에서도 해당 페이지가 xml임을 알려준다.

그리고 사용자가 직접 만든 <list>, <book>, <title> 등의 태그들이 존재한다.

웹 브라우저에서 xml을 읽으면 아래와 같이 출력된다.

image32

그냥 생 문자열로 출력된다….

이 xml데이터를 필터객체를 통해 아래처럼 html형식의 데이터로 반환하도록 response객체 안의 데이터를 변환해보자.

image33

먼저 xml을 html로 변경시켜주는 xsl이란 파일이 필요하다.

xml을 원하는 형식대로 출력하는데 도움을 주는 파일이다.

XSLT(Extensible Stylesheet Language Transformations)는 XML 문서를 다른 XML 문서로 변환하는데 사용하는 XML 기반 언어이다.(위키)

<!-- book.xsl -->
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
	<xsl:output method="html" indent="yes" encoding="utf-8"/>
	<xsl:template match="list">
		<!-- TODO: Auto-generated template -->
	<html>
		<head><title>책목록</title></head>
		<body>
			현재 등록되어 있는 책 목록은 다음과 같습니다.
			<ul>
				<xsl:for-each select="book">
				<li>
					<b><xsl:value-of select="title"/></b>
					(<xsl:value-of select="price"/>원)
					<br/>
					<i><xsl:value-of select="author"/></i>
				</li>
				</xsl:for-each>
			</ul>
		</body>
	</html>	
	</xsl:template>
</xsl:stylesheet>

xsl문법은 모르지만 적당히 추측 가능하다.

xsl에도 for문역할을 하는 제어문 <xsl:for-each>태그가 있으며
<xsl:value-of>태그는 xml안의 태그의 데이터를 가져오는 기능을 한다.

xsl:output태그의 indent는 들여쓰기, encoding은 html로 만들때 사용하는 인코딩타입, xsl:template match="list" 는 최상위 태그인 list를 찾는 역할을 한다.
match를 통해 범위를 최상위로, 줄일수 도 있다.

중간중간의 들어간 html태그들로 보건데 xsl은 xml데이터를 html화 시키는 작업을 해주는 역할임을 알 수 있다.

xml파일과 xml파일을 html화 시켜주는 xsl파일을 만들었으니

클라이언트가 xml파일을 요청하면 응답을 도중에 가로채 xsl파일을 거쳐 html화 시킨후 이 데이터를 클라이언트에게 반환하는 필터객체를 만들면 된다.

응답데이터를 변조시키려면 response객체안의 데이터를 변조해야 하고
response안의 데이터를 변조시키려면 response를 감쌀 수 있는 ServletResponseWrapper를 상속받은 클래스가 필요하다.

public class XSLTResponseWrapper extends HttpServletResponseWrapper{
	private ResponseBufferWriter buffur = null;

	public XSLTResponseWrapper(HttpServletResponse response) {
		super(response);
		buffur  = new ResponseBufferWriter();
	}

	@Override
	public PrintWriter getWriter() throws IOException {
		return buffur;
	}

	@Override
	public void setContentType(String type) {
	}
	
	public String getBufferedString()
	{
		return buffur.toString();
	}
}

response안의 데이터를 감싸줄 XSLTResponseWrapper가 상속한 클래스가 그냥 ServletResponseWrapper가 아닌 HttpServletResponseWrapper클래스이다.

HttpServletResponseWrapperServletResponseWrapper클래스를 상속한 클래스로 http프로토콜의 응답데이터를 감싸는 역할을 하는 클래스이다.

솔직히 Web에서 http프로토콜이 절대 다수를 차지하기 때문에 요청, 응답 데이터를 감싸는 역할로 대부분 HttpServletResponseWrapper, HttpServletRequestWrapper를 사용한다.

위에서 가장 중요한 코드는 오버라이딩된 getWriter()메서드이 이다.

@Override
public PrintWriter getWriter() throws IOException {
	return buffur;
}

XSLTResponseWrapper로 만들어지는 객체는 reponse객체로 만들어지는 객체이다.

그런데 out객체를 반환하는 getWriter()를 오버라이딩 해버리면 서블릿 객체는 responsegetWriter()메서드로 out객체를 얻어 데이터를 쓰지 않고
XSLTResponseWrapper의 오버라이딩된 getWriter()메서드로 ResponseBufferWriter 객체를 얻어 사용하게 된다.

즉 출력버퍼가 바꿔치기 되어버린다.

어쩃건 생성자로 response객체를 받고 responseout객체 대신 데이터를 담을 ResponseBufferWriter를 인스턴스화 한다.

ResponseBufferWriterPrintWriter클래스를 상속한 클래스이다.

public class ResponseBufferWriter extends PrintWriter{

	public ResponseBufferWriter() {
		//print, write 등의 메서드를 통해 전달된 데이터를 StringWriter에 저장,
		super(new StringWriter(4096));
	}
	public String toString()
	{
		//StringWriter에 저장된 데이터를 toString으로 출력할 수 있음
		return ((StringWriter) super.out).toString();
	}
}

PrintWriter는 우리가 지금까지 자주쓰던 System.out객체의 타입, 또 jsp의 기본객체인 out객체의 타입이다.

서블릿에선 response.getWriter() 메서드를 통해 얻을 수 있다.

https://kouzie.github.io/jsp/JSP-시작/#servlet-개요

왜 굳이 PrintWriter를 바로 생성해서 쓰지 않고 ResponseBufferWriter로 상속받아 사용하냐! 라고 묻는다면
PrintWriter의 버퍼역할을 하는 out객체가 protected이기 때문

자바 개발자가 그렇게 만들어 놓았다…. PrintWriter를 생으로 쓰지 말고 목적이 분명한 클래스를 정의하고 사용하도록 설계한듯 하다…(약간 인터페이스 기능을 접근제어자로 구현한 느낌이다)

어쨋건 ResponseBufferWriter객체는 response의 출력버퍼 대신에 사용되는 버퍼이며, toString()으로 버퍼안의 내용을 출력할 수 도 있다.

responsegetWriter를 오버라이딩해 response의 출력버퍼가 아닌 ResponseBufferWriter로 바꿔치기 하기 위해 만들어진 객체가 XSLTResponseWrapper이다.

response객체 대용품을 정의했으니 정품대신 대용품에 데이터를 write하는 필터를 작성해보자.

@WebFilter(filterName = "xsltFilter", urlPatterns= {"/days11/xml/*"})
public class XSLTFilter implements Filter{
	private String xslPath = null;

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		xslPath = filterConfig.getServletContext().getRealPath("/days11/xsl/book.xsl");
		System.out.println(xslPath); 
	}
	@Override
	public void destroy() {
	}
	...
	...

먼저 어노테이션을 설정하고, init()메서드를 정의하자.

xml폴더안의 모든 파일을 요청하는 url을 필터을 통해 가도록 매핑한다.

그리고 위에서 설정했던 book.xsl파일의 시스템 경로를 xslPath필드에 저장한다.

	...
	...
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		PrintWriter writer = response.getWriter();
		XSLTResponseWrapper responseWrapper = new XSLTResponseWrapper((HttpServletResponse)response);
		//response의 출력버퍼 백업, response대신 응답데이터를 받을 XSLTResponseWrapper 객체 생성

		chain.doFilter(request, responseWrapper);
		//응답데이터를 XSLTResponseWrapper가 대신 받았다!

		try {
			Reader xslReader = new BufferedReader(new FileReader(xslPath));
			StreamSource xslSource = new StreamSource(xslReader);
			//xsl파일을 읽어 xslSource라는 StreamSource객체 생성

			String xmlDocument = responseWrapper.getBufferedString();
			Reader xmlReader = new StringReader(xmlDocument);
			StreamSource xmlSource = new StreamSource(xmlReader);
			//응답데이터를 대신 받은 responseWrapper의 버퍼내용을 String으로 추출
			//읽어온 응답데이터를 사용해 xmlSource라는 StreamSource객체 생성
			
			StringWriter buffer = new StringWriter(4096);
			//변환 데이터가 들어갈 임시버퍼 생성

			TransformerFactory factory = TransformerFactory.newInstance();
			Transformer transformer = factory.newTransformer(xslSource);
			transformer.transform(xmlSource, new StreamResult(buffer));
			//변환작업을 거친후 buffer에 저장한다.  
			
			writer.println(buffer.toString());
			//기존 response객체의 출력버퍼 out객체에 임시버퍼의 정보를 출력.
		} catch (Exception e) {
			throw new ServletException(e);
		}
		writer.flush();
		writer.close();
		//백업해 놓은 출력버퍼 반납
	}
}

xml,xsl도 처음보고 xsl을 사용해 xml을 변환시키는 작업을 java코딩으로 해보는것도 처음이다.
Transformer, StreamSource같은 객체가 매우 낯설게 느껴진다.

그냥 xml파일을 파싱하는 메뉴얼이라 가볍개 생객하도록 하자.

간단히 말하면 response 대타용 객체 responseWrapper를 만들고 이녀석을 서블릿에게 대신 보낸다.
responseWrapper은 서블릿으로부터 응답데이터를 받아서 xmlSource라는 StreamSource객체를 만든다.
transformer는 응답데이터 xmlSource와 기존에 가지고 있던 xslSource을 사용해 xml을 html로 변환후 임시버퍼에 저장한다.
기존 reponse객체의 출력버퍼에 임시버퍼에 저장해 두었던 html데이터를 옮긴다.

끝!

ServletContextListener 인터페이스

보통 어떤 이벤트를 처리하는 객체가 대부분 Listener란 이름을 가지고 있었다.

ServletContextListener인터페이스를 구현한 클래스는 웹 컨테이너가 구동될 때 특정 작업을 하는 클래스이다.

지금까지 load-on-startup을 통해 시작과 동시에 객체를 만들었었는데 ServletContextListener만 구현하면 똑같은 기능을 한다.
게다가 서버가 종료될 때 수행하는 작업을 ServletContextListener의 가상메서드 contextDestroyed 에서 설정 가능함으로 상위 호환이라 할 수 있다.

예를들어 사이트의 총 방문자 수 를 구하고 싶다면 session이 생성되때 count +1하고 session이 제거될때 count -1 하고
이 데이터는 텍스트 파일로 유지하며 서버가 시작할 때 텍스트에서 읽어오고, 서버가 종료될 때 텍스트파일에 저장해 놓으면 된다.

ServletContextListener에서 구현해야 할 메서드는 2개이다.

public class DBCPInitListener implements ServletContextListener {

	@Override
	public void contextDestroyed(ServletContextEvent arg0) {
		System.out.println("DBCP contextDestroyed()....");
	}

	@Override
	public void contextInitialized(ServletContextEvent arg0) {
		System.out.println("DBCP contextinitialized()....");
		ServletContext application = sce.getServletContext();
	}
}

물론 web.xml에서도 서버 시작, 종료시 호출해야 하는 ServletContextListener구현한 클래스임을 알려야 한다.

<!-- web.xml -->
<listener>
	<listener-class>com.util.DBCPInitListener</listener-class>
</listener>
<context-param>
	<param-name>poolConfig</param-name>
	<param-value>
		className=oracle.jdbc.driver.OracleDriver
		url=jdbc:oracle:thin:@172.17.107.68:1521:xe
		user=scott
		password=tiger
	</param-value>
</context-param>

보통 이런 DB연결작업들을 시작과 동시에 많이 한다.
(우리는 톰켓이 제공하는 DBCP를 사용해서 태그를 통해 가져오기 때문에 상관X)

만약 서버시작 시 모든 서블릿 객체가 공유해야 하는 데이터가 있다면 <context-param>태그를 사용하거나 applicationsetAttribute()를 사용하도록 하자.

어노테이션을 사용한 리스너 등록

필터와 같이 리스터도 어노테이션을 통해 설정 가능하다.

@WebListener
public class DBCPInitListener implements ServletContextListener {
	...
	...
}

@WebListener어노테이션 하나면 끝난다.

web.xml에서 귀찮게 <listener>태그를 사용해 리스너 등록할 필요 없다.

리스터의 순서역시 web.xml에 등록된 수서이며 특이하게도 contextDestroyed()메서드는 등록한 역순으로 실행된다.

카테고리:

업데이트: