mojo's Blog

리스너와 필터 본문

JSP

리스너와 필터

_mojo_ 2022. 1. 3. 18:29

리스너

 

리스너란? 

컨테이너에서 발생하는 이벤트를 모니터링하다가 특정 이벤트가 발생하면 실행되는 특수한 서블릿으로, '이벤트 리스너'라고도 한다.

웹 애플리케이션 실행에 필요한 정보를 제공하거나 톰캣 시작/종료와 같은 특정 상황에 자동으로 동작하는 프로그램을 구현할 때 사용한다.

이때 리스너는 서블릿과 마찬가지로 애너테이션 기반 코드로 작성할 수 있다.

 

리스너를 이해하려면 먼저 리스너가 동작하기 위한 이벤트의 종류와 그에 따른 프로그램 API를 알아야 한다.

리스너는 일반적인 형태의 서블릿이 아니라 특정 이벤트에 따라 동작하는 인터페이스를 구현한 클래스라고 이해하면 쉽다.

리스너는 기본적으로 생명 주기 변화와 Scope Object에서 관리하는 속성의 변화를 모니터링하고 해당 이벤트가 발생하면 실행되는 구조다.

예를 들어 ServletContext는 웹 애플리케이션 단위로 생성되는 객체로, 보통 톰캣의 시작과 종료(혹은 웹 애플리케이션의 시작과 종료)와 일치하기 때문에 해당 시점에 실행을 원하는 프로그램이 있다면 리스너로 구현할 수 있다.

 

리스너의 동작 구조는 다음과 같다.

클라이언트의 요청에 따라 서블릿을 통해 Session, Request 객체가 생성되거나 속성의 변화가 발생하면 컨테이너에서 이벤트를 감지하고 해당 이벤트를 처리하는 리스너 클래스의 메서드를 호출하게 된다.

리스너의 경우 특정 서블릿이나 JSP에서 사용하기 위한 목적보다는 웹 애플리케이션 전체의 설계나 운영 관점에서 사용한다고 볼 수 있다.

 

리스너 활용의 대표적 유형은 다음과 같다

■ 초기화 매개 변수와 연동톰캣이 시작될 때 'web.xml'의 ServletContext 초기화 매개변수를 읽어 그에 따라 특정 객체를 초기화한 후 서블릿이나 JSP에 제공한다.

 

■ 예제 프로그램 등을 배포할 때 샘플 데이터 제공프로그램을 실행할 때 DB가 필요한 경우 미리 DB와 연결을 만들어두거나 테이블을 생성하고 샘플 데이터를 로딩하는 등의 작업을 자동으로 수행해서 추가적 작업이 없이 프로그램을 실행할 수 있다.

 

■ 복잡한 환경 설정 제공프로그램 실행에 필요한 여러 정보(DB, 다른 서비스 연동 정보, 관리자 계정 정보 등 프로그램 외부에서 변하는 정보를 주입하는 형태)가 고정되어 있지 않고 운영하는 서버 상황에 따라 변경되어야 하는 경우, 이를 파일로부터 읽어와 JSP 및 서블릿 등에 제공한다.

 

■ 특정 이벤트에 동작하는 기능 구현웹 애플리케이션을 실행할 때 함께 동작해야 하는 외부 프로그램이나 서비스의 동작 유무를 확인하고 자동으로 실행할 수 있다.

 

 

리스너의 종류란?

반적으로 웹 애플리케이션 단위인 서블릿 콘텍스트와 세션의 변화에 따라 실행되는 리스너를 만들게 된다.

서블릿 콘텍스트와 세션은 웹 애플리케이션 운영에서 중요한 객체이다.

각각의 객체에 대해 생명 주기(생성, 소멸 등) 변화 및 정보 공유를 위해 사용하는 속성의 상태 변화를 감지하고 실행되는 형태다.

 

유형 설명 리스너 인터페이스
ServletContext
생명 주기 변화
대표적 리스너 이벤트로, ServletContext의 생성과 소멸 시점에 동작한다. 보통 톰캣의 시작 및 종료 시점과 일치한다. javax.servlet.ServletContextListener
ServletContext
속성 변화
ServletContext의 속성이 추가되거나 변경되는 상황에 동작한다. javax.servlet.ServletContextAttributeListener
Session
생명 주기 변화
Session의 생성과 소멸 등 변화에 따라 동작한다. javax.servlet.http.HttpSessionListener
Session
속성 변화
Session의 속성이 추가되거나 변경되는 상황에 동작한다. javax.servlet.http.HttpSessionAttributeListener

 

리스너 구현

리스너 구현을 위해 먼저 위에 있는 표에서 설명한 리스너 인터페이스를 구현한 클래스를 만들어야 한다.

그리고 애너테이션을 이용해 리스너임을 명시한 다음, 오버라이딩된 메서드 중에서 필요한 부분의 코드를 작성하면 된다.

예를 들어 톰캣이 실행되거나 종료될 때 동작하는 리스너를 구현해본다.

우선 ServletContext Listener 인터페이스를 구현하는 클래스를 만든다.

클래스 생성은 일반 자바 클래스로 생성하고 implements와 애너테이션을 직접 추가하거나 이클립스에서 제공하는 [New] -> [Listener] 메뉴를 통해 생성하는 것도 가능하다.

 

@WebListener 애너테이션으로 리스너 클래스임을 컨테이너에 전달하고, 인터페이스 구현으로 생성된 오버라이딩 메서드 중에서 필요한 메서드만 구현해서 사용하면 된다.

예를 들어 웹 애플리케이션 시작(톰캣 시작)에 동작하는 코드는 contextInitialized() 메서드에 작성한다.

ServletContext에 대한 접근은 인자로 전달되는 ServletContextEvent 객체의 getServletContext() 메서드로 가능하다.

 

@WebListener // 리스너 클래스임을 컨테이너에게 전달
public class ListenerExam implementtts ServletContextListener {
    // ServletContext 시작
    public void contextInitialized(ServletContextEvent sce) {
        sce.getServletContext().log("ServletContext 시작됨!!");
    }
    ...
}

 

 

필터

 

 

필터란?

필터는 '서블릿 필터'라고도 하며 리스너와 마찬가지로 웹 애플리케이션을 지원하기 위한 특수한 형태의 서블릿이다.

클라이언트 요청에 따라 서블릿이나 JSP가 실행되기 전에 response 혹은 request 객체의 조작이나 추가적인 처리를 할 수 있다.

필터는 기본적으로 특정 요청에만 동작하며, 여러 개의 필터가 정해진 순서에 따라 배치될 수 있는데 클라이언트 요청 처리 이전에 먼저 실행된다.

리스너와 마찬가지로 단순히 기능만 구현하는 웹 프로그램의 경우에는 필터를 꼭 만들지 않아도 된다.

하지만 애플리케이션 설계 관점에서 좀 더 유연하고 효과적인 애플리케이션 구현 및 운영이 필요하다면 필터에 대해 잘 알아둘 필요가 있다.

 

필터는 기존 코드의 변경 없이 애플리케이션에서 공통적으로 사용할 수 있는 기능 구현에 널리 사용된다.

대표적으로 활용되는 분야는 다음과 같다.

인증(Authentication)

특정 페이지에서 로그인 여부나 특정 권한을 확인해야 할 때 컨트롤러에서 처리하는 방법이 있지만 권한 확인과 같이 공통된 기능을 개별 컨트롤러에서 중복해서 구현하는 것은 좋지 않은 방법이다.

이 경우 필터를 이용하면 애플리케이션 구조와 상관없이 기존 소스를 최대한 수정하지 않고 인증 기능을 수행하도록 할 수 있다.

 

로깅/감사(Logging and Auditing)

특정 페이지 또는 기능에 대해 사용 현황을 모니터링하고 로그로 관리할 필요가 있을 때 인증의 경우와 마찬가지로 필터를 통해 해당 요청을 수행하기 전 로깅 처리를 할 수 있다.

 

국제화(Localization)

다국어 처리는 프레임워크 등에서 제공하는 국제화 방법을 사용할 수도 있으며 필터를 이용할 경우 특정 페이지에 들어갈 메시지 등을 해당 언어로 변환해 전달할 수도 있다.

 

한글 인코딩 처리(Encoding)

지금까지 서블릿이나 JSP를 이용해 POST 방식으로 전달하는 파라미터의 한글 처리를 위해 request.setCharacterEncoding("utf-8")을 사용해왔다.

그런데 모든 컨트롤러 서블릿에 이 코드를 추가하면 너무 많은 중복이 발생하며 캐릭터셋 변경 시에도 문제가 되기 떄문에 필터를 통해 한 번에 처리하는 방법을 활용할 수 있다.

 

 

필터의 구조와 동작 과정

필터의 구조를 보면 리스너와 유사하다.

필터는 톰캣 서버를 시작할 때 필터 구현 클래스의 애너테이션을 참조하여 javax.servlet.Filter 인터페이스를 구현한 클래스가 초기화된다.

또한 필터는 여러 개 존재할 수 있으며, 각각의 필터는 init() 메서드를 통해 초기화 작업을 수행한다.

init() 메서드는 필터 초기화 시 한번만 실행된다.

이후 사용자 요청에 따라 서블릿이나 JSP가 호출될 떄 애너테이션으로 설정된 필터 매핑 정보를 참조해 특정 서블릿이나 JSP에 대해 서로 다른 필터를 적용할 수 있다.

doFilter()는 해당 필터가 적용되었을 때 수행할 작업을 구현하는 메인 메서드가 된다.

destroy()는 필터가 종료될 때 수행할 내용을 구현한다.

 

필터의 동작과정은 다음과 같다.

 

필터 A는 모든 서블릿에 적용되고, 필터 B는 서블릿 a, b에 적용되며, 필터 C는 서블릿 a, b, c에 적용된다.

결과적으로 서블릿 a, b는 필터 A, B, C를 차례로 적용받게 된다.

필터의 적용은 각 필터의 doFilter() 메서드의 내용이 수행되는 것으로, ServletRequest와 ServletResponse의 내용을 가로채 필요한 작업을 수행하고 다음 필터로 전달하거나, 요청한 서블릿으로 이동하는 구조이다.

 

예를 들어 가정에서 사용하는 정수기의 필터가 여러 종류이고, 수돗물이 각각의 필터를 통해 여과되고 정수되어 마실 수 있는 것과 유사한 구조라고 생각하면 된다.

즉 정수기의 물이 필터에서는 ServletRequest와 ServletResponse인 것이다.

물론 두 객체를 활용하지 않고 다른 필요한 작업만 수행하는 필터도 있을 수 있다.

 

 

필터 구현

리스너와 마찬가지로 이클립스에서 필터 생성 메뉴를 통해 손쉽게 생성하거나 javax.servlet.Filter 인터페이스를 구현하는 클래스를 직접 생성해도 된다.

또한 @WebFilter 애너테이션을 사용해 필터임을 알리고 서블릿과 유사하게 필터 요청을 위한 url 매핑 정보를 인자로 추가해주어야 한다.

자동 생성되는 오버라이딩 메서드는 필요한 부분만 구현하면 된다.

필터 자체의 생명 구지 메서드가 포함되어 있고, 실제로 필터가 동작할 때는 doFilter() 메서드가 호출된다.

 

@WebFilter("/EncodingFilter") // 필터임을 알림
public class EncodingFilter implements Filter {
    ...
    public void doFilter(ServletRequest request, ServleResponse response, FilterChain chain) throws IOException, ServletException {
        ...
        chain.doFilter(request, response);
    }
}
  • doFilter() :  request, response, chain 등의 주어진 객체를 사용하여 필요한 작업을 수행한 후에 필터를 실행하는 구조다. 만약 필터가 없을 경우 요청된 서블릿이나 jsp가 호출된다.

코드에서 보면 request, response가 계속 전달되므로 원래 요청에 포함되지 않은 관련 작업을 수행하는 것이 가능하다.

물론 request, response와 관련 없이 별도의 데이터베이스나 로깅 작업, 다른 서비스와의 연동 등 필요한 작업이 있다면 구현하면 된다.

 

여러개의 필터를 차례대로 적용하는 방법은 애너테이션만으로는 불가능하다.

애너테이션에 의한 필터 실행은 특정 url 매핑 조건에 따라 이루어지는 것으로 실행 순서를 조정할 수 없기 때문이다.

만약 필터의 실행 순서를 지정하려면 'web.xml' 파일에 필터를 등록하는 과정을 거쳐야 한다.

이 경우 필터 서블릿의 @WebFilter에는 url 매핑 대신 filterName 속성이 들어가야 한다.

@WebFilter(filterName="filterOne")
public class FirstFilter implements Filter {
   ...
}

 

 다음은 'web.xml'에 필터를 등록하는 코드이다.

<filter-mapping>
    <filter-name>filterOne</filter-name>
    <url-pattern> /ch11/* </url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>filterTwo</filter-name>
    <url-pattern> /ch11/* </url-pattern>
</filter-mapping>

 

 

리스너 종합 실습

 

1. 리스너 클래스 생성하기

(1) src/main/java => [New] => [Listener] 를 클릭한다.

 

 

(2) package 명과 Class 이름을 작성한다.

 

 

(3) 아래 사진과 같이 체크한 후 Finish 버튼을 클릭한다.

 

 

(4) 다음과 같이 완성이다.

 

 

2. ServletContext 이벤트 구현

ListenerExam.java 코드

package ch11;

import javax.servlet.ServletContextAttributeEvent;
import javax.servlet.ServletContextAttributeListener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

/**
 * Application Lifecycle Listener implementation class ListenerExam
 *
 */
@WebListener
public class ListenerExam implements ServletContextListener, ServletContextAttributeListener, HttpSessionListener, HttpSessionAttributeListener {

    /**
     * Default constructor. 
     */
    public ListenerExam() {
        // TODO Auto-generated constructor stub
    }

	/**
     * @see HttpSessionListener#sessionCreated(HttpSessionEvent)
     */
    public void sessionCreated(HttpSessionEvent se)  { 
         // TODO Auto-generated method stub
    }

	/**
     * @see ServletContextAttributeListener#attributeAdded(ServletContextAttributeEvent)
     */
    public void attributeAdded(ServletContextAttributeEvent scae)  { 
         // TODO Auto-generated method stub
    	scae.getServletContext().log("ServletContext 속성 추가: " + scae.getValue());
    }

	/**
     * @see ServletContextAttributeListener#attributeRemoved(ServletContextAttributeEvent)
     */
    public void attributeRemoved(ServletContextAttributeEvent scae)  { 
         // TODO Auto-generated method stub
    }

	/**
     * @see HttpSessionListener#sessionDestroyed(HttpSessionEvent)
     */
    public void sessionDestroyed(HttpSessionEvent se)  { 
         // TODO Auto-generated method stub
    }

	/**
     * @see ServletContextListener#contextDestroyed(ServletContextEvent)
     */
    public void contextDestroyed(ServletContextEvent sce)  { 
         // TODO Auto-generated method stub
    	sce.getServletContext().log("ServletContext 종료됨!!!");
    }

	/**
     * @see HttpSessionAttributeListener#attributeAdded(HttpSessionBindingEvent)
     */
    public void attributeAdded(HttpSessionBindingEvent se)  { 
         // TODO Auto-generated method stub
    }

	/**
     * @see HttpSessionAttributeListener#attributeRemoved(HttpSessionBindingEvent)
     */
    public void attributeRemoved(HttpSessionBindingEvent se)  { 
         // TODO Auto-generated method stub
    }

	/**
     * @see HttpSessionAttributeListener#attributeReplaced(HttpSessionBindingEvent)
     */
    public void attributeReplaced(HttpSessionBindingEvent se)  { 
         // TODO Auto-generated method stub
    }

	/**
     * @see ServletContextAttributeListener#attributeReplaced(ServletContextAttributeEvent)
     */
    public void attributeReplaced(ServletContextAttributeEvent scae)  { 
         // TODO Auto-generated method stub
    }

	/**
     * @see ServletContextListener#contextInitialized(ServletContextEvent)
     */
    public void contextInitialized(ServletContextEvent sce)  { 
         // TODO Auto-generated method stub
    	sce.getServletContext().log("ServletContext 시작됨!!!");
    }
	
}

 

ListenerTestServlet.java 코드

package ch11;

import java.io.IOException;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class ListenerTestServlet
 */
@WebServlet("/ListenerTestServlet")
public class ListenerTestServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
    ServletContext sc;
    
    public void init(ServletConfig config) throws ServletException {
    	super.init(config);
    	sc = getServletContext();
    }

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
		sc.setAttribute("name", "홍길동");
	}

}

 

 

3. 세션 이벤트 구현

ListenerExam.java 코드

package ch11;

import javax.servlet.ServletContextAttributeEvent;
import javax.servlet.ServletContextAttributeListener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

/**
 * Application Lifecycle Listener implementation class ListenerExam
 *
 */
@WebListener
public class ListenerExam implements ServletContextListener, ServletContextAttributeListener, HttpSessionListener, HttpSessionAttributeListener {

    /**
     * Default constructor. 
     */
    public ListenerExam() {
        // TODO Auto-generated constructor stub
    }

	/**
     * @see HttpSessionListener#sessionCreated(HttpSessionEvent)
     */
    public void sessionCreated(HttpSessionEvent se)  { 
         // TODO Auto-generated method stub
    	se.getSession().getServletContext().log("Session 생성됨:" + se.getSession().getId());
    }

	/**
     * @see ServletContextAttributeListener#attributeAdded(ServletContextAttributeEvent)
     */
    public void attributeAdded(ServletContextAttributeEvent scae)  { 
         // TODO Auto-generated method stub
    	scae.getServletContext().log("ServletContext 속성 추가: " + scae.getValue());
    }

	/**
     * @see ServletContextAttributeListener#attributeRemoved(ServletContextAttributeEvent)
     */
    public void attributeRemoved(ServletContextAttributeEvent scae)  { 
         // TODO Auto-generated method stub
    }

	/**
     * @see HttpSessionListener#sessionDestroyed(HttpSessionEvent)
     */
    public void sessionDestroyed(HttpSessionEvent se)  { 
         // TODO Auto-generated method stub
    }

	/**
     * @see ServletContextListener#contextDestroyed(ServletContextEvent)
     */
    public void contextDestroyed(ServletContextEvent sce)  { 
         // TODO Auto-generated method stub
    	sce.getServletContext().log("ServletContext 종료됨!!!");
    }

	/**
     * @see HttpSessionAttributeListener#attributeAdded(HttpSessionBindingEvent)
     */
    public void attributeAdded(HttpSessionBindingEvent se)  { 
         // TODO Auto-generated method stub
    	se.getSession().getServletContext().log("Session 속성 추가: " + se.getValue());
    }

	/**
     * @see HttpSessionAttributeListener#attributeRemoved(HttpSessionBindingEvent)
     */
    public void attributeRemoved(HttpSessionBindingEvent se)  { 
         // TODO Auto-generated method stub
    }

	/**
     * @see HttpSessionAttributeListener#attributeReplaced(HttpSessionBindingEvent)
     */
    public void attributeReplaced(HttpSessionBindingEvent se)  { 
         // TODO Auto-generated method stub
    }

	/**
     * @see ServletContextAttributeListener#attributeReplaced(ServletContextAttributeEvent)
     */
    public void attributeReplaced(ServletContextAttributeEvent scae)  { 
         // TODO Auto-generated method stub
    }

	/**
     * @see ServletContextListener#contextInitialized(ServletContextEvent)
     */
    public void contextInitialized(ServletContextEvent sce)  { 
         // TODO Auto-generated method stub
    	sce.getServletContext().log("ServletContext 시작됨!!!");
    }
	
}

 

ListenerTestServlet.java 코드

package ch11;

import java.io.IOException;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * Servlet implementation class ListenerTestServlet
 */
@WebServlet("/ListenerTestServlet")
public class ListenerTestServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
    ServletContext sc;
    
    public void init(ServletConfig config) throws ServletException {
    	super.init(config);
    	sc = getServletContext();
    }

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
		sc.setAttribute("name", "홍길동");
		HttpSession s = request.getSession();
		s.setAttribute("ssName", s.getId()+": 세션 속성 저장!!");
	}

}

 

 

 

필터 실습

 

1. [New] -> [Filter] 를 선택한 후 class Name을 EncodingFilter 으로 설정한 후 Next 버튼을 클릭한다.

 

2. /EncodingFilter 를 클릭 후 Edit 버튼을 클릭해서 *.nhn 요청에 대해 동작하도록 url 매핑을 설정한다.

 

 

3. 실제 필터가 동작하는 부분 doFilter() 에 작성하면 된다.

 

package ch11;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;

/**
 * Servlet Filter implementation class EncodingFilter
 */
@WebFilter("*.nhn")
public class EncodingFilter implements Filter {

    /**
     * Default constructor. 
     */
    public EncodingFilter() {
        // TODO Auto-generated constructor stub
    }

	/**
	 * @see Filter#destroy()
	 */
	public void destroy() {
		// TODO Auto-generated method stub
	}

	/**
	 * @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
	 */
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		// TODO Auto-generated method stub
		// place your code here
		HttpServletRequest httpReq = (HttpServletRequest)request;
		if(httpReq.getMethod().equalsIgnoreCase("POST")) {
			request.setCharacterEncoding("utf-8");
		}
		// pass the request along the filter chain
		chain.doFilter(request, response);
	}

	/**
	 * @see Filter#init(FilterConfig)
	 */
	public void init(FilterConfig fConfig) throws ServletException {
		// TODO Auto-generated method stub
	}

}

 

 

'JSP' 카테고리의 다른 글

REST API 구현  (0) 2022.01.05
REST API  (0) 2022.01.05
프로젝트 : 뉴스 기사 관리 웹 서비스  (5) 2022.01.03
SQL 및 JDBC 기본 구조와 API  (0) 2022.01.02
데이터베이스와 JDBC  (0) 2021.12.31
Comments