본문 바로가기
개발, IT

서블렛 + JDBC 연동시 코딩 고려사항 -제2탄-

by Nabi™ 2008. 5. 22.

5. JDBC Connection Pooling 을 왜 사용해야 하는가 ?

 Pooling 이란 용어는 일반적인 용어입니다. Socket Connection Pooling, Thread
 Pooling, Resource Pooling 등 "어떤 자원을 미리 Pool 에 준비해두고 요청시 Pool에
 있는 자원을 곧바로 꺼내어 제공하는 기능"인 거죠.

 JDBC Connection Pooling 은 JDBC를 이용하여 자바에서 DB연결을 할 때, 미리 Pool에
 물리적인 DB 연결을 일정개수 유지하여 두었다가 어플리케이션에서 요구할 때 곧바로
 제공해주는 기능을 일컫는 용어입니다. JDBC 연결시에, (DB 종류마다, 그리고 JDBC
 Driver의 타입에 따라 약간씩 다르긴 하지만 ) 대략 200-400 ms 가 소요 됩니다. 기껏
 0.2 초 0.4 초 밖에 안되는 데 무슨 문제냐 라고 반문할수도 있습니다.
 
 하지만, 위 시간은 하나의 연결을 시도할 때 그러하고, 100 - 200개를 동시에 연결을
 시도하면 얘기가 완전히 달라집니다.

 아래는 직접 JDBC 드라이버를 이용하여 연결할 때와 JDBC Connection Pooling 을 사용
 할 때의 성능 비교 결과입니다.


 ------------------------------------------------------------------
 테스트 환경
 LG-IBM 570E Notebook(CPU:???MHz , MEM:320MB)
 Windows NT 4.0 Service Pack 6
 IBM WebSphere 3.0.2.1 + e-Fixes
 IBM HTTP Server 1.3.6.2
 IBM UDB DB2 6.1

 아래에 첨부한 파일는 자료를 만들때 사용한 JSP소스입니다. (첨부파일참조)

 [HttpConn.java] 간단한 Stress Test 프로그램


 아래의 수치는 이 문서 이외에는 다른 용도로 사용하시면 안됩니다. 테스트를 저의
 개인 노트북에서 측정한 것이고, 또한 테스트 프로그램 역시 직접 만들어한 것인
 만큼, 공정성이나 수치에 대한 신뢰를 부여할 수는 없습니다.
 그러나 JDBC Connection Pooling 적용 여부에 따른 상대적인 차이를 설명하기에는
 충분할 것 같습니다.

 테스트 결과

 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 매번 직접 JDBC Driver 연결하는 경우
 java HttpConn http://localhost/db_jdbc.jsp -c <동시유저수> -n <호출횟수> -s 0

 TOTAL( 1,10)  iteration=10 ,  average=249.40 (ms),   TPS=3.93
 TOTAL(50,10)  iteration=500 , average=9,149.84 (ms), TPS=4.83
 TOTAL(100,10)  iteration=1000 , average=17,550.76 (ms), TPS=5.27
 TOTAL(200,10)  iteration=2000 , average=38,479.03 (ms), TPS=4.89
 TOTAL(300,10)  iteration=3000 , average=56,601.89 (ms), TPS=5.01

 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 DB Connection Pooing 을 사용하는 경우
 java HttpConn http://localhost/db_pool_cache.jsp -c <동시유저수> -n <호출횟수> -s 0

 TOTAL(1,10)  iteration=10 , average=39.00 (ms), TPS=23.26
 TOTAL(1,10)  iteration=10 , average=37.10 (ms), TPS=24.33
 TOTAL(50,10)  iteration=500 , average=767.36 (ms), TPS=45.27
 TOTAL(50,10)  iteration=500 , average=568.76 (ms), TPS=61.26
 TOTAL(50,10)  iteration=500 , average=586.51 (ms), TPS=59.79
 TOTAL(50,10)  iteration=500 , average=463.78 (ms), TPS=67.02
 TOTAL(100,10)  iteration=1000 , average=1,250.07 (ms), TPS=57.32
 TOTAL(100,10)  iteration=1000 , average=1,022.75 (ms), TPS=61.22
 TOTAL(200,10)  iteration=1462 , average=1,875.68 (ms), TPS=61.99
 TOTAL(300,10)  iteration=1824 , average=2,345.42 (ms), TPS=61.51

 NOTE: average:평균수행시간, TPS:초당 처리건수
 ------------------------------------------------------------------

 즉, JDBC Driver 를 이용하여 직접 DB연결을 하는 구조는 기껏 1초에 5개의 요청을
 처리할 수 있는 능력이 있는 반면, DB Connection Pooling 을 사용할 경우는 초당
 60여개의 요청을 처리할 수 있는 것으로 나타났습니다.
 12배의 성능향상을 가져온거죠. (이 수치는 H/W기종과 측정방법, 그리고 어플리케이션에
 따라 다르게 나오니 이 수치자체에 너무 큰 의미를 두진 마세요)


 주의: 흔히 "성능(Performance)"을 나타낼 때, 응답시간을 가지고 얘기하는 경향이
  있습니다. 그러나 응답시간이라는 것은 ActiveUser수가 증가하면 당연히 그에 따라
  느려지게 됩니다. 반면 "단위시간당 처리건수"인 TPS(Transaction Per Second) 혹은
  RPS(Request Per Second)는 ActiveUser를 지속적으로 끌어올려 임계점을 넘어서면,
  특정수치 이상을 올라가지 않습니다. 따라서 성능을 얘기할 땐 평균응답시간이 아니라
  "단위시간당 최대처리건수"를 이야기 하셔야 합니다.
  성능(Performance)의 정의(Definition)은 "단위시간당 최대처리건수"임을 주지하세요.
  성능에 관련한 이론은 아래의 문서를 통해, 함께 연구하시지요.
  [강좌]웹기반시스템하에서의 성능에 대한 이론적 고찰
  http://www.javaservice.net/~java/bbs/read.cgi?m=resource&b=consult&c=r_p&n=1008701211

 PS: IBM WebSphere V3 의 경우, Connection 을 가져올 때, JNDI를 사용하게 되는데
   이때 사용되는 DataSource 객체를 매번 initialContext.lookup() 을 통해 가져오게
   되면, 급격한 성능저하가 일어납니다.(NOTE: V4부터는 내부적으로 cache를 사용하여
   성능이 보다 향상되었습니다)
   DataSource를 매 요청시마다 lookup 할 경우 다음과 같은 결과를 가져왔습니다.

   java HttpConn http://localhost/db_pool.jsp -c <동시유저수> -n <호출횟수> -s 0

   TOTAL(1,10)  iteration=10 , average=80.00 (ms), TPS=11.61
   TOTAL(50,10)  iteration=500 , average=2,468.30 (ms), TPS=16.98
   TOTAL(50,10)  iteration=500 , average=2,010.43 (ms), TPS=18.18
   TOTAL(100,10)  iteration=1000 , average=4,377.24 (ms), TPS=18.16
   TOTAL(200,10)  iteration=1937 , average=8,991.89 (ms), TPS=18.12

   TPS 가 18 이니까 DataSource Cache 를 사용할 때 보다 1/3 성능밖에 나오지 않는 거죠.


6. JDBC Connection Pooling 을 사용하여 코딩할 때 고려사항

 JDBC Connecting Pooling은 JDBC 1.0 스펙상에 언급되어 있지 않았습니다. 그러다보니
 BEA WebLogic, IBM WebSphere, Oracle OAS, Inprise Server, Sun iPlanet 등 어플리케
 이션 서버라고 불리는 제품들마다 그 구현방식이 달랐습니다.
 인터넷에서 돌아다니는 Hans Bergsten 이 만든 DBConnectionManager.java 도 그렇고,
 JDF 에 포함되어 있는 패키지도 그렇고 각자 독특한 방식으로 개발이 되어 있습니다.
 
 JDBC를 이용하여 DB연결하는 대표적인 코딩 예를 들면 다음과 같습니다.
 
 ------------------------------------------------------------------
 [BEA WebLogic Application Server]

    import java.sql.*;

    // Driver loading needed.
    static {
      try {
        Class.forName("weblogic.jdbc.pool.Driver").newInstance();         
      }
      catch (Exception e) {
        ...
      }
    }

    ......

    Connection conn = null;
    Statement stmt = null;
    try {
      conn = DriverManager.getConnection("jdbc:weblogic:pool:<pool_name>", null);
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("select ....");
      while(rs.next()){
        .....
      }
      rs.close();
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( conn != null ) try{conn.close();}catch(Exception e){}
    }


 ------------------------------------------------------------------
 IBM WebSphere Application Server 3.0.2.x / 3.5.x

    /* IBM WebSphere 3.0.2.x for JDK 1.1.8 */
    //import java.sql.*;
    //import javax.naming.*;
    //import com.ibm.ejs.dbm.jdbcext.*;
    //import com.ibm.db2.jdbc.app.stdext.javax.sql.*;

    /* IBM WebSphere 3.5.x for JDK 1.2.2 */
    import java.sql.*;
    import javax.sql.*;
    import javax.naming.*;

    // DataSource Cache 사용을 위한 ds 객체 static 초기화
    private static DataSource ds = null;
    static {
      try {
        java.util.Hashtable props = new java.util.Hashtable();
        props.put(Context.INITIAL_CONTEXT_FACTORY,
                  "com.ibm.ejs.ns.jndi.CNInitialContextFactory");
        Context ctx = null;
        try {
          ctx = new InitialContext(props);
          ds = (DataSource)ctx.lookup("jdbc/<data_source_name>");
        }
        finally {
          if ( ctx != null ) ctx.close();
        }
      }
      catch (Exception e) {
       ....
      }
    }

    .....  

    Connection conn = null;
    Statement stmt = null;
    try {
      conn = ds.getConnection("userid", "password");
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("select ....");
      while(rs.next()){
        .....
      }
      rs.close();
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( conn != null ) try{conn.close();}catch(Exception e){}
    }

 ------------------------------------------------------------------
 IBM WebSphere Application Server 2.0.x
 
    import java.sql.*;
    import com.ibm.servlet.connmgr.*;


    // ConnMgr Cache 사용을 위한 connMgr 객체 static 초기화

    private static IBMConnMgr connMgr = null;
    private static IBMConnSpec spec = null;
    static {
      try {
        String poolName = "JdbcDb2";  // defined in WebSphere Admin Console
        spec = new IBMJdbcConnSpec(poolName, false, 
              "com.ibm.db2.jdbc.app.DB2Driver",
              "jdbc:db2:<db_name>",   // "jdbc:db2://ip_address:6789/<db_name>",
              "userid","password");    
        connMgr = IBMConnMgrUtil.getIBMConnMgr();
      }
      catch(Exception e){
        .....
      }
    }

    .....

    IBMJdbcConn cmConn = null; // "cm" maybe stands for Connection Manager.
    Statement stmt = null;
    try {
      cmConn = (IBMJdbcConn)connMgr.getIBMConnection(spec);   
      Connection conn = jdbcConn.getJdbcConnection();
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("select ....");
      while(rs.next()){
        .....
      }
      rs.close();
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( cmConn != null ) try{cmConn.releaseIBMConnection();}catch(Exception e){}
    }
    // NOTE: DO NOT "conn.close();" !!


 ------------------------------------------------------------------
 Oracle OSDK(Oracle Servlet Development Kit)

    import java.sql.*;
    import oracle.ec.ctx.*;


    .....
   
    oracle.ec.ctx.Trx trx = null;
    Connection conn = null;
    Statement stmt = null;
    try {
      oracle.ec.ctx.TrxCtx ctx = oracle.ec.ctx.TrxCtx.getTrxCtx();
      trx = ctx.getTrx();
      conn = trx.getConnection("<pool_name>");
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("select ....");
      while(rs.next()){
        .....
      }
      rs.close();
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( conn != null ) try{ trx.close(conn,"<pool_name>");}catch(Exception e){}
    }
    // NOTE: DO NOT "conn.close();" !!


 ------------------------------------------------------------------
 Hans Bergsten 의 DBConnectionManager.java

    import java.sql.*;

    .....
   
    db.DBConnectionManager connMgr = null;
    Connection conn = null;
    Statement stmt = null;
    try {
      connMgr = db.DBConnectionManager.getInstance();
      conn = connMgr.getConnection("<pool_name>");
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("select ....");
      while(rs.next()){
        .....
      }
      rs.close();
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( conn != null ) connMgr.freeConnection("<pool_name>", conn);
    }
    // NOTE: DO NOT "conn.close();" !!

 ------------------------------------------------------------------
 JDF 의 DB Connection Pool Framework

    import java.sql.*;
    import com.lgeds.jdf.*;
    import com.lgeds.jdf.db.*;
    import com.lgeds.jdf.db.pool.*;


    private static com.lgeds.jdf.db.pool.JdbcConnSpec spec = null;
    static {
      try {
        com.lgeds.jdf.Config conf = new com.lgeds.jdf.Configuration();
        spec =  new com.lgeds.jdf.db.pool.JdbcConnSpec(
             conf.get("gov.mpb.pbf.db.emp.driver"),
             conf.get("gov.mpb.pbf.db.emp.url"),
             conf.get("gov.mpb.pbf.db.emp.user"),
             conf.get("gov.mpb.pbf.db.emp.password")
          );
      }
      catch(Exception e){
        .....
      }
    }

    .....

    PoolConnection poolConn = null;
    Statement stmt = null;
    try {
      ConnMgr mgr = ConnMgrUtil.getConnMgr();
      poolConn = mgr.getPoolConnection(spec);
      Connection conn = poolConnection.getConnection();
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("select ....");
      while(rs.next()){
        .....
      }
      rs.close();
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( poolConn != null ) poolConn.release();
    }
    // NOTE: DO NOT "conn.close();" !!

 ------------------------------------------------------------------


 여기서 하고픈 얘기는 DB Connection Pool 을 구현하는 방식에 따라서 개발자의 소스도
 전부 제 각기 다른 API를 사용해야 한다는 것입니다.
 프로젝트를 이곳 저곳 뛰어 본 분은 아시겠지만, 매 프로젝트마나 어플리케이션 서버가
 다르고 지난 프로젝트에서 사용된 소스를 새 프로젝트에 그대로 적용하지 못하게 됩니다.
 JDBC 관련 API가 다르기 때문이죠.
 같은 제품일지라도 버전업이 되면서 API가 변해버리는 경우도 있습니다. 예를 들면,
 IBM WebSphere 버전 2.0.x에서 버전 3.0.x로의 전환할 때, DB 연결을 위한 API가
 변해버려 기존에 개발해둔 400 여개의 소스를 다 뜯어 고쳐야 하는 것과 같은 상황이
 벌어질 수도 있습니다.
 닷컴업체에서 특정 패키지 제품을 만들때도 마찬가지 입니다. 자사의 제품이 어떠한
 어플리케이션 서버에서 동작하도록 해야 하느냐에 따라 코딩할 API가 달라지니 소스를
 매번 변경해야만 하겠고, 특정 어플리케이션 서버에만 동작하게 하려니 마켓시장이
 좁아지게 됩니다.
 IBM WebSphere, BEA WebLogic 뿐만 아니라 Apache JServ 나 JRun, 혹은 Tomcat 에서도
 쉽게 포팅하길 원할 것입니다.

 이것을 해결하는 방법은 우리들 "SE(System Engineer)"만의 고유한 "Connection Adapter
 클래스"를 만들어서 사용하는 것입니다.

 예를 들어 개발자의 소스는 이제 항상 다음과 같은 유형으로 코딩되면 어떻겠습니까 ?


    -----------------------------------------------------------
    .....
    ConnectionResource resource = null;
    Statement stmt = null;
    try {
      resource = new ConnectionResource();
      Connection conn = resource.getConnection();

      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("select ....");
      while(rs.next()){
        .....
      }
      rs.close();
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( resource != null ) resource.release();
    }
    // NOTE: DO NOT "conn.close();" !!
    -----------------------------------------------------------


 이 때 ConnectionResource 는 다음과 같은 형식으로 누군가 한분이 만들어 두면 되겠죠.

    -----------------------------------------------------------
    import java.sql.*;
    public class ConnectionResource
    {
      private java.sql.Connection conn = null;
      public ConnectionResource() throws Exception {
         ......
         conn =  ..... GET Connection by Connection Pooling API
      }
      public Connection getConnection() throws Exception {
         return conn;
      }
      public void release(){
         // release conn into "Connection Pool"
         .......
      }
    }
    -----------------------------------------------------------


  예를 들어 IBM WebSphere Version 3.0.2.x 의 경우를 든다면 다음과 같이 될 겁니다.

  -----------------------------------------------------------
  package org.jsn.connpool;
  /*
   * ConnectionResource V 1.0
   * JDBC Connection Pool Adapter for IBM WebSphere V 3.0.x/3.5.x
   * Author: WonYoung Lee, javaservice@hanmail.net, 011-898-7904
   * Last Modified : 2000.11.10
   * NOTICS: You can re-distribute or copy this source code freely,
   *         you can NOT remove the above subscriptions.
  */

  /* IBM WebSphere 3.0.2.x for JDK 1.1.8 */
  //import java.sql.*;
  //import javax.naming.*;
  //import com.ibm.ejs.dbm.jdbcext.*;
  //import com.ibm.db2.jdbc.app.stdext.javax.sql.*;

  /* IBM WebSphere 3.5.x for JDK 1.2.2 */
  import java.sql.*;
  import javax.sql.*;
  import javax.naming.*;

  public class ConnectionResource
  {
     private static final String userid = "userid";
     private static final String password = "password"
     private static final String datasource = "jdbc/<data_source_name>";
     private static DataSource ds = null;

     private java.sql.Connection conn = null;

     public ConnectionResource() throws Exception {
       synchronized ( ConnectionResource.class ) {
         if ( ds == null ) {
           java.util.Hashtable props = new java.util.Hashtable();
           props.put(Context.INITIAL_CONTEXT_FACTORY,
              "com.ibm.ejs.ns.jndi.CNInitialContextFactory");
           Context ctx = null;
           try {
             ctx = new InitialContext(props);
             ds = (DataSource)ctx.lookup("jdbc/<data_source_name>");
           }
           finally {
             if ( ctx != null ) ctx.close();
           }
         }
       }
       conn =  ds.getConnection( userid, password );
     }
     public Connection getConnection() throws Exception {
        if ( conn == null ) throw new Exception("Connection is NOT avaiable !!");
        return conn;
     }
     public void release(){
        // release conn into "Connection Pool"
        if ( conn != null ) try { conn.close(); }catch(Excepton e){}
        conn = null;
     }
   }
   -----------------------------------------------------------
         


 
  만약 Hans Bersten 의 DBConnectionManager.java 를 이용한 Connection Pool 이라면
  다음과 같이 만들어 주면 됩니다.

   -----------------------------------------------------------
   package org.jsn.connpool;
   import java.sql.*;
   public class ConnectionResource
   {
      private String poolname = "<pool_name>";
      private Connection conn = null;
      private db.DBConnectionManager connMgr = null;

      public ConnectionResource() throws Exception {
         connMgr = db.DBConnectionManager.getInstance();
         conn =  connMgr.getConnection(poolname);
      }
      public Connection getConnection() throws Exception {
         if ( conn == null ) throw new Exception("Connection is NOT avaiable !!");
         return conn;
      }
      public void release(){
         if ( conn != null ) {
           // Dirty Transaction을 rollback시키는 부분인데, 생략하셔도 됩니다.
           boolean autoCommit = true;
           try{ autoCommit = conn.getAutoCommit(); }catch(Exception e){}
           if ( autoCommit == false ) {
             try { conn.rollback(); }catch(Exception e){}
             try { conn.setAutoCommit(true); }catch(Exception e){}
           }

           connMgr.freeConnection(poolname, conn);
           conn = null;
         }
      }
   }
   -----------------------------------------------------------


  또, Resin 1.2.x 의 경우라면 다음과 같이 될 겁니다.
  -----------------------------------------------------------
  package org.jsn.connpool;
  /*
   * ConnectionResource V 1.0
   * JDBC Connection Pool Adapter for Resin 1.2.x
   * Author: WonYoung Lee, javaservice@hanmail.net, 011-898-7904
   * Last Modified : 2000.10.18
   * NOTICS: You can re-distribute or copy this source code freely,
   *         you can NOT remove the above subscriptions.
  */
  import java.sql.*;
  import javax.sql.*;
  import javax.naming.*;
  public class ConnectionResource
  {
     private static final String datasource = "jdbc/<data_source_name>";
     private static final String userid = "userid";
     private static final String password = "password"
     private static DataSource ds = null;

     private java.sql.Connection conn = null;

     public ConnectionResource() throws Exception {
        synchronized ( ConnectionResource.class ) {
          if ( ds == null ) {
            Context env = (Context) new InitialContext().lookup("java:comp/env");
            ds = (DataSource) env.lookup(datasource);
          }
        }   
        conn =  ds.getConnection( userid, password );
     }
     public Connection getConnection() throws Exception {
        if ( conn == null ) throw new Exception("Connection is NOT avaiable !!");
        return conn;
     }
     public void release(){
        // release conn into "Connection Pool"
        if ( conn != null ) try { conn.close(); }catch(Excepton e){}
        conn = null;
     }
   }
   -----------------------------------------------------------


  Oracle 8i(8.1.6이상)에서 Oracle의 JDBC 2.0 Driver 자체가 제공하는 Connection
  Pool을 이용한다면 다음과 같이 될 겁니다.
  -----------------------------------------------------------
  package org.jsn.connpool;
  /*
   * ConnectionResource V 1.0
   * JDBC Connection Pool Adapter for Oracle JDBC 2.0
   * Author: WonYoung Lee, javaservice@hanmail.net, 011-898-7904
   * Last Modified : 2001.10.29
   * NOTICS: You can re-distribute or copy this source code freely,
   *         but you can NOT remove the above subscriptions.
  */
  import java.sql.*;
  import javax.sql.*;
  import oracle.jdbc.driver.*;
  import oracle.jdbc.pool.*;

  public class ConnectionResource
  {
     private static final String dbUrl = "jdbc:oracle:thin@192.168.0.1:1521:ORCL";
     private static final String userid = "userid";
     private static final String password = "password"
     private static OracleConnectionCacheImpl oraclePool = null;
     private static boolean initialized = false;

     private java.sql.Connection conn = null;

     public ConnectionResource() throws Exception {
        synchronized ( ConnectionResource.class ) {
          if ( initialized == false ) {
            DriverManager.registerDriver (new oracle.jdbc.driver.OracleDriver());
            oraclePool = new OracleConnectionCacheImpl();
            oraclePool.setURL(dbUrl);
            oraclePool.setUser(userid);
            oraclePool.setPassword(password);
            oraclePool.setMinLimit(10);
            oraclePool.setMaxLimit(50);
            //oraclePool.setCacheScheme(OracleConnectionCacheImpl.FIXED_WAIT_SCHEME);
            // FIXED_WAIT_SCHEME(default), DYNAMIC_SCHEME, FIXED_RETURN_NULL_SCHEME
            //oraclePool.setStmtCacheSize(0); //default is 0

            initialized = true;
          }
        }   
        conn = oraclePool.getConnection();
     }
     public Connection getConnection() throws Exception {
        if ( conn == null ) throw new Exception("Connection is NOT avaiable !!");
        return conn;
     }
     public void release(){
        // release conn into "Connection Pool"
        if ( conn != null ) try { conn.close(); }catch(Exception e){}
        conn = null;
     }
   }
   -----------------------------------------------------------

  또한 설령 Connection Pool 기능을 지금을 사용치 않더라도 향후에 적용할 계획이
  있다면, 우선은 다음과 같은 Connection Adapter 클래스를 미리 만들어 두고 이를
  사용하는 것이 효과적입니다. 향후에 이 클래스만 고쳐주면 되니까요.
  Oracle Thin Driver를 사용하는 경우입니다.

  -----------------------------------------------------------
  import java.sql.*;
  public class ConnectionResource
  {
     private static final String userid = "userid";
     private static final String password = "password"
     private static final String driver = "oracle.jdbc.driver.OracleDriver"
     private static final String url = "jdbc:oracle:thin@192.168.0.1:1521:ORCL";
     private static boolean initialized = false;

     private java.sql.Connection conn = null;

     public ConnectionResource() throws Exception {
        synchronized ( ConnectionResource.class ) {
          if ( initialized == false ) {
              Class.forName(driver);
              initialized = true;
          }
        }
        conn = DriverManager.getConnection( url, userid, password );
     }
     public Connection getConnection() throws Exception {
        if ( conn == null ) throw new Exception("Connection is NOT avaiable !!");
        return conn;
     }
     public void release(){
        if ( conn != null ) try { conn.close(); }catch(Excepton e){}
        conn = null;
     }
   }
   -----------------------------------------------------------

 
 프로그램의 유형이나, 클래스 이름이야 뭐든 상관없습니다. 위처럼 우리들만의 고유한
 "Connection Adapter 클래스"를 만들어서 사용한다는 것이 중요하고, 만약 어플리케이션
 서버가 변경된다거나, DB Connection Pooling 방식이 달라지면 해당 ConnectionResource
 클래스 내용만 살짝 고쳐주면 개발자의 소스는 전혀 고치지 않아도 될 것입니다.

 NOTE: ConnectionResource 클래스를 만들때 주의할 것은 절대 Exception 을 클래스 내부에서
   가로채어 무시하게 하지 말라는 것입니다. 그냥 그대로 throw 가 일어나게 구현하세요.
   이렇게 하셔야만 개발자의 소스에서 "Exception 처리"를 할 수 있게 됩니다.

 NOTE2: ConnectionResource 클래스를 만들때 반드시 package 를 선언하도록 하세요.
   IBM WebSphere 3.0.x 의 경우 JSP에서 "서블렛클래스패스"에 걸려 있는 클래스를
   참조할 때, 그 클래스가 default package 즉 package 가 없는 클래스일 경우 참조하지
   못하는 버그가 있습니다. 통상 "Bean"이라 불리는 클래스 역시 조건에 따라 인식하지
   않을 수도 있습니다.
   클래스 다지인 및 설계 상으로 보더라도 package 를 선언하는 것이 바람직합니다.

 NOTE3: 위에서  "userid", "password" 등과 같이 명시적으로 프로그램에 박아 넣지
   않고, 파일로 관리하기를 원한다면, 그렇게 하셔도 됩니다.
   JDF의 Configuration Framework 을 참조하세요.


 PS: 혹자는 왜 ConnectionResource 의 release() 메소드가 필요하냐고 반문할 수도 있습
   니다. 예를 들어 개발자의 소스가 다음처럼 되도록 해도 되지 않느냐라는 거죠.

    -----------------------------------------------------------
    .....
    Connection conn = null;
    Statement stmt = null;
    try {
      conn = ConnectionResource.getConnection(); // <---- !!!

      stmt = conn.createStatement();
      .....
    }
    catch(Exception e){
      .....
    }
    finally {
      if ( stmt != null ) try{stmt.close();}catch(Exception e){}
      if ( conn != null ) try{conn.close();}catch(Exception e){} // <---- !!!
    }
    -----------------------------------------------------------

  이렇게 하셔도 큰 무리는 없습니다. 그러나, JDBC 2.0 을 지원하는 제품에서만
  conn.close() 를 통해 해당 Connection 을 실제 close() 시키는 것이 아니라 DB Pool에
  반환하게 됩니다. BEA WebLogic 이나 IBM WebSphere 3.0.2.x, 3.5.x 등이 그렇습니다.
  그러나, 자체제작된 대부분의 DB Connection Pool 기능은 Connection 을 DB Pool에
  반환하는 고유한 API를 가지고 있습니다. WebSphere Version 2.0.x 에서는
  cmConn.releaseIBMConnection(), Oracle OSDK 에서는 trx.close(conn, "<pool_name">);
  Hans Bersten 의 DBConnectionManager 의 경우는
  connMgr.freeConnection(poolname, conn); 등등 서로 다릅니다. 이러한 제품들까지
  모두 지원하려면 "release()" 라는 우리들(!)만의 예약된 메소드가 꼭 필요하게 됩니다.

  물론, java.sql.Connection Interface를 implements 한 별도의 MyConnection 을 만들어
  두고, 실제 java.sql.Connection 을 얻은 후 MyConnection의 생성자에 그 reference를
  넣어준 후, 이를 return 시에 넘기도록 하게 할 수 있습니다. 이때, MyConnection의
  close() 함수를 약간 개조하여 DB Connection Pool로 돌아가게 할 수 있으니까요.


 PS: 하나 이상의 DB 를 필요로 한다면, 다음과 같은 생성자를 추가로 만들어서 구분케
  할 수도 있습니다.

  -----------------------------------------------------------
  ....
  private String poolname = "default_pool_name";
  private String userid = "scott";
  private String password = "tiger";
  public ConnectionResource() throws Exception {
    initialize();
  }
  public ConnectionResource(String poolname) throws Exception {
    this.poolname = poolname;
    initialize();
  }
  public ConnectionResource(String poolname,String userid, String passwrod)
  throws Exception
  {
    this.poolname = poolname;
    this.userid = userid;
    this.password = password;
    initialize();
  }
  private void initialize() throws Exception {
    ....
  }
  ...
  -----------------------------------------------------------


-------------------------------------------------------------------------------
2001.03.20 추가
2001.10.22 수정
2001.11.09 수정(아래 설명을 추가함)

실 운영 사이트의 장애진단 및 튜닝을 다녀보면, 장애의 원인이 DataBase 연결개수가
지속적으로 증가하고, Connection Pool 에서 더이상 가용한 연결이 남아 있지 않아
발생하는 문제가 의외로 많습니다. 이 상황의 십중팔구는 개발자의 코드에서 Pool로
부터 가져온 DB연결을 사용하고 난 후, 이를 여하한의 Exception 상황에서도 다시
Pool로 돌려보내야 한다는 Rule 를 지키지 않아서 발생한 문제가 태반입니다.
이름만 말하면 누구나 알법한 큼직한 금융/뱅킹사이트의 프로그램소스에서도 마찬가지
입니다.
문제는 분명히 어떤 특정 응용프로그램에서 DB Connection 을 제대로 반환하지 않은
것은 분명한데, 그 "어떤 특정 응용프로그램"이 꼭집어 뭐냐 라는 것을 찾아내기란
정말 쉽지 않습니다. 정말 쉽지 않아요. 1초당 수십개씩의 Request 가 다양하게 들어
오는 상황에서, "netstat -n"으로 보이는 TCP/IP 레벨에서의 DB연결수는 분명히 증가
하고 있는데, 그 수십개 중 어떤 것이 문제를 야기하느냐를 도저히 못찾겠다는 것이지요.
사용자가 아무도 없는 새벽에 하나씩 컨텐츠를 꼭꼭 눌러 본들, 그 문제의 상황은
대부분 정상적인 로직 flow 에서는 나타나지 않고, 어떤 특별한 조건, 혹은 어떤
Exception 이 발생할 때만 나타날 수 있기 때문에, 이런 방법으로는 손발과 눈만 아프게
되곤 합니다.

따라서, 애초 부터, 이러한 상황을 고려하여, 만약, 개발자의 코드에서 실수로 DB연결을
제대로 Pool 에 반환하지 않았을 때, 그 개발자 프로그램 소스의 클래스 이름과 함께
Warnning 성 메세지를 남겨 놓으면, 되지 않겠느냐는 겁니다.

Java 에서는 java.lang.Object 의 finalize() 라는 기막한 메소드가 있습니다. 해당
Object instance의 reference 가 더이상 그 어떤 Thread 에도 남아 있지 않을 경우
JVM의 GC가 일어날 때 그 Object 의 finalize() 메소드를 꼭 한번 불러주니까요.

계속 반복되는 얘기인데, 아래 글에서 또한번 관련 의미를 찾을 수 있을 것입니다.
Re: DB Connection Pool: Orphan and Idle Timeout
http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=servlet&c=r_p&n=1005294960

아래는 이것을 응용하여, Hans Bergsten 의 DBConnectionManager 를 사용하는
ConnectionAdapter 클래스를 만들어 본 샘플입니다. Trace/Debuging 을 위한 몇가지
기능이 첨가되어 있으며, 다른 ConnectionPool을 위한 Connection Adapter 를 만들때도
약간만 수정/응용하여 사용하실 수 있을 것입니다.


/**
 * Author : Lee WonYoung, javaservice@hanmail.net
 * Date   : 2001.03.20, 2001.10.22
 */

import java.util.*;
import java.sql.*;
import java.io.*;
import java.text.SimpleDateFormat;

public class ConnectionResource
{
    private boolean DEBUG_MODE = true;
    private String DEFAULT_DATASOURCE = "idb";  // default db name
    private long GET_CONNECTION_TIMEOUT = 2000; // wait only 2 seconds if no
                                                // avaiable connection in the pool
    private long WARNNING_MAX_ELAPSED_TIME = 3000;

    private SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd/HHmmss");
    private db.DBConnectionManager manager = null;
    private Connection conn = null;
    private String datasource = DEFAULT_DATASOURCE;
    private String caller = "unknown";
    private long starttime = 0;

    private static int used_conn_count = 0;
    private static Object lock = new Object();

    // detault constructor
    public ConnectionResource()
    {
        manager = db.DBConnectionManager.getInstance();
    }

    // For debugging, get the caller object reference
    public ConnectionResource(Object caller_obj)
    {
        this();
        if ( caller_obj != null )
            caller = caller_obj.getClass().getName();
    }

    public Connection getConnection()  throws Exception
    {
        return getConnection(DEFAULT_DATASOURCE);
    }

    public Connection getConnection(String datasource)  throws Exception
    {
        if ( conn != null ) throw new Exception
            ("You must release the connection first to get connection again !!");

        this.datasource = datasource;
        // CONNECTION_TIMEOUT is very important factor for performance tuning
        conn = manager.getConnection(datasource, GET_CONNECTION_TIMEOUT);
        synchronized( lock ) { ++used_conn_count; }
        starttime = System.currentTimeMillis();
        return conn;
    }

    // you don't have to get "connection reference" as parameter,
    // because we already have the referece as the privae member variable.
    public synchronized void release() throws Exception { 
        if ( conn == null ) return;
        // The following is needed for some DB connection pool.
        boolean mode = true;
        try{
            mode = conn.getAutoCommit();
        }catch(Exception e){}
        if ( mode == false ) {
            try{conn.rollback();}catch(Exception e){}
            try{conn.setAutoCommit(true);}catch(Exception e){}
        }
        manager.freeConnection(datasource, conn);
        conn = null;
        int count = 0;
        synchronized( lock ) { count = --used_conn_count; }
        if ( DEBUG_MODE ) {
            long endtime = System.currentTimeMillis();
            if ( (endtime-starttime) > WARNNING_MAX_ELAPSED_TIME ) {
                System.err.println(df.format(new java.util.Date()) +
                    ":POOL:WARNNING:" + count +
                    ":(" + (endtime-starttime) + "):" +
                    "\t" + caller
                );
            }
        }
    }
    // finalize() method will be called when JVM's GC time.
    public void finalize(){
        // if "conn" is not null, this means developer did not release the "conn".
        if ( conn != null ) {
            System.err.println(df.format(new java.util.Date()) +
                ":POOL:ERROR connection was not released:" +
                used_conn_count + ":\t" + caller
            );
            release();
        }
    }
}

-------------------------------------------------------------------------------

위의 Connection Adaptor 클래스를 Servlet 이나 JSP에서 사용할 때는 다음과 같이
사용할 수 있습니다.


사용법: DB Transaction 처리가 필요치 않을 때...


ConnectionResource resource = null;
Connection conn = null;
Statement stmt = null;
try{
    // For debugging and tracing, I recommand to send caller's reference to
    // the adapter's constructor.
    resource = new ConnectionResource(this); // <--- !!!
    //resource = new ConnectionResource();

    conn = resource.getConnection();
    // or you can use another database name
    //conn = resource.getConnection("other_db");

    stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery("select ...");
    while(rs.next()){
        .....
    }
    rs.close();

}
//catch(Exception e){
//    // error handling if you want
//    ....
//}
finally{
    if ( stmt != null ) try{stmt.close();}catch(Exception e){}
    if ( conn != null ) resource.release(); // <--- !!!
}

-------------------------------------------------------------------------------
사용법: Transaction 처리가 필요할 때.

ConnectionResource resource = null;
Connection conn = null;
Statement stmt = null;
try{
    // For debugging and tracing, I recommand to send caller's reference to
    // the adapter's constructor.
    resource = new ConnectionResource(this);
    //resource = new ConnectionResource();

    conn = resource.getConnection();
    // or you can use another database name
    //conn = resource.getConnection("other_db");

    conn.setAutoCommit(false); // <--- !!!

    stmt = conn.createStatement();
    stmt.executeUpdate("update ...");
    stmt.executeUpdate("insert ...");
    stmt.executeUpdate("update ...");
    stmt.executeUpdate("insert ...");
    stmt.executeUpdate("update ...");
   
    int affected = stmt.executeUpdate("update....");
    // depends on your business logic,
    if ( affected == 0 )
        throw new Exception("NoAffectedException");
    else if ( affected > 1 )
        throw new Exception("TooManyAffectedException:" + affected);

    conn.commit(); //<<-- commit() must locate at the last position in the "try{}"
}
catch(Exception e){
    // if error, you MUST rollback
    if ( conn != null ) try{conn.rollback();}catch(Exception e){}

    // another error handling if you want
    ....
    throw e; // <--- throw this exception if you want
}
finally{
    if ( stmt != null ) try{stmt.close();}catch(Exception e){}
    if ( conn != null ) resource.release(); // <-- NOTE: autocommit mode will be
                                            //           initialized
}
-----------------------------------------------------------------

PS: 더 깊이있는 내용을 원하시면 다음 문서들을 참조 하세요...
    JDF 제4탄 - DB Connection Pool
    http://www.javaservice.net/~java/bbs/read.cgi?m=jdf&b=framework&c=r_p&n=945156790

    JDF 제5탄 - DB Connection Resource Framework
    http://www.javaservice.net/~java/bbs/read.cgi?m=jdf&b=framework&c=r_p&n=945335633

    JDF 제6탄 - Transactional Connection Resource Framework
    http://www.javaservice.net/~java/bbs/read.cgi?m=jdf&b=framework&c=r_p&n=945490586


PS: IBM WebSphere의 JDBC Connection Pool 에 관심이 있다면 다음 문서도 꼭 참조
 하세요...
    Websphere V3 Connection Pool 사용법
    http://www.javaservice.net/~java/bbs/read.cgi?m=appserver&b=was&c=r_p&n=970209527
    WebSphere V3 DB Connection Recovery 기능 고찰
    http://www.javaservice.net/~java/bbs/read.cgi?m=appserver&b=was&c=r_p&n=967473008


NOTE: 2004.04.07 추가
그러나, 2004년 중반을 넘어서는 이 시점에서는, ConnectionResource와 같은 Wapper의
중요성이 별로 높이 평가되지 않는데, 그 이유는 Tomcat 5.0을 비롯한 대부분의 WAS(Web
Application Server)가 자체의 Connection Poolinig기능을 제공하며, 그 사용법은 다음과
같이 JDBC 2.0에 준하여 모두 동일한 형태를 띠고 있기 때문입니다.
InitialContext ctx = new InitialContext();
DataSource ds = (DataSoruce)ctx.lookup("java:comp/env/jdbc/ds");
Connection conn = ds.getConnection();
...
conn.close();
결국 ConnectionResource의 release()류의 메소드는 Vendor별로, JDBC Connection Pool의
종류가 난무하던 3-4년 전에는 어플리케이션코드의 Vendor종속성을 탈피하기 위해 의미를
가졌으나, 지금은 굳이 필요가 없다고 보여 집니다.

단지, caller와 callee의 정보, 즉, 응답시간을 추적한다거나, close() 하지 않은
어플리케이션을 추적하는 의미에서의 가치만 남을 수 있습니다.  (그러나, 이것도,
IBM WebSphere v5의 경우, 개발자가 conn.close() 조차 하지 않을 지라도 자동을 해당
Thread의 request ending시점에 "자동반환"이 일어나게 됩니다.)

------------------------------------------------------- 
  본 문서는 자유롭게 배포/복사 할 수 있으나 반드시
  이 문서의 저자에 대한 언급을 삭제하시면 안됩니다
================================================
  자바서비스넷 이원영
  E-mail: javaservice@hanmail.net
  PCS:011-898-7904

좋은정보가 되셨다면 아래 한번 클릭해주세요^^


댓글