본문 바로가기

프로그래밍/JAVA

Tomcat 5 JNDI DataSource를 통한 DB 커넥션 풀 사용

반응형

Tomcat 5 JNDI DataSource를 통한 DB 커넥션 풀 사용 


이미 눈치 채셨겠지만, 요즘 내가 RDBMS 배우기에 열을 올리고 있다. 

지금까지는 JSP/Servlet에서 직접 커넥션을 맺거나, 웹 컨텍스트내에 커넥션 풀 라이브러리를 두고 호출에서 사용했는데, 좀 바꿔야겠다. 

JNDI를 통한 커넥션 풀 사용은 J2EE 표준이고, 현존하는 거의 모든 웹 컨테이너가 지원한다고 한다. 


JNDI를 서버에 설정하는 방법은 각 WAS 별로 다르지만, 사용하는 것은 모두 동일하므로 호환성에 문제도 없다. 


이 글은 Jakarta의 DBCP 커넥션 풀과 Tomcat JNDI 설정을 통해 데이터베이스 커넥션 풀을 사용하는 방법이다. 


JNDI와 커넥션 풀에 관한 자세한 설명이 JavaServer Pages 3rd ed.에 실려있다. 이 책 너무 좋다. 꼭 읽어보라고 강력하게 권하고 싶다. 


* 참조 URL : http://jakarta.apache.org/tomcat/tomcat-5.0-doc/jndi-datasource-examples-howto.html 

이 글은 사실상 저 참조 URL의 번역에 가깝다. 

* Tomcat Admin을 이용한 DataSource 설정 : http://www.okjsp.pe.kr/lecture/viewlet/okjsp2005/05_webdev_datasource.html 


기본적으로 필요한 라이브러리 


* commons-dbcp.jar 

* commons-collections.jar 

* commons-pool.jar 


예제 JDBC 드라이버 


* Oracle 9i classes12.jar 


JNDI Naming Resource 설정 


1. 위 라이브러리들을 $CATALINA_HOME/common/lib 에 복사한다. 그 이외 디렉토리에 두면 안된다. ZIP 파일은 JAR로 확장자를 바꿔야 한다. 톰캣은 JAR파일만 클래스패스에 추가한다. 


2. Connection 풀을 이용할 경우에는 ResultSet과 Connection 객체를 필히 직접 닫아 줘야만 한다. 


3. $CATALINA_HOME/conf/server.xml 혹은 각 웹 컨텍스트별 XML 파일의 <Context>의 자식 요소로 다음을 추가한다. 


<Resource name="jdbc/forumDb" auth="Container" type="javax.sql.DataSource"/> 

<!-- Resource의 name 속성을 이용해서 각 어플리케이션에서 

javax.sql.DataSource 객체를 얻어가게 된다. --> 



<!-- 자세한 파라미터 설정은 

http://jakarta.apache.org/tomcat/tomcat-5.0-doc/jndi-datasource-examples-howto.html 참조 --> 

<ResourceParams name="jdbc/forumDb"> 

<parameter> 

<name>factory</name> 

<value>org.apache.commons.dbcp.BasicDataSourceFactory</value> 

</parameter> 


<parameter> 

<name>maxActive</name> 

<value>100</value> 

</parameter> 


<parameter> 

<name>maxIdle</name> 

<value>30</value> 

</parameter> 


<parameter> 

<name>maxWait</name> 

<value>10000</value> 

</parameter> 


<!-- DB 사용자명과 비밀번호 설정 --> 

<parameter> 

<name>username</name> 

<value>dbuser</value> 

</parameter> 

<parameter> 

<name>password</name> 

<value>dbpasswd</value> 

</parameter> 


<!-- JDBC 드라이버 클래스 --> 

<parameter> 

<name>driverClassName</name> 

<value>oracle.jdbc.driver.OracleDriver</value> 

</parameter> 


<!-- JDBC 접속 URL --> 

<parameter> 

<name>url</name> 

<value>jdbc:oracle:thin:@dbhost:1521:ORA</value> 

</parameter> 

</ResourceParams> 



4. 웹 어플리케이션의 web.xml파일에 다음을 추가하여 JNDI 리소스를 사용할 수 있도록 한다. 


<resource-ref> 

<description>Forum DB Connection</description> 

<!-- 다음이 바로 리소스의 이름 --> 

<res-ref-name>jdbc/forumDb</res-ref-name> 

<res-type>javax.sql.DataSource</res-type> 

<res-auth>Container</res-auth> 

</resource-ref> 



JSP/Servlet 에서 사용하기 

이제 다음과 같이 JNDI를 이용해 DataSource 객체를 얻고, 그 객체에서 커넥션을 얻어오면 된다. 


다음은 서블릿을 작성하고, 그 서블릿에서 DB커넥션을 얻고, 쿼리를 날린 뒤, 그 결과를 JSP 파일에 포워딩하여 JSTL을 이용해 출력하는 것이다. 


1. 예제 테이블과 데이터 SQL - 오라클 계정으로 다음과 같은 데이터를 생성했다고 가정하면 


create table test ( 

num NUMBER NOT NULL, 

name VARCHAR2(16) NOT NULL 

); 


truncate table test; 


insert into test values(1,'영희'); 

insert into test values(2, '철수'); 

insert into test values(3, '미숙'); 

commit; 



2. test.JndiDataSourceTestServlet 소스 


package test; 


import java.io.IOException; 

import java.sql.Connection; 

import java.sql.ResultSet; 

import java.sql.SQLException; 

import java.sql.Statement; 

import java.util.ArrayList; 

import java.util.HashMap; 

import java.util.List; 

import java.util.Map; 


import javax.naming.Context; 

import javax.naming.InitialContext; 

import javax.naming.NamingException; 

import javax.servlet.RequestDispatcher; 

import javax.servlet.ServletException; 

import javax.servlet.http.HttpServlet; 

import javax.servlet.http.HttpServletRequest; 

import javax.servlet.http.HttpServletResponse; 

import javax.sql.DataSource; 


public class JndiDataSourceTestServlet extends HttpServlet { 


protected void doGet(HttpServletRequest request, 

HttpServletResponse response) throws ServletException, IOException { 


Connection conn = null; 

ResultSet rs = null; 

Statement stmt = null; 


try { 

// 커넥션을 얻기 위한 전초작업. 이 부분을 메소드화 하면 되겠다. ------------ 

Context initContext = new InitialContext(); 

Context envContext = (Context)initContext.lookup("java:/comp/env"); 

DataSource ds = (DataSource)envContext.lookup("jdbc/forumDb"); 


// 커넥션 얻기 

conn = ds.getConnection(); 

//------------------------------------------------------------------ 


String sql = "SELECT * from test"; 

stmt = conn.createStatement(); 


rs = stmt.executeQuery(sql); 


List results = new ArrayList(); 


while (rs.next()) { 

Map row = new HashMap(); 


row.put("num", rs.getString("num")); 

row.put("name", rs.getString("name")); 


results.add(row); 


request.setAttribute("results", results); 


RequestDispatcher rd = request.getRequestDispatcher("/dbtest.jsp"); 

rd.forward(request, response); 


} catch (NamingException e) { 

throw new ServletException("JNDI 부분 오류", e); 

} catch (SQLException e) { 

throw new ServletException("SQL 부분 오류", e); 

} finally { 

// 리소스를 필히 반환할 것! 

if (rs != null) { try { rs.close(); } catch (Exception ignored) {} } 

if (stmt != null) { try { stmt.close(); } catch (Exception ignored) {} } 

if (conn != null) { try { conn.close(); } catch (Exception ignored) {} } 


3. web.xml에 서블릿 등록 


<servlet> 

<servlet-name>dbtest.svl</servlet-name> 

<servlet-class>test.JndiDataSourceTestServlet</servlet-class> 

</servlet> 

<servlet-mapping> 

<servlet-name>dbtest.svl</servlet-name> 

<url-pattern>/dbtest.svl</url-pattern> 

</servlet-mapping> 


4. /dbtest.jsp 소스 


<%@ page contentType="text/html" pageEncoding="EUC-KR" %> 

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 

<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql" %> 


<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en"> 

<html> 

<head> 

<title>JNDI DataSource Test</title> 

</head> 

           <body            bgcolor="#FFFFFF"> 

<h2>Results</h2> 


<c:forEach var="row" items="${results}"> 

NUM : ${row.num}<br /> 

Name : ${row.name}<br /> 

<hr /> 

</c:forEach> 

</body> 

</html> 



5. 이제 웹 브라우저에서 "/dbtest.svl" 을 호출해보면 결과를 볼 수 있다. 



전역적인 JNDI 리소스 이용 

<Resource>와 <ResourceParams> 요소를 server.xml의 <GlobalNamingResources> 의 자식노드로 옮기면 특정 웹 어플리케이션이 아니라, 이 톰캣에 설치된 전체 웹 어플리케이션에서 사용 할 수 있게 된다. 하지만 각 웹 어플리케이션 "<Context>"에 다음과 같은 설정을 해야 한다. 


<ResourceLink 

name="jdbc/forumDb" 

global="jdbc/forumDb" 

type="javax.sql.DataSource" 

/> 



아니면 server.xml에서 <Host> 요소의 자식으로 다음을 추가하면 각 컨텍스트별 설정 필요없이 전체 웹 어플리케이션 컨텍스트에서 GlobalNamingResources로 지정된 JNDI 리소스를 사용할 수 있다. 


<DefaultContext> 

<ResourceLink 

name="jdbc/forumDb" 

global="jdbc/forumDb" 

type="javax.sql.DataSource" 

/> 

</DefaultContext> 



문제가 생겼어요! 


1. DB 커넥션이 간혹 끊어져요. 

Tomcat이 작동중인 JVM이 가비지 컬렉션을 할 때, 그 시간이 JNDI Resource에 파라미터로 설정한 maxWait보다 길게 갈 경우 DB 커넥션이 끊어질 수 있다. 

CATALINA_OPTS=-verbose:gc 옵션을 주면 $CATALINA_HOME/logs/catalina.out에 가비지 컬렉션 상황이 기록된다. 거기에 GC 작업에 걸린 시간도 나오니 그것을 참조로 maxWait 파라미터를 늘려주면 된다. 보통 10~15초로 주면 된다. 

GC 시간은 거의 99% 이상 1초 이내에 끝나야 하나보다.. 


2. 무작위로 커넥션이 close 되요. 

그건.. Connection 객체를 두 번 이상 close 했기 때문이다. 

DB Connection Pool은 close()를 호출할 때 정말로 닫는 것이 아니라, 단지 재사용할 수 있다고 표시만 할 뿐이다. 

커넥션을 사용하다가 close()하면 다른 쓰레드이서 이 커넥션을 쓸 수 있는데, 이걸 현재 쓰레드에서 계속 잡고 있다가 또 다시 close()를 해버리면, 다른 쓰레드에서 사용중에 close()됐다고 나와 버리게 되는 거다. 


conn.close(); 

conn = null; 


위와 같이 커넥션을 close()한 뒤에 바로 null 로 설정하여 절대로 다시 사용할 수 없게 만들면 이런 실수는 생기지 않을 것이다 

반응형