Dev Note/Tomcat2008. 3. 17. 15:45

기본적인 Tomcat의 server.xml이나 context path xml에 DBCP를 설정하는 방법은 대부분 알고 있을 것이다. 그럼 DBCP를 Tomcat에서 설정하는 것이 아니라 Application이나 Servlet을 이용하여 설정하여 사용하고 싶을 때는 어떻게 해야 하나?
그건 DBCP의 Sample 소스를 보면 금방 알 수 있다.
이번에 Servlet을 이용하여 web.xml에 init-param으로 DBCP를 설정하여 web에서 사용하는 간략한 소스를 올려볼까 한다. ^^

-- DBCP를 서비스하기 위한 Servlet
   package com.junducki.blog.dbcp;

  
import java.io.IOException;
   import java.io.UnsupportedEncodingException;
   import java.sql.Connection;
   import java.sql.DriverManager;
   import java.sql.SQLException;
   import javax.servlet.ServletConfig;
   import javax.servlet.ServletException;
   import javax.servlet.http.HttpServlet;
   import javax.servlet.http.HttpServletRequest;
   import javax.servlet.http.HttpServletResponse;
   import org.apache.commons.dbcp.ConnectionFactory;
   import org.apache.commons.dbcp.DriverManagerConnectionFactory;
   import org.apache.commons.dbcp.PoolableConnectionFactory;
   import org.apache.commons.dbcp.PoolingDriver;
   import org.apache.commons.logging.Log;
   import org.apache.commons.logging.LogFactory;
   import org.apache.commons.pool.impl.GenericObjectPool;
   import com.goorm.common.security.SeedCipher;
   import com.goorm.common.util.StringUtil;
   import com.goorm.fw.web.servlet.ServletInitParamException;
   import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;

   /**
      * DBCP를 Default 설정 server.xml or context path xml에 설정하는 것이 아닌 web.xml에 Servlet을
      * 이용하여 설정하여 Servlet을 통해 DB Connection을 가져오기 위한 Servlet
      *
      * @author jinuk jung, junducki@naver.com
      * @version 1.0, 2008. 03. 12
      */

   public class DBCPServlet extends HttpServlet {
     
private static final String DBCP_CONNECT_NAME_PREFIX = "jdbc:apache:commons:dbcp:";

     
// ********** Define Initial Parameter Names
      private final String JNDI_NAME = "name";
      // db connection info parameter names
      private final String DRIVER_CLASS_NAME = "driverClassName";
      private final String URL = "url";
      private final String USERNAME = "username";
      private final String PASSWORD = "password";
      private final String MAX_ACTIVE = "maxActive";
      private final String MAX_IDLE = "maxIdle";
      private final String MAX_WAIT = "maxWait";
      private final String VALIDATION_QUERY = "validationQuery";
      // abandoned setting을 위한 parameter names
      private final String REMOVE_ABANDONED = "removeAbandoned";
      private final String REMOVE_ABANDONED_TIMEOUT = "removeAbandonedTimeout";
      private final String LOG_ABANDONED = "logAbandoned";
      // ********** Define Initial Parameter Names End

      /** logger instance */
      protected Log logger = null;

     
/** DBCP JNDI Name */
      private String jndiName = null;

     
/**
         * Servlet을 초기화한다. <br />
         *
         * @param config ServletConfig Servlet Config
         * @throws ServletException
         */
      public void init(ServletConfig config) throws ServletException {
         super.init(config);

         
// create logger instance
         logger = LogFactory.getLog(this.getClass());

         
try {
            registDBCPToJNDI(config);

           
StringBuilder trace = new StringBuilder("DBCP Regist Success [").append(jndiName).append("]");
            logger.info(trace.toString());
         } catch (ServletInitParamException e) {
            logger.error("DBCP Regist Error :: " + jndiName, e);
         }
      }

     
public void service(HttpServletRequest req, HttpServletResponse res) throws IOException { }

      public void destroy() {
         try {
            super.destroy();

            PoolingDriver poolingDriver = (PoolingDriver) DriverManager.getDriver(DBCP_CONNECT_NAME_PREFIX);
            ((GenericObjectPool) poolingDriver.getConnectionPool(jndiName)).close();

           
StringBuilder trace = new StringBuilder("DBCPServlet Destroy :: DBCP JNDI NAME [")
                  .append( jndiName).append("]");
            logger.info(trace.toString());
         } catch (Exception e) {
            logger.error(e.getMessage(), e);
         }
      }

     
/**
         * 설정정보를 로딩하여 DBCP를 생성하여 등록한다.
         *
         * @param config ServletConfig Servlet Config
         * @throws ServletInitParamException
         */
      private void registDBCPToJNDI(ServletConfig config) throws ServletInitParamException {
         
String initParamValue = null;
     
         
// load dbcp configuration
         jndiName = (initParamValue = config.getInitParameter(JNDI_NAME)) != null ? initParamValue : null;
         if (jndiName == null) {
            throw new ServletInitParamException("DBCP Configuration Error :: name is mandatory.");
         }

         
String driverClassName = (initParamValue = config.getInitParameter(DRIVER_CLASS_NAME)) != null
               ? initParamValue : null;
         if (driverClassName == null) {
            throw new ServletInitParamException("DBCP Configuration Error :: driverClassName is mandatory.");
         }

         
String url = (initParamValue = config.getInitParameter(URL)) != null ? initParamValue : null;
         if (url == null) {
            throw new ServletInitParamException("DBCP Configuration Error :: url is mandatory.");
         }

         
String username = (initParamValue = config.getInitParameter(USERNAME)) != null ? initParamValue : null;
         if (username == null) {
            throw new ServletInitParamException("DBCP Configuration Error :: username is mandatory.");
         }

         
String password = (initParamValue = config.getInitParameter(PASSWORD)) != null ? initParamValue : null;
         if (password == null) {
            throw new ServletInitParamException("DBCP Configuration Error :: password is mandatory.");
         }

         
int maxActive = (initParamValue = config.getInitParameter(MAX_ACTIVE)) != null
               ? Integer.parseInt(initParamValue) : 10;
         int maxIdle = (initParamValue = config.getInitParameter(MAX_IDLE)) != null
               ? Integer.parseInt(initParamValue) : 5;
         int maxWait = (initParamValue = config.getInitParameter(MAX_WAIT)) != null
               ? Integer.parseInt(initParamValue) : 15000; // millisecond
     
         String validationQuery = (initParamValue = config.getInitParameter(VALIDATION_QUERY)) != null
               ? nitParamValue : null;

         
// jdbc driver lodding
         try {
            Class.forName(driverClassName);
         } catch (ClassNotFoundException e) {
            throw new ServletInitParamException(e);
         }

         
// db connection pool로 사용할 GenericObjectPool을 생성하고 설정한다.
         GenericObjectPool connPool = new GenericObjectPool();
         connPool.setMaxActive(maxActive);
         connPool.setMaxIdle(maxIdle);
         connPool.setMaxWait(maxWait);

         
// Object Pool에서 DB Connection을 생성하기 위한 DriverManager Factory를 생성한다.
         ConnectionFactory connFactory = new DriverManagerConnectionFactory(url, username, password);

         
// ConnectionFactory의 래퍼 클래스 생성
         PoolableConnectionFactory poolableConnFactory
               = new PoolableConnectionFactory(connFactory, connPool, null, validationQuery, false, true);

         
// DBCP에서 Abandoned 설정 정보를 사용하려면 주석을 푼다. dbcp-1.2.2에서 Abandoned에 대한 모든
         // 객체가 Deprecated 되었다.
         /*boolean removeAbandoned = (initParamValue = config.getInitParameter(REMOVE_ABANDONED))
               != null ? Boolean.parseBoolean(initParamValue) : false;
         
int removeAbandonedTimeout = (initParamValue
               = config.getInitParameter(REMOVE_ABANDONED_TIMEOUT)) != null
               ? Integer.parseInt(initParamValue) : 300;
         
boolean logAbandoned = (initParamValue = config.getInitParameter(LOG_ABANDONED)) != null
               ? Boolean.parseBoolean(initParamValue) : false;

         
AbandonedConfig abandonedConfig = new AbandonedConfig();
         abandonedConfig.setLogAbandoned(true);
         abandonedConfig.setRemoveAbandoned(true);
         abandonedConfig.setRemoveAbandonedTimeout(120);

         
AbandonedObjectPool abandonedObjectPool
               = new AbandonedObjectPool(poolableConnFactory, abandonedConfig);*/

         
// PoolingDriver를 로딩하여 DB Connection Pool을 등록한다.
         try {
            Class.forName("org.apache.commons.dbcp.PoolingDriver");
            PoolingDriver poolingDriver
                  = (PoolingDriver) DriverManager.getDriver(DBCP_CONNECT_NAME_PREFIX);
            poolingDriver.registerPool(jndiName, connPool);
         } catch (ClassNotFoundException e) {
            throw new ServletInitParamException(e);
         } catch (SQLException e) {
            throw new ServletInitParamException(e);
         }
      }

     
/**
         * DBCP에 있는 DB Connection을 가져와 리턴한다.
         *
         * @param poolName String DBCP Pool Name
         * @return Connection DataBase Connection
         * @throws SQLException
         */
      public static Connection getDBCPConnection(String poolName) throws SQLException {
         
return DriverManager.getConnection(DBCP_CONNECT_NAME_PREFIX + "oraDBCP");
      }
   }

위 소스는 DBCP를 web.xml을 이용하여 DBCPServlet을 이용하여 Servlet을 등록하여 사용하는 방식이다. DBCP DataBase Connection을 가져올때는 DBCPServlet.getDBCPConnection(jndiName)을 이용하면 된다.
그럼 DBCPServlet을 web.xml에 어떻게 등록하여 사용하는가?

-- web.xml에 DBCPServlet 등록
   <!-- oraDB DBCP Servlet -->
   <servlet>
      <servlet-name>oraDbcpServlet</servlet-name>
      <servlet-class>com.junducki.blog.dbcp.GoormDBCPServlet</servlet-class>
      <init-param>
         <param-name>name</param-name>
         <param-value>oraDBCP</param-value>
      </init-param>
      <init-param>
         <param-name>driverClassName</param-name>
         <param-value>oracle.jdbc.driver.OracleDriver</param-value>
      </init-param>
      <init-param>
         <param-name>url</param-name>
         <param-value>jdbc:oracle:thin:@xxx.xxx.xxx.xxx:1521:ora</param-value>
      </init-param>
      <init-param>
         <param-name>username</param-name>
         <param-value>scott</param-value>
      </init-param>
      <init-param>
         <param-name>password</param-name>
         <param-value>tiger</param-value>
      </init-param>
      <init-param>
         <param-name>maxActive</param-name>
         <param-value>10</param-value>
      </init-param>
      <init-param>
         <param-name>maxIdle</param-name>
         <param-value>3</param-value>
      </init-param>
      <init-param>
         <param-name>maxWait</param-name>
         <param-value>20000</param-value>
      </init-param>
      <init-param>
         <param-name>validationQuery</param-name>
         <param-value>SELECT 1 FROM DUAL</param-value>
      </init-param>
      <load-on-startup>1</load-on-startup>
   </servlet>

-- JSP Sample
   <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
   <%@ page import="javax.naming.*, javax.sql.*, java.sql.*, com.junducki.blog.dbcp.*" %>
   <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

   <html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
      <title>Blog Welcome Page</title>
   </head>
  
<body>
   <%
      Connection conn = null;
      Statement stmt = null;
      ResultSet rs = null;

      try {
         conn = DBCPServlet.getDBCPConnection("oraDBCP");
         stmt = conn.createStatement();
         rs = stmt.executeQuery("SELECT TO_CHAR(SYSDATE, 'YYYY/MM/DD HH24:MI:SS') FROM DUAL");
         while(rs.next()) {
            out.println("DataBase Date :: " + rs.getString(1));
         }
      } catch (Exception e) {
         e.printStackTrace();
      } finally {
         try {
            rs.close();
            stmt.close();
            conn.close();
         } catch(SQLException e) {}
      }
   %>
   </body>
   </html>

Posted by as.wind.914
Dev Note/Tomcat2008. 3. 14. 15:41

WAS로 Tomcat을 사용하면 DB Connection Pool로 DBCP를 많이들 사용할 것이다.
여기서 제가 한가지 짚고 넘어갈 것은 DBCP 설정을 server.xml에 하든 Context Path XML에 설정을 하든 문제가 되는 부분은 DataBase의 url, username, password 등 DB에 대한 정보가 그대로 파일에 명시된다. 그럼 만약 Web Server가 해킹이 된다면, DB의 연결 정보는 모두 노출되는 보안의 취약점으 발생하게 된다.
WebLogic과 같은 상용 WAS들은 대부분이 DB User의 Password를 암호화하여 저장되어 있다.
그럼 Tomcat에서 DBCP를 사용할 때는 상용 WAS와 같이 설정 정보를 암호화 하여 사용할 수는 없는것인가? 당연히 가능하다. DBCP에 있는 DataSourceFactory를 조금만 변경하면 됩니다.

-- Default DBCP Setting
   <Context path="test" docBase="d:/www-root/test/webapp" debug="5" reloadable="true" crossContext="true">
     
<Resource name="jdbc/testDB" auth="Container" type="javax.sql.DataSource"
            factory="org.apache.commons.dbcp.BasicDataSourceFactory"
            initialSize="5" maxActive="10" maxIdle="5" maxWait="15000"
            username="test" password="1234" driverClassName="oracle.jdbc.driver.OracleDriver"
            url="jdbc:oracle:thin:@xxx.xxx.xxx.xxx:1521:ora" validationQuery="SELECT 1 FROM DUAL"
            removeAbandoned="false" removeAbandonedTimeout="120" logAbandoned="false" />
   </Context>
위 설정은 기본으로 설정 된 DBCP이다. 여기서 factory, username, password, url 부분을 수정하여 DB 접속 정보를 암호화 해볼것이다. 물론 다른 정보들도 암호화가 가능하다.

-- DBCP 설정 정보 암호화하기
일단 먼저 factory를 수정해야 한다. org.apache.commons.dbcp.BasicDataSourceFacroty의 소스를 조금 변경하여 암호화 된 정보를 복호화하여 적용하는 부분이 추가된 새로운 DataSourceFactory를 만든다.

   package com.junducki.blog.dbcp;
   
   import java.io.ByteArrayInputStream;
   import java.io.UnsupportedEncodingException;
   import java.sql.Connection;
   import java.util.Enumeration;
   import java.util.Hashtable;
   import java.util.Properties;
   import javax.naming.Context;
   import javax.naming.Name;
   import javax.naming.RefAddr;
   import javax.naming.Reference;
   import javax.naming.spi.ObjectFactory;
   import javax.sql.DataSource;
   import org.apache.commons.dbcp.BasicDataSource;
   import org.apache.commons.logging.Log;
   import org.apache.commons.logging.LogFactory;
   import com.goorm.common.security.SeedCipher;
   import com.goorm.common.util.StringUtil;
   import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;

   /**
      * Tomcat의 Resource(DBCP)를 Context Path XML에 설정하여 사용할때, DataBase
      * url/username/password 부분을 암호화하여 사용하기 위한 DataSourceFactory
      *
      * @author jinuk jung, junducki@naver.com
      * @version 1.0, 2008. 03. 13
      */

   public class EncryptDataSourceFacroty implements ObjectFactory {
      protected static final int UNKNOWN_TRANSACTIONISOLATION = -1;

      private final static String PROP_DEFAULTAUTOCOMMIT = "defaultAutoCommit";
      private final static String PROP_DEFAULTREADONLY = "defaultReadOnly";
      private final static String PROP_DEFAULTTRANSACTIONISOLATION = "defaultTransactionIsolation";
      private final static String PROP_DEFAULTCATALOG = "defaultCatalog";
      private final static String PROP_DRIVERCLASSNAME = "driverClassName";
      private final static String PROP_MAXACTIVE = "maxActive";
      private final static String PROP_MAXIDLE = "maxIdle";
      private final static String PROP_MINIDLE = "minIdle";
      private final static String PROP_INITIALSIZE = "initialSize";
      private final static String PROP_MAXWAIT = "maxWait";
      private final static String PROP_TESTONBORROW = "testOnBorrow";
      private final static String PROP_TESTONRETURN = "testOnReturn";
      private final static String PROP_TIMEBETWEENEVICTIONRUNSMILLIS = "timeBetweenEvictionRunsMillis";
      private final static String PROP_NUMTESTSPEREVICTIONRUN = "numTestsPerEvictionRun";
      private final static String PROP_MINEVICTABLEIDLETIMEMILLIS = "minEvictableIdleTimeMillis";
      private final static String PROP_TESTWHILEIDLE = "testWhileIdle";
      private final static String PROP_PASSWORD = "password";
      private final static String PROP_URL = "url";
      private final static String PROP_USERNAME = "username";
      private final static String PROP_VALIDATIONQUERY = "validationQuery";
      private final static String PROP_ACCESSTOUNDERLYINGCONNECTIONALLOWED
            = "accessToUnderlyingConnectionAllowed";
      private final static String PROP_REMOVEABANDONED = "removeAbandoned";
      private final static String PROP_REMOVEABANDONEDTIMEOUT = "removeAbandonedTimeout";
      private final static String PROP_LOGABANDONED = "logAbandoned";
      private final static String PROP_POOLPREPAREDSTATEMENTS = "poolPreparedStatements";
      private final static String PROP_MAXOPENPREPAREDSTATEMENTS = "maxOpenPreparedStatements";
      private final static String PROP_CONNECTIONPROPERTIES = "connectionProperties";

      private final static String[] ALL_PROPERTIES = { PROP_DEFAULTAUTOCOMMIT,
            PROP_DEFAULTREADONLY, PROP_DEFAULTTRANSACTIONISOLATION, PROP_DEFAULTCATALOG,
            PROP_DRIVERCLASSNAME, PROP_MAXACTIVE, PROP_MAXIDLE, PROP_MINIDLE,
            PROP_INITIALSIZE, PROP_MAXWAIT, PROP_TESTONBORROW, PROP_TESTONRETURN,
            PROP_TIMEBETWEENEVICTIONRUNSMILLIS, PROP_NUMTESTSPEREVICTIONRUN,
            PROP_MINEVICTABLEIDLETIMEMILLIS, PROP_TESTWHILEIDLE, PROP_PASSWORD,
            PROP_URL, PROP_USERNAME, PROP_VALIDATIONQUERY,
            PROP_ACCESSTOUNDERLYINGCONNECTIONALLOWED, PROP_REMOVEABANDONED,
            PROP_REMOVEABANDONEDTIMEOUT, PROP_LOGABANDONED,
            PROP_POOLPREPAREDSTATEMENTS, PROP_MAXOPENPREPAREDSTATEMENTS,
            PROP_CONNECTIONPROPERTIES };

      /** logger instance */
      protected static Log logger = LogFactory.getLog(GoormDataSourceFacroty.class);

      /** DataSource Factory Name */
      private static String dsFactoryName = null;

      public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment)
            throws Exception {
         dsFactoryName = name.toString();

         // We only know how to deal with <code>javax.naming.Reference</code>s
         // that specify a class name of "javax.sql.DataSource"
         if ((obj == null) || !(obj instanceof Reference)) {
            return null;
         }
         Reference ref = (Reference) obj;
         if (!"javax.sql.DataSource".equals(ref.getClassName())) {
            return null;
         }

         Properties properties = new Properties();
         for (int i = 0; i < ALL_PROPERTIES.length; i++) {
            String propertyName = ALL_PROPERTIES[i];
            RefAddr ra = ref.get(propertyName);
            if (ra != null) {
               String propertyValue = ra.getContent().toString();
               properties.setProperty(propertyName, propertyValue);
            }
         }

         return createDataSource(properties);
      }

      public static DataSource createDataSource(Properties properties)
            throws Exception {
         BasicDataSource dataSource = new BasicDataSource();
         String value = null;
         StringBuilder trace = null;

         value = properties.getProperty(PROP_DRIVERCLASSNAME);
         if (value != null) {
               dataSource.setDriverClassName(value);
         }

        
value = properties.getProperty(PROP_URL);
         if (value != null) {
            dataSource.setUrl(decryptDBCPProperty(value));
         }

         value = properties.getProperty(PROP_USERNAME);
         if (value != null) {
            dataSource.setUsername(decryptDBCPProperty(value));
         }

         value = properties.getProperty(PROP_PASSWORD);
         if (value != null) {
            dataSource.setPassword(decryptDBCPProperty(value));
         }

         value = properties.getProperty(PROP_DEFAULTAUTOCOMMIT);
         if (value != null) {
            dataSource.setDefaultAutoCommit(Boolean.valueOf(value).booleanValue());
         }

         value = properties.getProperty(PROP_DEFAULTREADONLY);
         if (value != null) {
               dataSource.setDefaultReadOnly(Boolean.valueOf(value).booleanValue());
         }
     
         value = properties.getProperty(PROP_DEFAULTTRANSACTIONISOLATION);
         if (value != null) {
            int level = UNKNOWN_TRANSACTIONISOLATION;
            if ("NONE".equalsIgnoreCase(value)) {
               level = Connection.TRANSACTION_NONE;
            } else if ("READ_COMMITTED".equalsIgnoreCase(value)) {
               level = Connection.TRANSACTION_READ_COMMITTED;
            } else if ("READ_UNCOMMITTED".equalsIgnoreCase(value)) {
               level = Connection.TRANSACTION_READ_UNCOMMITTED;
            } else if ("REPEATABLE_READ".equalsIgnoreCase(value)) {
               level = Connection.TRANSACTION_REPEATABLE_READ;
            } else if ("SERIALIZABLE".equalsIgnoreCase(value)) {
               level = Connection.TRANSACTION_SERIALIZABLE;
            } else {
               try {
                  level = Integer.parseInt(value);
               } catch (NumberFormatException e) {
                  System.err.println("Could not parse defaultTransactionIsolation: " + value);
                  System.err.println("WARNING: defaultTransactionIsolation not set");
                  System.err.println("using default value of database driver");

                  level = UNKNOWN_TRANSACTIONISOLATION;
               }
            }
            dataSource.setDefaultTransactionIsolation(level);
         }

          value = properties.getProperty(PROP_DEFAULTCATALOG);
         if (value != null) {
            dataSource.setDefaultCatalog(value);
         }

         value = properties.getProperty(PROP_MAXACTIVE);
         if (value != null) {
               dataSource.setMaxActive(Integer.parseInt(value));
         }

         value = properties.getProperty(PROP_MAXIDLE);
         if (value != null) {
            dataSource.setMaxIdle(Integer.parseInt(value));
         }

         value = properties.getProperty(PROP_MINIDLE);
         if (value != null) {
            dataSource.setMinIdle(Integer.parseInt(value));
         }

         value = properties.getProperty(PROP_INITIALSIZE);
         if (value != null) {
            dataSource.setInitialSize(Integer.parseInt(value));
         }

         value = properties.getProperty(PROP_MAXWAIT);
         if (value != null) {
            dataSource.setMaxWait(Long.parseLong(value));
         }
     
          value = properties.getProperty(PROP_TESTONBORROW);
         if (value != null) {
            dataSource.setTestOnBorrow(Boolean.valueOf(value).booleanValue());
         }

         value = properties.getProperty(PROP_TESTONRETURN);
         if (value != null) {
            dataSource.setTestOnReturn(Boolean.valueOf(value).booleanValue());
         }

         value = properties.getProperty(PROP_TIMEBETWEENEVICTIONRUNSMILLIS);
         if (value != null) {
            dataSource.setTimeBetweenEvictionRunsMillis(Long.parseLong(value));
         }

         value = properties.getProperty(PROP_NUMTESTSPEREVICTIONRUN);
         if (value != null) {
            dataSource.setNumTestsPerEvictionRun(Integer.parseInt(value));
         }

         value = properties.getProperty(PROP_MINEVICTABLEIDLETIMEMILLIS);
         if (value != null) {
            dataSource.setMinEvictableIdleTimeMillis(Long.parseLong(value));
         }

         value = properties.getProperty(PROP_TESTWHILEIDLE);
         if (value != null) {
            dataSource.setTestWhileIdle(Boolean.valueOf(value).booleanValue());
         }

         value = properties.getProperty(PROP_VALIDATIONQUERY);
         if (value != null) {
            dataSource.setValidationQuery(value);
         }

         value = properties.getProperty(PROP_ACCESSTOUNDERLYINGCONNECTIONALLOWED);
         if (value != null) {
            dataSource.setAccessToUnderlyingConnectionAllowed(Boolean.valueOf(value).booleanValue());
         }

         value = properties.getProperty(PROP_REMOVEABANDONED);
         if (value != null) {
            dataSource.setRemoveAbandoned(Boolean.valueOf(value).booleanValue());
         }

         value = properties.getProperty(PROP_REMOVEABANDONEDTIMEOUT);
         if (value != null) {
            dataSource.setRemoveAbandonedTimeout(Integer.parseInt(value));
         }

         value = properties.getProperty(PROP_LOGABANDONED);
         if (value != null) {
            dataSource.setLogAbandoned(Boolean.valueOf(value).booleanValue());
         }

         value = properties.getProperty(PROP_POOLPREPAREDSTATEMENTS);
         if (value != null) {
            dataSource.setPoolPreparedStatements(Boolean.valueOf(value).booleanValue());
         }

         value = properties.getProperty(PROP_MAXOPENPREPAREDSTATEMENTS);
         if (value != null) {
            dataSource.setMaxOpenPreparedStatements(Integer.parseInt(value));
         }

         value = properties.getProperty(PROP_CONNECTIONPROPERTIES);
         if (value != null) {
            Properties p = getProperties(value);
            Enumeration e = p.propertyNames();
            while (e.hasMoreElements()) {
               String propertyName = (String) e.nextElement();
               dataSource.addConnectionProperty(propertyName, p.getProperty(propertyName));
            }
         }

         return dataSource;
      }

      static private Properties getProperties(String propText) throws Exception {
         Properties p = new Properties();
         if (propText != null) {
            p.load(new ByteArrayInputStream(propText.replace(';', '\n').getBytes()));
         }
     
         return p;
      }

     
/**
         * 암호화 된 DBCP Property를 복호화해 준다.
         *
         * @param encryptStr String 암호화 된 Config 값
         * @return String 복호화 된 Config 값
         */
      private static String decryptDBCPProperty(String encryptStr) {
         SeedCipher seed = new SeedCipher();

         try {
               return seed.decryptAsString(Base64.decode(encryptStr), key.getBytes(), "UTF-8");
         } catch (UnsupportedEncodingException e) {
            return seed.decryptAsString(Base64.decode(encryptStr), key.getBytes());
         }
      }

   }

위의 소스를 보시면 BasicDataSourceFactory가 어떻게 수정되었는지를 금방 알수 있을 것이다. 변경되거나 추가된 부분은 소스 색을 다르게 해 놨다.
decryptDBCPProperty(String encryptStr)은 설정 된 암호화 된 properties를 복호화하기 위해 추가 된 것으로 여기에 암호화는 SEED API를 적용해 놨다. key는 변경하거나 룰을 가져가면 된다. 물론 다른 암호화 알고리즘을 적용해도 된다. 대신 설정 된 암호화 properties와 복호화 하는 부분에는 동일한 키와 알고리즘을 사용해야 한다.
getObjectInstance() 함수의 달라진 부분은 읽어들인 properties를 decryptDBCPProperty()를 이용하여 복호화하여 적용하게끔 변경되었다. url, username, password 부분만 현재는 적용 됨.

그럼 위의 EncryptDataSourceFacroty가 적용 된 DBCP 설정은 어떻게 달라지는가? 위의 DBCP 설정을 변경해 보면
   <Context path="test" docBase="d:/www-root/test/webapp" debug="5" reloadable="true" crossContext="true">
      <Resource name="jdbc/testDB" auth="Container" type="javax.sql.DataSource"
            factory="com.junducki.blog.dbcpEncryptDataSourceFacroty"
            initialSize="5" maxActive="10" maxIdle="5" maxWait="15000"
            username="Owm8SZ89IPJLdu2pR84DQg==" password="3N+gGSCi3+9N2v43K18e5A==" 
            driverClassName="oracle.jdbc.driver.OracleDriver"
            url="8l1Z5t8i0tpJJaMA0hE5NjAfbfQL1UXxNEKWxyPOYXkuB1bMqmOGir5QP9G9S/wh" 
            validationQuery="SELECT 1 FROM DUAL"
            removeAbandoned="false" removeAbandonedTimeout="120" logAbandoned="false" />
   </Context>
위와 같이 factory 부분은 변경 된 DataSourceFactory가 적용되었고, url, username, password는 암호화 된 정보로 설정되어 있다.

위 변경 전후의 설정은 맞지가 않다. 이 Factory는 제가 운영하는 Tomcat에 적용되어 있기 때문에 암호화 방식과 키관리 룰은 공개를 할 수 없기 때문이다. 단, 위의 내용으로 조금만 수정하면 쉽게 적용할 수 있을 것이다.


Posted by as.wind.914
Dev Note/Tomcat2008. 3. 10. 19:23
Java로 Web Service를 하기 위해 DB Connection Pool을 사용하지 않는 것은 이제 상상도 할 수가 없다. Java로 된 DB Connection Pool API는 수도 없이 많다. 대부분의 상용 WAS에서는 자기네 벤더에서 제공하는 DB Connection Pool이 있고, 그 외에도 무료로 제공되는 많은 DB Connection Pool 들이 있다. DBCP도 그중 하나이다.

DBCP(DataBase Connection Polling Service)는 Apache의 많은 Project 중 하나로 DataBase의 연결을 Pool에 생성해 놓고 재활용 및 관리를 가능하게 해주는 API이다. 여기서는 DBCP를 Tomcat에서 사용하기 위한 설정에 대해서 알아본다.

-- DBCP Install
Jakarta Commons DBCP Download : http://commons.apache.org/dbcp/downloads.html
Jakarta Commons Pool Download : http://commons.apache.org/pool/downloads.html
   è Generic object pooling component
Jakarta Commons Collections Download : http://commons.apache.org/downloads/download_collections.cgi
   è Extends or augments the Java Collections Framework
Java JDBC Driver : 사용하는 DataBase의 벤더가 제공하는 Driver를 사용한다.
Jakarta Commons DBCP API Document : http://commons.apache.org/dbcp/apidocs/index.html

위의 파일들을 다운 받은 jar 파일을 $CATALINA_HOME/common/lib 폴더에 복사한다. /WEB-INF/lib에 넣어두어도 무방하나, DB Connection Pool은 거의 모든 Context에서 사용하므로 모든 Context path의 /WEB-INF/lib 밑에 넣어두는 것보다는 $CATALINA_HOME/common/lib 밑에 넣어두는 것이 좋다.

-- DBCP Configuration Parameters
* Mandatory Parameters *
username : JDBC Driver로 연결할 DataBase의 사용자 계정 이름
password : JDBC Driver로 연결할 DataBase의 사용자 계정 비밀번호
url : JDBC Driver로 연결할 DataBase의 URL
driverClassName : DataBase 연결에 사용할 JDBC Driver Class 이름 (full path)

* Option Parameters - () 안의 값은 Default *
connectionProperties : DataBase 연결 시 사용할 Properties (charset 등 ...)

defaultAutoCommit (true) : DB Connection의 기본 auto-commit 상태 값
defaultReadOnly (driver default) : Pool에 DB Connection이 생성 될 때의 기본 read-only 상태 값
defaultTransactionIsolation (driver default) : Pool에 DB Connection이 생성 될 때의 기본 TransactionIsolation 상태 값. (NONE, READ_COMMITTED, READ_UNCOMMITED, REPEATABLE_READ, SERIALIZABLE 중 하나)
defaultCatalog (driver default) : Pool에 DB Connection이 생성 될 때의 기본 catalog 값

initialSize (0) : pool이 최초 동작할 때 생성 할 DB Connection
maxActive (8) : pool 내에서 동시에 최대로 생성할 수 있는 DB Connection (음수는 no limit)
maxIdle (8) : pool 내에서 대기 상태로 존재할 수 있는 최대 DB Connection (음수는 no limit)
minIdle (0) : pool 내에서 대기 상태로 존재할 수 있는 최소 DB Connection
maxWait (indefinitely) : pool에 DB Connection이 반환 될 최대 milliseconds (음수는 indefinitely)

validationQuery : DB Connection이 caller에게서 주거나 반환되기 전 해당 Connection이 사용가능한지 확인하는 쿼리
testOnBorrow (true) : DB Connection의 사용가능 여부 확인을 Caller가 pool에서 가져가지 전에 확인하는 것으로 지정한다. 만약 확인결과 사용불가능이면 pool에서 제거하고 다른 Connection을 준다.
NOTE - 이 값이 true이면 validationQuery 값은 non-null string 이어야 한다.
testOnReturn (false) : DB Connection의 사용가능 여부 확인 Caller가 pool에 반환하기 전에 확인하는 것으로 지정한다. 만약 확인결과 사용불가능이면 pool에서 제거하고 다른 Connection을 준다.
NOTE - 이 값이 true이면 validationQuery 값은 non-null string 이어야 한다.
testWhileIdle (false) : 유효하지 않은 DB Connection은 pool에서 제거한다.
NOTE - 이 값이 true이면 validationQuery 값은 non-null string 이어야 한다.
timeBetweenEvictionRunsMillis (-1) :  사용되지 않는 DB Connection을 추출하는 Thread의 실행주기 (단위 : milliseconds)
numTestsPerEvictionRun (3) : 사용되지 않는 Connection을 몇개 검사할지 지정
minEvictableIdleTimeMillis (1000 * 60 * 30) : 지정 시간만큼 대기상태인 DB Connection만 추출한다. (단위 : milliseconds)
poolPreparedStatements (false) : PreparedStatements가 풀링되어야 하는지 아닌지 설정
maxOpenPreparedStatements (unlimited) : open 된 최대 PreparedStatements 수

removeAbandoned (false) : 대기 상태로 removeAbandonedTimeout 이상동안 유지되면 Connection을 버릴 것인지 여부 설정
removeAbandonedTimeoout : removeAbandoned 하기 위한 Timeout (단위 : second)
logAbandoned : removeAbandoned에 의해 Connection을 버릴 때 로그로 남길지 여부

-- 설정 예
Context에 설정하기
   <Context path="dbcp" docBase="/home/app/www-root/dbcp/webapp" debug="5"
         reloadable="false" crossContext="true">
      <Resource name="jdbc/dbcpDB" auth="Container" type="javax.sql.DataSource"
            factory="org.apache.commons.dbcp.BasicDataSourceFactory"
            driverClassName="해당 JDBC Driver Class (oracle.jdbc.driver.OracleDriver)"
            url="jdbc:oracle:thin:@xxx.xxx.xxx.xxx:[port]:[sid]"
            username="DataBase User Name (scott)" password="DataBase User Password (tiger)"
            initialSize="1" maxActive="10" maxIdle="5" maxWait="15000"
            removeAbandoned="true" removeAbandonedTimeout="120" logAbandoned="false"
      />
   />

web.xml 추가 설정
   <resource-ref>
      <description>DB Connection</description>
      <res-ref-name>jdbc/dbcpDB</res-ref-name> <!-- Context에 설정한 DBCP의 이름 -->
      <res-tyep>javax.sql.DataSource</res-type>
      <res-auth>Container</res-auth>
   </resource-ref>

이상이 DBCP 설정 방법 중 Context에 설정하는 방법에 대해 알아보았다. DBCP를 설정하는 방법은 여기서 소개한것 외에도 많다. 다음에는 DBCP를 web.xml에 Servlet 처럼 등록하여 설정정보를 암호화하여 로딩하는 방법과 예제 소스를 다뤄볼 것이다.
Posted by as.wind.914