ServletContextListener 구현

de_sj_awa·2021년 5월 23일
1

서블릿은 다양한 시점에 발생하는 이벤트와 이벤트를 처리하기 위한 인터페이스를 정의하고 있다. 이들 이벤트와 인터페이스를 이용하면 웹 어플리케이션에서 필요로 하는 데이터의 초기화와 요청 처리 등을 추적할 수 있게 된다. 서블릿 규약은 다양한 이벤트를 처리할 수 있는 인터페이스를 정의하고 있는데, 여기서는 그 중에서 ServletContextListener 인터페이스의 활용 방법을 설명할 것이다.

ServletContextListener를 이용한 이벤트 처리

웹 컨테이너는 웹 어플리케이션(컨텍스트)이 시작되거나 종료되는 시점에 특정 클래스의 메서드를 실행할 수 있는 기능을 제공하고 있다. 이 기능을 사용하면 웹 어플리케이션을 실행할 때 필요한 초기화 작업이나 웹 어플리케이션이 종료된 후 사용한 자원을 반환하는 등의 작업을 수행할 수 있다.

웹 어플리케이션이 시작되고 종료될 때 특정한 기능을 실행하려면 다음과 같이 코드를 작성하면 된다.

  1. javax.servlet.ServletContextListener 인터페이스를 구현한 클래스를 작성한다.
  2. web.xml 파일에 1번에서 작성한 클래스를 등록한다.

javax.servlet.ServletContextListener 인터페이스는 웹 어플리케이션이 시작되거나 종료될 때 호출할 메서드를 정의할 인터페이스로서, 다음과 같은 두 개의 메서드를 정의하고 있다.

  • public void contextInitialized(ServletContext sce) : 웹 어플리케이션을 초기화할 때 호출한다.
  • public void contextDestroyed(ServletContext sce) : 웹 어플리케이션을 종료할 때 호출한다.

웹 어플리케이션이 시작되거나 종료될 때 ServletContextListener 인터페이스를 구현한 클래스를 실행하려면 web.xml 파일에 <listener> 태그와 <listener-class> 태그를 사용해서 완전한 클래스 이름을 명시해주면 된다.

<web-app ...>
  
  <listener>
    <listener-class>jdbc.DBCPInitListener</listener-class>
  </listener>
  
  <listener>
    <listener-class>chap20.CodeInitListener</listener-class>
  </listener>
  ...
</web-app>

한 개 이상의 <listener> 태그를 등록할 수 있으며, 각 <listener> 태그는 반드시 한 개의 <listener-class> 태그를 자식 태그로 가져야 한다. <listener-class> 태그는 웹 어플리케이션의 시작/종료 이벤트를 처리할 리스너 클래스의 완전한 이름을 값으로 갖는다.

ServletContextListener 인터페이스에 정의된 두 메서드는 모두 파라미터로 javax.servlet.ServletContextEvent 타입의 객체를 전달받는다. ServletContextEvent 클래스는 웹 어플리케이션 컨텍스트를 구할 수 있는 getServletContext() 메서드를 제공하고 있다.

  • public ServletContext getServletContext()

getServletContext() 메서드가 리턴하는 ServletContext 객체는 JSP의 application 기본 객체와 동일한 객체로서, ServletContext 객체를 이용하면 web.xml 파일에 설정된 컨텍스트 초기화 파라미터를 구할 수 있다. 컨텍스트 초기화 파라미터는 다음과 같이 <context-param> 태그를 사용해서 web.xml 파일에 설정한다.

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
  ...
  <context-param>
    <param-name>jdbcdriver</param-name>
    <param-value>com.mysql.jdbc.Driver</param-value>
  </context-param>
  
  ...
</web-app>

web.xml 파일에 설정한 초기화 파라미터 값을 구하는데 사용되는 ServletContext의 메서드는 다음과 같다.

  • String getInitParameter(String name) : 지정한 이름을 갖는 컨텍스트 초기화 파라미터의 값을 리턴한다. 존재하지 않는 경우 null을 리턴한다. name 파라미터에는 <param-name> 태그로 지정한 이름을 입력한다.
  • java.util.Enumeration<String> getInitParameterNames() : 컨텍스트 초기화 파라미터의 이름 목록을 Enumeration 타입으로 리턴한다.

컨텍스트 초기화 파라미터는 주로 웹 어플리케이션의 초기화 작업을 수행하는 데 필요한 값을 설정할 때 사용한다.

DB 연동할 때 커넥션 풀을 초기화하기 위해 서블릿을 사용했는데, 여기서는 ServletContextListener를 이용해서 커넥션 풀을 초기화하는 클래스를 구현해보도록 하겠다. 이 클래스는 다음과 같이 작성한다.

  • web.xml 파일에 커넥션 풀을 초기화할 때 사용할 컨텍스트 초기화 파라미터를 설정한다.
  • ServletContextListener 인터페이스를 구현한 클래스는 contextInitialized() 메서드에서 컨텍스트 초기화 파라미터를 이용해서 커넥션 풀을 초기화하는 데 필요한 값을 로딩한다.

이 예제에서 사용할 web.xml은 아래와 같다. 커넥션 풀을 생성하는 데 필요한 컨텍스트 초기화 파라미터를 설정하고 있다.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
  
   <listener>
       <listener-class>jdbc.DBCPInitListener</listener-class>
   </listener>

    <context-param>
        <param-name>poolConfig</param-name>
        <param-value>
            jdbcdriver=com.mysql.jdbc.Driver
            jdbcUrl=jdbc:mysql://localhost:3306/guestbook?characterEncoding=utf-8
            dbUser=jspexam
            dbPass=jsppw
            poolName=guestbook
        </param-value>
    </context-param>
  
</web-app>

poolConfig 컨텍스트 초기화 파라미터는 여러 줄에 걸쳐 값을 갖고 있는데, 실제 자바 코드에서 poolConfig 컨텍스트 초기화 파라미터 값을 읽으면 다음과 같다.

jdbcdriver=com.mysql.jdbc.Driver
            jdbcUrl=jdbc:mysql://localhost:3306/guestbook?characterEncoding=utf-8
            dbUser=jspexam
            dbPass=jsppw
            poolName=guestbook

<param-value> 태그 사이에 있는 문자열의 앞뒤 공백을 제거한 결과를 실제 값으로 사용한다. 하지만, 중간에 위치한 공백은 제거하지 않기 때문에 poolConfig 컨텍스트 초기화 파라미터는 위 코드에서 보는 것처럼 jdbcUrl이나 dbUser 앞의 공백을 그대로 포함한다.

poolConfig 컨텍스트 초기화 파라미터를 이용해서 커넥션 풀을 초기화하는 코드는 아래 예제 코드와 같다.

package jdbc;

import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import org.apache.commons.dbcp2.*;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import java.io.IOException;
import java.io.StringReader;
import java.sql.DriverManager;
import java.util.Properties;

public class DBCPInitListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce){
        String poolConfig = sce.getServletContext().getInitParameter("poolConfig");
        Properties prop = new Properties();
        try{
            prop.load(new StringReader(poolConfig));
        }catch (IOException e){
            throw new RuntimeException(e);
        }
        loadJDBCDriver(prop);
        initConnectionPool(prop);
    }

    private void loadJDBCDriver(Properties prop){
        String driverClass = prop.getProperty("jdbcdriver");
        try{
            Class.forName(driverClass);
        }catch (ClassNotFoundException ex){
            throw new RuntimeException("fail to load JDBC Driver", ex);
        }
    }

    private void initConnectionPool(Properties prop){
        try{
            String jdbcUrl = prop.getProperty("jdbcUrl");
            String username = prop.getProperty("dbUser");
            String pw = prop.getProperty("dbPass");

            ConnectionFactory connFactory = new DriverManagerConnectionFactory(jdbcUrl, username, pw);

            PoolableConnectionFactory poolableConnFactory = new PoolableConnectionFactory(connFactory, null);

            poolableConnFactory.setValidationQuery("select 1");

            GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
            poolConfig.setTimeBetweenEvictionRunsMillis(1000L * 60L * 5L);
            poolConfig.setTestWhileIdle(true);
            poolConfig.setMinIdle(4);
            poolConfig.setMaxTotal(50);

            GenericObjectPool<PoolableConnection> connectionPool = new GenericObjectPool<>(poolableConnFactory, poolConfig);
            poolableConnFactory.setPool(connectionPool);

            Class.forName("org.apache.commons.dbcp2.PoolingDriver");
            PoolingDriver driver = (PoolingDriver) DriverManager.getDriver("jdbc:apache:commons:dbcp:");
            String poolName = prop.getProperty("poolName");
            driver.registerPool(poolName, connectionPool);
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce){
    }
}

위 코드에서는 Properties.load() 메서드를 사용하는데, 이 메서드는 "키=값" 형식으로 구성된 문자열로부터 프로퍼티를 로딩한다. poolConfig 컨텍스트 초기화 파라미터가 "키=값" 형식을 가지므로 Properties.load() 메서드를 사용하면 poolConfig 초기화 파라미터의 설정 값을 Properties 컨텍스트 초기화 파라미터는 "dbUser=jspexam" 문자열을 포함하고 있으므로, Properties는 "dbUser" 프로퍼티 값으로 "jspexam"을 갖게 된다.

웹 컨테이너를 구동하면 web.xml 파일에 등록한 DBCPInitListener 객체의 contextInitialized() 메서드가 실행되면서 커넥션 풀을 초기화한다. loadJDBCDriver() 메서드와 initConnectionPool() 메서드는 poolConfig 초기화 파라미터를 이용해서 생성한 Properties 객체로부터 커넥션 풀을 생성하는 데 필요한 값을 읽어온다.

1. 리스너의 실행 순서

웹 어플리케이션에는 한 개 이상의 리스너를 web.xml 파일에 등록할 수도 있다.

<web-app ...>
  
  <listener>
    <listener-class>chap20.Alistener</listener-class>
  </listener>
  
  <listener>
    <listener-class>hap20.listener.BListener</listener-class>
  </listener>

  ...
</web-app>

한 개 이상의 리스너가 등록된 경우, contextInitialized() 메서드는 등록된 순서대로 실행되고 contextDestroyed() 메서드는 등록된 반대 순서대로 실행된다. 즉, 위 코드의 경우에는 웹 어플리케이션이 시작될 때 AListener가 먼저 실행되고 그다음에 BListener가 실행된다. 반대로 웹 어플리케이션이 종료될 때는 BListener가 실행되고 그다음에 AListener가 실행된다.

2. 리스너에서의 익셉션 처리

위의 DBCPInitListener 클래스의 코드를 보면 다음과 같이 catch 블록에서 RuntimeException을 발생시키는 것을 확인할 수 있다.

public void contextInitialized(ServletContextEvent sce){
    String poolConfig = sce.getServletContext().getInitParameter("poolConfig");
    Properties prop = new Properties();
    try{
        prop.load(new StringReader(poolConfig));
    }catch (IOException e){
        throw new RuntimeException(e);
    }
    loadJDBCDriver(prop);
    initConnectionPool(prop);
}

리스너에서 RuntimeException을 발생시키는 이유는 contextInitialized() 메서드 정의에 throws가 없기 때문이다. 이 메서드는 발생시킬 수 있는 checked 익셉션을 지정하고 있지 않으므로 익셉션을 발생시키려면 RuntimeException이나 그 하위 타입의 익셉션을 발생시켜야 한다.

3. 애노테이션을 이용한 리스너 등록

서블릿 3.0 버전부터는 web.xml 파일에 등록하지 않고, @WebListener 애노테이션을 리스너 클래스에 적용하면 자동으로 리스너로 등록된다.

import javax.servlet.annotation.WebListener;

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

참고

  • 최범균의 JSP2.3 웹 프로그래밍
profile
이것저것 관심많은 개발자.

0개의 댓글