JSP/서블릿 흝어 보기

DataSource

Connection 관리

기존 JDBC 프로그램 구현으로 DBMS와 연동 작업을 할때는 웹 클라이언트로부터 요청이 있을 때마다 DB서버에 연결하기 위해 Connection 객체를 얻어내야 했습니다. 이러한 기존의 JDBC 프로그래밍 작업에는 다음과 같은 문제가 있습니다.

  • DB프로그램에서 트랜잭션 처리와 Connection 관리는 시스템의 성능과 안전성에 큰 영향을 미친다.
  • Connection 과정은 일정 시간이 필요한 부담과 작업이다.
  • 불필요한 연결에 의한 서버 자원의 낭비를 발생한다.

Connection Pool

커넥션 풀 개념은 Connection 객체를 프로그램이 실행될 때마다 생성하는 것이 아니라, 웹 애플리케이션이 서비스되기 전에 웹서버에서 미리 생성하여 준비한 다음, 필요할 때 준비된 Connection을 가져다 사용함으로써 JDBC 프로그래밍의 문제점들을 개선한 기술입니다.

DataSource

커넥션풀에는 여러개의 Connection 객체가 생성되어 운용되는데, 이를 직접 웹 애플리케이션에서 다루기 힘들기 때문에 DataSource라는 개념을 도입하여 사용합니다.

DataSource에 대해 정의하자면 아래와 같다.

  • 커넥션 풀의 Connection을 관리하기 위한 객체이다.
  • JNDI Server를 통해서 이용된다.
  • DataSource 객체를 통해서 필요한 Connection을 획득, 반납 등의 작업을 한다.

DataSource를 이용하려면 다음의 절차를 따릅니다.

  1. JNDI Server에서 lookup( ) 메소드를 통해 DataSource 객체를 획득한다.
  2. DataSource 객체의 getConnection( ) 메소드를 통해서 Connection Pool에서 Free 상태의 Connection 객체를 획득한다.
  3. Connection 객체를 통한 DBMS 작업을 수행한다.
  4. 모든 작업이 끝나면 DataSource 객체를 통해서 Connection Pool에 Connection을 반납한다.

JNDI

JNDI(Java Naming and Directory Interface)는 디렉터리 서비스에서 제공하는 데이터 및 객체를 발견하고 참고하기 위한 자바 API다.

Naming & Directory 서비스

Naming & Directory 서비스는 실제 어떤 자원을 가지고 서비스 한다는 의미가 아니라, 어떤 서버나 애플리케이션에서 분산환경에 서비스하고자 하는 자원을 이 Naming & Directory 서버에 이름값과 실제 자원을 연결하여 등록하면, 해당 자원을 이용하고자 하는 다른 애플리케이션에서 Naming & Directory 서버에 접근하여 이름값만을 가지고 자원을 연결하여 이용할 수 있게 하는 개념입니다.

네이밍 서비스의 대표적인 예로 DNS서버가 이런 기능을 한다. 브라우저창에 URL을 입력하면 브라우저는 DNS서버를 통해 도메인에 해당하는 IP주소를 얻습니다. 그리고 IP 주소를 통해 실제 인터넷 서버에 접속합니다. 결국 DNS 서버는 실제 인터넷 서비스를 수행해주는 곳이 아니며, 단지 도메인과 IP 주소만을 연결해주는 기능을 하는 것입니다. 

Naming & Directory 서버 또한 분산환경에서 자원을 연결해주는 기능을 합니다.

구현하기

DataSource는 서버에서 관리하는 리소스인 커넥션 풀을 사용할 수 있게 하는 객체로서, 서버가 시작할 때 커넥션 풀이 서버에 준비되어 있어야 합니다. 그래야 DB 프로그래밍을 할 때 커넥션 풀에서 Connection 객체를 얻어낼 수 있습니다. 

(1) server.xml 설정

Connection Pool은 서버에서 관리하는 자원입니다. 그래서 서버 환경 설정 파일인 Server.xml에 Connection 풀에 관한 설정을 합니다. 설정한 내용에 따라 서버가 시작하면서 리소스 준비 작업이 이루어집니다.

WAS_HOME/conf/server.xml 파일을 편집합니다. 이클립스에서는 다음과 같습니다.

<GlobalNamingResources>
    .... 생략 ....
    <Resource driverClassName="oracle.jdbc.driver.OracleDriver"
        	  url="jdbc:oracle:thin:@127.0.0.1:1521:xe"
    		  username="scott"
    		  password="tiger"
    		  name="jdbc/myoracle"
    		  type="javax.sql.DataSource"
    		  maxActive="4"
    		  maxIdle="2"
    		  maxWait="5000" />    
</GlobalNamingResources>

<Resource> 태그의 속성에 대한 설명은 다음과 같습니다.

  • driverClassName : DB작업을 위해 로딩할 JDBC 드라이버 파일에 드라이버 인터페이를 상속하는 파일명을 전체 이름으로 지정합니다. Class.forName() 메소드의 인자값입니다.
  • url : 접속할 DB 서버의 URL을 지정합니다.
  • username : DB 서버에 로그인할 계정을 지정합니다.
  • password : DB 서버에 로그인할 게정의 비밀번호를 지정합니다.
  • name : 현재 리소스를 등록할 이름을 지정합니다.
  • type : 리소스의 타입을 지정합니다. Connection Pool을 사용할 수있도록 해주는 객체의 javax.sqlDataSource입니다.
  • maxActive : 생성할 Conneciton 수를 지정합니다.
  • maxIdle : 일반적으로 활용할 Connection 수를 지정합니다.
  • maxWait : Connection의 사용 요청이 있을 때 대기 시간을 지정합니다. 5000은 5초를 의미하며, 5초가 지난 후에도 Connection을 얻지 못하면 Exception이 발생합니다.

※DBCP2를 사용시에는 이클립스 콘솔에 다음과 같은 메시지를 볼 수 있습니다.

 DBCP2에서는 maxActive 속성이 사용되지 않으며 maxActive로 설정한 값은 무시되며 대신 maxTotal을 사용하라고 경고 메시지를 띄운다.  maxWait도 마찬가지다. maxWaitMillis를 사용하라고 안내해준다.

꼭 한번 체크하자!!!

WAS_HOME/conf/context.xml 파일을 편집합니다. 이클립스에서는 다음과 같습니다.

<Context>
    .... 생략 ....
    <ResourceLink 
    global="jdbc/myoracle" 
	name="jdbc/myoracle" 
	type="javax.sql.DataSource" 	/>
</Context>

서버에 등록된 리소스를 웹 애플리케이션에서 찾아서 사용할 수 있도록 글로벌한 이름을 지정합니다. 서버에서의 설정은 server.xml에 Connection을 만들기 위해 정보를 설정한 후 context.xml 파일에 Connection Pool의 이름을 등록함으로써 완료되었습니다.

(2) web.xml 설정

서버에서 관리하는 리소스를 웹 애플리케이션에서 사용하기 위하여 /WEB-INF/web.xml 파일에 사용할 리소스에 대한 정보를 다음과 같이 지정합니다.

  .... 생략....
  <resource-ref>
      <description>Oracle DataSource example</description>
  	<res-ref-name>jdbc/myoracle</res-ref-name>
  	<res-type>javax.sql.DataSource</res-type>
  	<res-auth>Container</res-auth>
  </resource-ref>
  .... 생략....
</web-app>

<resource-ref> 태그의 하위 태그에 대한 자세한 설명은 다음과 같습니다.

  • <description> : 리소스에 대한 설명을 지정합니다.
  • <res-ref-name> : 사용하고자 하는 리소스의 이름을 지정합니다.
  • <res-type> : 사용하고자 하는 리소스의 타입을 지정합니다.
  • <res-auth> : 리소스에 대한 권한이 누구인지 지정합니다.

(3) DataSource를 사용하여 DB작업을 해보자.

dataSource.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
<%@ page import="javax.sql.*" %>
<%@ page import="javax.naming.*" %>
<%
//1. JNDI 서버 객체 새성
InitialContext ic= new InitialContext();

//2. lookup()
DataSource ds = (DataSource) ic.lookup("java:comp/env/jdbc/myoracle");

//3. getConnection()
Connection conn = ds.getConnection();

Statement stmt=conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from test");

while(rs.next()) {
    out.print(rs.getString("id") + ":" + rs.getString(2) + "<br>");
}

//반납
rs.close();
stmt.close();
conn.close();
%>

소스에 대한 자세한 설명

<%@ page import="java.sql.*" %>
<%@ page import="javax.sql.*" %>
<%@ page import="javax.naming.*" %>

java.sql 패키지는 일반 데이터베이스 작업을, javax.sql 패키지는 DataSource 객체를 사용하기 위해, javax.naming 패키지는 JNDI 작업을 하기 위해 import 합니다.

InitialContext ic= new InitialContext();

커넥션 풀에 접근하려면 JNDI 서비스를 사용해야 합니다. JNDI는 서버에서 관리하고 있는 리소스에 대한 정보를 알고 있고 특정 리소스를 찾아서 사용할 수있도록 객체를 반환해주는 역활을 합니다. JNDI 서버역활을 하는 객체를 생성합니다. 리소스가 로컬에 있을 때는 단순히 InitialContext 객체만 생성하면 됩니다.

DataSource ds = (DataSource) ic.lookup("java:comp/env/jdbc/myoracle");

ic.lookup( )은 리소스를 찾은 후 리소스를 사용할 수 있도록 객체를 반환해주는 메소드 입니다. lookup( ) 메소드의 인자값으로는 찾으려는 리소스의 등록된 이름을 지정합니다. 우리가 찾으려는 리소스의 이름은 "jdbc/myoracle" 이고 WAS인 톰캣에서 리소스를 관리하는 가상의 디렉터리는 "java:comp/env"입니다. 그래서 lookup( ) 메소드의 최종인자 값은 "java:comp/env/jdbc/myoracle"이 됩니다.

lookup( ) 메소드가 반환하는 객체의 타입은 Object 이기 때문에 원래 리소스 타입으로 변환해줍니다. 앞에서 server.xml 파일에 커넥션 풀을 설정할 때 리소스의 타입을 DataSource로 등록했습니다. 즉, 원래 DataSource로 타입을 변환합니다.

Connection conn = ds.getConnection();

ds 변수는 DataSource 입니다. DataSource 객체의 getConnection( )는 커넥션 풀에 준비된 Connection 객체를 빌려오는 메소드 입니다. 빌려오는 Connection을 conn에 담습니다.

Statement stmt=conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from test");

while(rs.next()) {
    out.print(rs.getString("id") + ":" + rs.getString(2) + "<br>");
}

rs.close();
stmt.close();
conn.close();//반납
%>

이후에는 기존의 JDBC프로그래밍와 같습니다. 하지만 리소스를 반납할 때 약간의 차이가 있습니다. Connection 객체을 반납할 때 리소스를 제거하는 것이 아니라 빌려온 Connection 객체를 다음 사용자가 사용할 수있도록 커넥션 풀에 그대로 반납합니다. 리소스를 반납하지 않고 코드를 종료해버리면 Connection 객체가 정상적으로 반납되지 않아 커넥션 풀에 구멍이 납니다.

이에 대한 해결책으로 이렇게 버려진 커넥션을 찾아 복구하고 열고서는 닫지 않은 코드를 찾아 추적 결과를 만들어낼 수 있습니다. 이를 위해서는 server.xml에 커넥션 풀을 설정할 때 관련 설정을 추가할 수 있습니다.

(참고 : http://www.ssiso.net/cafe/club/club1/board1/content.php?idx=30269)

  • removeAbandoned : 사용할 수 있는 커넥션이 부족해지면 DBCP는 버려진 커넥션을 찾아 복구하고 재생합니다. 기본값은 false 입니다.
  • removeAbandonedTimeout : 커넥션이 버려졌다고 간주되기 전에 사용되지 않은 시간(초)를 설정합니다.
  • logAbandoned : 만일 커넥션 자원을 낭비한 코드 위치의 로그를 남깁니다.

※DBCP2를 사용시에는 이클립스 콘솔에 다음과 같은 메시지를 볼 수 있습니다.

위에서 maxActive와 maxWait에 대한 속성처럼 removeAbandoned 대신에 removeAbandonedOnBorrow o또는 removeAbandonedOnMaintenance을 사용하라고 경고 메시지로 안내해준다.

실제로 커넥션 복구와 반납을 하지 않아 낭비하는 코드의 추적이 이루어지는지 확인하기 위해서 저의 경우 DBCP2 를 사용하므로 removeAbandonedOnBorrow 를 설정하고 removeAbandonedTimeout와 logAbandoned를 설정한후 datasoure.jsp에서 Connection 객체를 반납하는 코드를 주석 처리하였습니다.

rs.close();
stmt.close();
//conn.close();//반납

그런 다음 브라우저로 datasource.jsp를 실행하여 F5를 반복적으로 4회 이상 요청하면 커넥션을 반납하는 부분을 주석 처리했으므로 서버에서 보유하고 있는 4개의 커넥션을 모두 사용하게 되어 다음 브라우저의 요청이 이루어졌을 때 이클립스의 콘솔에는 다음과 같이 커넥션이 반납되지 않았다는 메시지와 함께 반납이 이루어지지 않은 코드의 위치를 Stack Trace에서 확인할 수있습니다. 

"at org.apache.jsp.dataSource_jsp._jspService(dataSource_jsp.java:127)" 

스택 트레이스에 따라 자바소스로 변환된 JSP 파일의 127라인을 살펴보자. 그러면 반납이 이루어지 않은 Connection 객체를 확인할 수 있습니다.

감사합니다.

커넥션풀와 DataSource의 정리는 여기까지 입니다.

책의 내용 외에 검색 중 알게된 커넥션 복구와 추적에 대해서도 다루어보았습니다.

댓글

댓글 본문
작성자
비밀번호
  1. DongHyun Kim
    댓글 감사합니다.
    도움이되었으면좋겠습니다.
    대화보기
    • 푸르
      정리 감사합니다.~~
    버전 관리
    DongHyun Kim
    현재 버전
    선택 버전
    graphittie 자세히 보기