http://www.daesangit.com/ditboard/board/read.cgi?board=oralib&y_number=48&nnew=2


   ▒   자료실   ▒    

 이름 관리자 
 등록일 2002/9/21(토) 20:35 
 
 서블렛 + JDBC 연동시 코딩 고려사항 ③   
하나의 Connection을 init()에 미리 연결해 두고 사용하는 경우

public class TestServlet extends HttpServlet {
 private final static String drv = "oracle.jdbc.driver.OracleDriver";
 private final static String url = "jdbc:orache:thin@210.220.251.96:1521:ORA8i";
 private final static String user = "scott";
 private final static String password = "tiger";

 private ServletContext context;
 private Connection conn = null; <--- !!!

 public void init(ServletConfig config) throws ServletException {
   super.init(config);
   context = config.getServletContext();
   try {
     Class.forName(drv);
     conn = DriverManager.getConnection(url,user,password);
   }
   catch (ClassNotFoundException e) {
     throw new ServletException("Unable to load JDBC driver:"+ e.toString());
   }
   catch (SQLException e) {
     throw new ServletException("Unable to connect to database:"+ e.toString());
   }
 }
 public void doGet(HttpServletRequest req, HttpServletResponse res) 
   throws ServletException, IOException, SQLException
 {
   Statement stmt = null;
   ResultSet rs = null;
   String id = req.getParameter("id");
   stmt = conn.createStatement();
   rs = stmt.executeQuery("select ..... where id = '" + id + "'");
   while(rs.next()) {
     ...... 
   } 
   rs.close(); 
   stmt.close();
   .....
 }
 public void destroy() {
   if ( conn != null ) try {conn.close();}catch(Exception e){}
 } 
}

위에서 잘못된 것은 무엇일까? 서블렛당 하나씩 java.sql.Connection 을 init()에서 미리 맺어 두고 사용하는 구조이다. 

얼핏 생각하면 아무런 문제가 없는 것 같다. doGet() 내에서 별도의 Statement와 ResultSet 을 사용하고 있으니, 각 Thread는 자신만의 Reference를 가지고 사용하면 될 것처럼 보인다. 

이 구조엔 크게 세가지 문제가 있다. 하나는 DB 연결 자원의 낭비를 가져오며, 두 번째는 수많은 동시 사용자에 대한 처리 한계를 가져오고, 마지막으로는 insert, update, delete와 같이 하나 이상의 SQL문장을 수행하면서 단일의 Transaction 처리를 보장받을 수 없다는 것이다. 

1) 대량의 동시 사용자 처리 불가
위의 구조는 서블렛당 하나씩 java.sql.Connection을 점유하고 있다. 실 프로젝트에서 보통 서블렛이 몇개나 될요? 최소 100개에서 400개가 넘어 갈 때도 있다. 그렇다면 java.sql.Connection에 할당돼야 할 DB연결수도 서블렛 갯수 많큼 필요하게 된다. 

DB 연결 자원은 DB에서 설정하기 나름이지만 통상 maximum을 셋팅한다. 하지만 요청이 없을 경우에도 400여개의 DB가 연결돼 있어야 한다는 것은 자원의 낭비다. 

2) 대량의 동시 사용자 처리 불가
또한 같은 서블렛에 대해 동시에 100 이상의 요청이 들어온다고 가정해보자. 이 경우 동이한 java.sql.Connection에 대해 각각의 요청이 conn.createStatement()를 호출하게 된다. 

문제는 하나의 Connection에 대해 동시에 Open할 수 있는 Statement 수(이 역시 DB 에서 셋팅하기 나름이지만)의 maximum에 제한이 있다. 오라클의 경우 Default는 50이다. 만약 이 수치 이상을 동시에 Open하려고 하면 "maximum open cursor exceed !" 라는 SQLExceptoin이 발생하게 된다. 

예를 들어 다음과 같은 프로그램을 실행시켜 보자. 

public class DbTest {
  public static void main(String[] args) throws Exception {
    Class.forName("jdbc driver...");
    Connection conn = DriverManager.getConnection("url...","id","password");
    int i=0;
    while(true) {
      Statement stmt = conn.createStatement();
      System.out.println( (++i) + "- stmt created");
    }
  }
}

과연 몇개까지 conn.createStement()가 수행될 것인가? 이는 DB에서 설정하기 나름이다. 중요한 것은 한계가 있다는 것이다. 또한 conn.createStatement()을 통해 만들어진 stmt는 java.sql.Connection의 자원이기 때문에 위처럼 stmt의 reference가 없어져도 GC(Garbage Collection)가 되진 않는다. 

3) Transaction 중복현상 발생

예를 들어 다음과 같은 서비스가 있다고 가정해보자. 

public void doGet(HttpServletRequest req, HttpServletResponse res) 
  throws ServletException, IOException, SQLException
{
  Statement stmt = null;
  String id = req.getParameter("id");
  try {
    conn.setAutoCommit(false);
    stmt = conn.createStatement();
    stmt.executeUpdate("update into XXXX..... where id = " + id + "'");
    stmt.executeUpdate("delete from XXXX..... where id = " + id + "'");
    stmt.executeUpdate(".... where id = " + id + "'");
    stmt.executeUpdate(".... where id = " + id + "'");
    conn.commit();
  }
  catch(Exception e){
    try{conn.rollback();}catch(Exception e){}
  }
  finally {
    if ( stmt != null ) try{stmt.close();}catch(Exception e){}
    conn.setAutoCommit(true);
  }
  .....
}

아무런 문제가 없을 듯도 하다. 하지만 위의 서비스를 동시에 요청하면 같은 java.sql.Connection을 갖고 작업을 하고 있으니 Transaction이 중첩된다. 왜냐하면 conn.commit(), conn.rollback()과 같이 conn이라는 Connection에 대해 Transaction이 관리되기 때문. 

요청 A가 총 4개의 SQL문장 중 3개를 정상적으로 수행하고 마지막 4번째의 SQL문장을 수행하려 한다. 이때 요청 B가 뒤따라 들어와서 2개의 SQL 문장을 열심히 수행했다. 근데 요청 A에 의한 마지막 SQL 문장 수행중에 SQLException이 발생했다. 그렇담 요청 A는 catch(Exception e) 절로 분기가 일어나고 conn.rollback()을 수행해 이미 수행한 3개의 SQL 수행들을 모두 rollback시킨다. 

문제는 요청 B 에 의해 수행된 2개의 SQL문장까지 rollback()돼 버린다. 왜냐하면 동일한 conn 객체이기 때문이다. 결국 요청 B는 영문도 모르고 마지막 2개의 SQL문장만 수행하게 된다. 

정리하면, Connection, Statement, ResultSet 는 doGet() , doPost() 내에서 선언되고 사용돼야 한다. @

public void doGet(HttpServletRequest req, HttpServletResponse res) 
  throws ServletException, IOException, SQLException 
{
  Connection conn = null; <----- 이곳으로 와야죠..
  Statement stmt = null; <-------
  ResultSet rs = null; <---------
  .....
}
