서블릿은 다양한 시점에 발생하는 이벤트와 이벤트를 처리하기 위한 인터페이스를 정의하고 있다. 이들 이벤트와 인터페이스를 이용하면 웹 어플리케이션에서 필요로 하는 데이터의 초기화와 요청 처리 등을 추적할 수 있게 된다. 서블릿 규약은 다양한 이벤트를 처리할 수 있는 인터페이스를 정의하고 있는데, 여기서는 그 중에서 ServletContextListener 인터페이스의 활용 방법을 설명할 것이다.
웹 컨테이너는 웹 어플리케이션(컨텍스트)이 시작되거나 종료되는 시점에 특정 클래스의 메서드를 실행할 수 있는 기능을 제공하고 있다. 이 기능을 사용하면 웹 어플리케이션을 실행할 때 필요한 초기화 작업이나 웹 어플리케이션이 종료된 후 사용한 자원을 반환하는 등의 작업을 수행할 수 있다.
웹 어플리케이션이 시작되고 종료될 때 특정한 기능을 실행하려면 다음과 같이 코드를 작성하면 된다.
javax.servlet.ServletContextListener 인터페이스는 웹 어플리케이션이 시작되거나 종료될 때 호출할 메서드를 정의할 인터페이스로서, 다음과 같은 두 개의 메서드를 정의하고 있다.
웹 어플리케이션이 시작되거나 종료될 때 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() 메서드를 제공하고 있다.
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의 메서드는 다음과 같다.
컨텍스트 초기화 파라미터는 주로 웹 어플리케이션의 초기화 작업을 수행하는 데 필요한 값을 설정할 때 사용한다.
DB 연동할 때 커넥션 풀을 초기화하기 위해 서블릿을 사용했는데, 여기서는 ServletContextListener를 이용해서 커넥션 풀을 초기화하는 클래스를 구현해보도록 하겠다. 이 클래스는 다음과 같이 작성한다.
이 예제에서 사용할 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 객체로부터 커넥션 풀을 생성하는 데 필요한 값을 읽어온다.
웹 어플리케이션에는 한 개 이상의 리스너를 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가 실행된다.
위의 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.0 버전부터는 web.xml 파일에 등록하지 않고, @WebListener 애노테이션을 리스너 클래스에 적용하면 자동으로 리스너로 등록된다.
import javax.servlet.annotation.WebListener;
@WebListener
public class CListener implements ServletContextListener{
...
}
참고