JSP/Servlet - 필터, 리스너!
Filter 필터
요청과 응답데이터를 필터링하여 제어, 변경하는 역할을 하는 것을 필터라 한다.
요청에서 로그인 체크, 요청명령을 확인해 요청 경로를 제어하고
응답에서 필터를 통해 암호화 작업, 로그처리 등을 할 수 있다.
보통 필터를 사용해 처리하는 작업은 다음과 같다.
- 사용자 인증
- 캐싱 필터
- 자원접근에 대한 로그처리
- 응답 데이터 변환 (HTML변환, 응답헤더 변환, 데이터 암호화 등)
- 공통 기능 수행 (Context Path설정, Download경로설정, Encoding설정 등)
필터의 핵심 클래스
-
javax.servlet.Filter
인터페이스
클라이언트와 최종 자원 사이에 위치하는 필터를 나타내는 객체가 구현해야 하는 인터페이스 -
javax.servlet.ServletRequestWrapper
클래스
필터가 요청을 변경한 결과를 저장하는 래퍼 -
javax.servlet.ServletResponseWrapper
클래스
필터가 응답을 변경하기 위해 사용하는 저장하는 래퍼
필터 역할을 3가지 타입을 잘 알아야 한다.
Filter
를 구현함으로써 필터의 기능을 수행하고 ServletRequestWrapper
, ServletResponseWrapper
을 통해 요청, 응답 데이터를 변경할 수 있다.
1. Filter 인터페이스
Filter
에서 구현해야 하는 메서드는 총 3가지이다.
-
public void init(FilterConfig filterConfig) throws ServletException
필터 초기화시 호출. -
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws java.io.IOException, ServletException
필터가 기능을 수행하는 메서드, 요청을 변경, 제어할 수 있고 다음 필터로 처리를 전담 가능하다. -
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 chain
와 chain.doFilter(request, response)
메서드…
사실 필터는 그림처럼 여러개 올 수 있으며 이를 필터 체인이라 한다.
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
클릭으로 추가 가능하다.
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을 읽으면 아래와 같이 출력된다.
그냥 생 문자열로 출력된다….
이 xml데이터를 필터객체를 통해 아래처럼 html형식의 데이터로 반환하도록 response
객체 안의 데이터를 변환해보자.
먼저 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
클래스이다.
HttpServletResponseWrapper
은 ServletResponseWrapper
클래스를 상속한 클래스로 http
프로토콜의 응답데이터를 감싸는 역할을 하는 클래스이다.
솔직히 Web에서 http프로토콜이 절대 다수를 차지하기 때문에 요청, 응답 데이터를 감싸는 역할로 대부분 HttpServletResponseWrapper
, HttpServletRequestWrapper
를 사용한다.
위에서 가장 중요한 코드는 오버라이딩된 getWriter()
메서드이 이다.
@Override
public PrintWriter getWriter() throws IOException {
return buffur;
}
XSLTResponseWrapper
로 만들어지는 객체는 reponse
객체로 만들어지는 객체이다.
그런데 out
객체를 반환하는 getWriter()
를 오버라이딩 해버리면 서블릿 객체는 response
의 getWriter()
메서드로 out
객체를 얻어 데이터를 쓰지 않고
XSLTResponseWrapper
의 오버라이딩된 getWriter()
메서드로 ResponseBufferWriter
객체를 얻어 사용하게 된다.
즉 출력버퍼가 바꿔치기 되어버린다.
어쩃건 생성자로 response
객체를 받고 response
의 out
객체 대신 데이터를 담을 ResponseBufferWriter
를 인스턴스화 한다.
ResponseBufferWriter
는 PrintWriter
클래스를 상속한 클래스이다.
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()
으로 버퍼안의 내용을 출력할 수 도 있다.
즉 response
의 getWriter
를 오버라이딩해 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를 사용해서
만약 서버시작 시 모든 서블릿 객체가 공유해야 하는 데이터가 있다면
<context-param>
태그를 사용하거나application
의setAttribute()
를 사용하도록 하자.
어노테이션을 사용한 리스너 등록
필터와 같이 리스터도 어노테이션을 통해 설정 가능하다.
@WebListener
public class DBCPInitListener implements ServletContextListener {
...
...
}
@WebListener
어노테이션 하나면 끝난다.
web.xml
에서 귀찮게 <listener>
태그를 사용해 리스너 등록할 필요 없다.
리스터의 순서역시 web.xml에 등록된 수서이며 특이하게도
contextDestroyed()
메서드는 등록한 역순으로 실행된다.