웹 어플리케이션 구동 시 JDBC 드라이버 로딩하기
앞서 살펴본 예제에서는 Class.forName([JDBC_DRIVER_CLASS]); 를 사용하여 로딩을 했었습니다.
JDBC 드라이버는 한번만 로딩하면 이후로 페이지마다 매번 로딩할 필요가 없기 때문에 계속 사용이 가능합니다.
웹 어플리케이션이 시작될 때 자동으로 JDBC 드라이버를 로딩하게 만들려면 서블릿 클래스를 사용하여 설정해주면 됩니다.
실습을 위해 다음 경로에 서블릿 클래스를 만들어줍니다.
▶ [실습 디렉터리]\WEB-INF\src\jdbc\loader\Loader.java
lib 디렉터리에 src디렉터리를 생성하고 패키지와 클래스를 만들면 Java Resources에 자동으로 옮겨집니다.
서블릿 클래스는 다음과 같이 작성해줍니다.
<Loader.java>
- package jdbc.loader;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.ServletConfig;
- import javax.servlet.ServletException;
- import java.util.StringTokenizer;
- public class Loader extends HttpServlet {
- public void init(ServletConfig config) throws ServletException {
- try {
- String drivers = config.getInitParameter("jdbcdriver");
- StringTokenizer st = new StringTokenizer(drivers, ",");
- while(st.hasMoreTokens()) {
- String jdbcDriver = st.nextToken();
- Class.forName(jdbcDriver);
- }
- } catch(Exception ex) {
- throw new ServletException(ex);
- }
- }
- }
그 다음 cygwin 을 실행시키고 해당 경로로 이동하여 컴파일해줍니다. (각자 지정하신 프로젝트의 경로를 찾아가면 됩니다)
컴파일을 하면 다음과 같은 경로에 .class 파일이 생성됩니다.
그 다음 web.xml 파일에 <servlet> 태그를 추가하여 자동으로 Loader 서블릿 클래스가 실행되도록 설정해줍니다.
<servlet>
<servlet-name>JDBCDriverLoader</servlet-name>
<servlet-class>jdbc.loader.Loader</servlet-class>
<init-param>
<param-name>jdbcdriver</param-name>
<param-value>com.mysql.jdbc.Driver</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
* web.xml 파일을 수정한 후엔 웹 컨테이너를 재가동하는데, 이러한 경우엔 엔진을 shutdown 한 다음 다시 재시작하는 것이 변경내용을 적용하는 가장 확실한 방법입니다.
트랜잭션 처리
한 개 이상의 쿼리가 모두 성공적으로 실행되어야 데이터가 정상적으로 처리되는 경우,
DBMS는 트랜잭션(transaction)을 이용하여 다수의 쿼리를 하나의 쿼리처럼 처리할 수 있도록 해줍니다.
트랜잭션은 시작과 종료를 가지고 있는데 트랜잭션 시작 후 실행되는 쿼리 결과는 곧바로 반영되지 않고 임시로 보관됩니다.
트랜잭션을 커밋하면 임시로 보관된 모든 쿼리결과가 실제 데이터에 반영되며, 커밋하기 전에 에러가 발생하면 임시로 보관된 모든 쿼리 결과를 취소합니다.
트랜잭션 구현방법에는 아래와 같이 크게 두가지가 있습니다.
▶ JDBC의 오토 커밋 모드를 false로 지정하는 방법
▶ JTA(Java Transaction API)를 이용하는 방법
JTA는 현재 설명하고자하는 부분의 범위를 넘어서므로 나중에 알아보고 우선 JDBC API에 대한 내용을 알아보겠습니다.
<insertForm.jsp>
- <%@ page language="java" contentType="text/html; charset=EUC-KR" pageEncoding="EUC-KR"%>
- <html>
- <head><title>TableRecordInsert</title></head>
- <body>
- <form action="<%=request.getContextPath() %>/TransactionTest/insertItem.jsp" method="post">
- <table border="1" cellpadding="0" cellspacing="0">
- <tr>
- <th>MEMBERID</th>
- <td><input type="text" name="memberID" size="10"/></td>
- <th>PASSWORD</th>
- <td><input type="password" name="password" size="10"/></td>
- </tr>
- <tr>
- <th>NAME</th>
- <td><input type="text" name="name" size="10"/></td>
- <th>EMAIL</th>
- <td><input type="text" name="email" size="20"/></td>
- </tr>
- <tr>
- <th>ERROR(input '1' to Error)</th>
- <td><input type="text" name="error" size="5"/></td>
- </tr>
- <tr>
- <td colspan="4" align="center">
- <input type="submit" value="INSERT"/>
- </td>
- </tr>
- </table>
- </form>
- </body>
- </html>
<insertItem.jsp>
- <%@ page language="java" contentType="text/html; charset=EUC-KR" pageEncoding="EUC-KR"%>
- <%@ page import="java.sql.DriverManager" %>
- <%@ page import="java.sql.Connection" %>
- <%@ page import="java.sql.PreparedStatement" %>
- <%@ page import="java.sql.SQLException" %>
- <%
- request.setCharacterEncoding("euc-kr");
- String memberID = request.getParameter("memberID");
- String password = request.getParameter("password");
- String name = request.getParameter("name");
- String email = request.getParameter("email");
- Class.forName("com.mysql.jdbc.Driver");
- Connection conn = null;
- PreparedStatement pstmt = null;
- Throwable occuredException = null;
- try {
- String jdbcDriver = "jdbc:mysql://localhost:3306/test?unicode=true&characterEncoding=euckr";
- String dbUser = "root";
- String dbPass = "1234";
- conn = DriverManager.getConnection(jdbcDriver, dbUser, dbPass);
- conn.setAutoCommit(false);
- pstmt = conn.prepareStatement("insert into member values(?, ?, ?, ?)");
- pstmt.setString(1, memberID);
- pstmt.setString(2, password);
- if(request.getParameter("error").equals("1")) {
- throw new Exception("의도적 예외발생!");
- } else {
- pstmt.setString(3, name);
- pstmt.setString(4, email);
- pstmt.executeUpdate();
- conn.commit();
- }
- } catch(Throwable e) {
- if(conn != null) { try { conn.rollback(); } catch(SQLException ex) {} }
- occuredException = e;
- } finally {
- if(pstmt != null) { try { pstmt.close(); } catch(SQLException ex) {} }
- if(conn != null) { try { conn.close(); } catch(SQLException ex) {} }
- }
- %>
- <html>
- <head><title>TransactionTestResult</title></head>
- <body>
- <% if(occuredException != null) { %>
- <%=occuredException.getMessage() %>
- <% } else { %>
- 데이터가 성공적으로 들어갔습니다.
- <% } %>
- </body>
- </html>
위의 파일 작성이 모두 완료되면 실행해줍니다.
예제 실행시 1을 입력하여 전달할 경우 예외가 발생하여 커밋되지 않도록 하였습니다.
커넥션 풀
데이터베이스와 연결된 커넥션을 미리 만들어서 풀(pool) 속에 저장해 두고 있다가 필요할 때에 가져다 쓰고 다시 반환하는 기법입니다.
커넥션 풀의 특징은 다음과 같습니다.
▶ 커넥션 생성시간과 연결시간이 소비되지 않는다.
▶ 계속 재사용 되므로 생성되는 커넥션 수가 많지 않다.
이러한 특징을 이유로 동시 접속자가 몰려도 쉽게 다운되지 않고, 전체적인 성능 및 처리량이 향상되기 때문에 많은 웹 어플리케이션에서 커넥션 풀을 기본적으로 사용합니다.
다양한 프레임워크에서 사용되는 DBCP(DataBase Connection Pool) API를 이용해서 커넥션 풀을 제공하는 방법을 알아보겠습니다.
아래 경로로 들어가서 필요한 .jar 파일을 다운로드 받습니다.
commons-dbcp-1.4.jar 파일을 /WEB-INF/lib 디렉터리에 위치시켜줍니다.
다음으로 Commons-Pool API의 Jar 파일을 받아줍니다.
commons-pool-1.6.jar 파일도 /WEB-INF/lib 디렉터리에 위치시켜줍니다.
그 다음으로 커넥션 풀 설정 파일을 작성해주는데 /WEB-INF/classes 디렉터리에 pool.jocl 파일을 생성하여 아래와 같이 작성해주면 됩니다.
<pool.jocl>
<object class="org.apache.commons.dbcp.PoolableConnectionFactory"
xmlns="http://apache.org/xml/xmlns/jakarta/commons/jocl">
<object class="org.apache.commons.dbcp.DriverManagerConnectionFactory">
<string value="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=euckr"/>
<string value="root"/>
<string value="1234"/>
</object>
<object class="org.apache.commons.pool.impl.GenericObjectPool">
<object class="org.apache.commons.pool.PoolableObjectFactory" null="true" />
</object>
<object class="org.apache.commons.pool.KeyedObjectPoolFactory" null="true"/>
<string null="true"/>
<boolean value="false"/>
<boolean value="true"/>
</object>
* 커넥션 풀 설정 파일은 XML 문서로 처리되는데, 여기서는 앰퍼샌드('&')를 & 로 써줘야 합니다.
다음으로 할 작업은 커넥션 풀과 관련된 JDBC 드라이버를 로딩해 주는 것입니다. 아래 두 가지 JDBC 드라이버를 로딩해주면 됩니다.
▶ DBMS에 연결할 때 사용될 JDBC 드라이버
▶ DBCP API의 JDBC 드라이버
앞서 실습해본 Loader.java 처럼 웹 어플리케이션이 시작될 때 자동으로 JDBC 드라이버를 로딩한 것처럼 비슷한 형태로 작성해 주면 됩니다.
/WEB-INF/src/jdbc/loader에 DBCPInit.java 파일을 작성해줍니다.
<DBCPInit.java>
package .jdbc.loader;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import java.util.StringTokenizer;
public class DBCPInit extends HttpServlet {
public void init(ServletConfig config) throws ServletException {
try {
String drivers = config.getInitParameter("jdbcdriver");
StringTokenizer st = new StringTokenizer(drivers, ",");
while (st.hasMoreTokens()) {
String jdbcDriver = st.nextToken();
Class.forName(jdbcDriver);
}
Class.forName("org.apache.commons.dbcp.PoolingDriver");
} catch(Exception ex) {
throw new ServletException(ex);
}
}
}
작성 후엔 컴파일하여 클래스파일을 생성해줍니다.
마지막으로 web.xml 파일에 DBCPInit 서블릿 클래스에 대한 설정 정보를 추가해주고, 서블릿 API 를 추가해주도록 합니다.
(서블릿 API는 apache-tomcat 내의 lib 디렉터리 안에 있는 servlet-api.jar 파일인데 이것을 /WEB-INF/lib 디렉터리에 옮겨주면 됩니다)
<web.xml 설정정보>
<servlet>
<servlet-name>DBCPInit</servlet-name>
<servlet-class>jdbc.loader.DBCPInit</servlet-class>
<init-param>
<param-name>jdbcdriver</param-name>
<param-value>com.mysql.jdbc.Driver</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
여기까지 작업을 마치면 이클립스의 Project Explorer는 다음과 같은 구조가 됩니다.
이제 커넥션 풀로부터 커넥션을 사용하기 위한 예제 페이지를 작성해줍니다.
<viewUsingPool.jsp>
- <%@ page language="java" contentType="text/html; charset=EUC-KR" pageEncoding="EUC-KR"%>
- <%@ page import="java.sql.DriverManager" %>
- <%@ page import="java.sql.Connection" %>
- <%@ page import="java.sql.Statement" %>
- <%@ page import="java.sql.ResultSet" %>
- <%@ page import="java.sql.SQLException" %>
- <html>
- <head><title>MemberList</title></head>
- <body>
- MEMBER 테이블의 내용
- <table border="1" cellpadding="0" cellspacing="0">
- <tr>
- <td>MEMBERID</td>
- <td>PASSWORD</td>
- <td>NAME</td>
- <td>EMAIL</td>
- </tr>
- <%
- Connection conn = null;
- Statement stmt = null;
- ResultSet rs = null;
- try {
- String jdbcDriver = "jdbc:apache:commons:dbcp:/pool";
- String query = "select * from member order by memberid";
- conn = DriverManager.getConnection(jdbcDriver);
- stmt = conn.createStatement();
- rs = stmt.executeQuery(query);
- while(rs.next()) {
- %>
- <tr>
- <td><%=rs.getString("MEMBERID") %></td>
- <td><%=rs.getString("PASSWORD") %></td>
- <td><%=rs.getString("NAME") %></td>
- <td><%=rs.getString("EMAIL") %></td>
- </tr>
- <%
- }
- } catch(SQLException ex) {
- out.println(ex.getMessage());
- } finally {
- if(rs!=null) { try{ rs.close(); } catch(SQLException ex) {} }
- if(stmt!=null) { try{ rs.close(); } catch(SQLException ex) {} }
- if(conn!=null) { try{ rs.close(); } catch(SQLException ex) {} }
- }
- %>
- </table>
- </body>
- </html>
실행해보면 다음과 같이 테이블의 목록이 출력됩니다.
* 커넥션 풀에서 사용한 커넥션의 close() 메서드를 호출하면, 커넥션이 닫히는 것이 아니라 커넥션 풀로 반환됩니다.
마지막으로 커넥션 풀의 속성을 알아보겠습니다.
앞서 작성한 커넥션 풀 설정파일 pool.jocl 에 지정해 주면 됩니다.
<object class="org.apache.commons.pool.impl.GenericObjectPool">
<object class="org.apache.commons.pool.PoolableObjectFactory" null="true" />
<int value="10"/> <!-- maxActive -->
<byte value="1"/> <!-- whenExhaustedAction -->
<long value="10000"/> <!-- maxWait -->
<int value="19"/> <!-- maxIdle -->
<int value="3"/> <!-- minIdle -->
<boolean value="true"/> <!-- testOnBorrow -->
<boolean value="true"/> <!-- testOnReturn -->
<long value="600000"/> <!-- timeBetweenEvictionRunsMillis -->
<int value="5"/> <!-- numTestsPerEvictionRun -->
<long value="3600000"/> <!-- minEvictableIdleTimeMillis -->
<boolean value="true"/> <!-- testWhileIdle -->
</object>
각 속성의 특징은 다음과 같습니다.
maxActive |
커넥션 풀이 제공할 최대 커넥션 개수. 동시 접속자 수에 따라 지정한다. |
whenExhaustedAction |
커넥션 풀에서 가져올 커넥션이 없을 경우 어떻게 동작할지를 지정. 0일 경우 : 에러 발생 1일 경우 : maxWait 속성에서 지정한 시간만큼 커넥션을 구할때 까지 대기 2일 경우 : 일시적으로 커넥션을 생성하여 사용 |
maxWait | whenExhaustedAction 속성의 값이 1일때 사용되는 대기시간. (1/1000초 단위) 0 보다 작을 경우 무한대기 |
maxIdle | 사용되지않고 풀에 저장될 최대 커넥션 개수. 음수일 경우 무제한 |
minIdle |
사용되지 않고 풀에 저장될 최소 커넥션 개수. 0 으로 지정할 경우 커넥션이 필요할때 다시 생성할 수 있으므로, 최소 5개로 지정하는 것이 좋다. |
testOnBorrow |
true 일 경우 커넥션을 가져올 때 유효 여부를 검사 |
testOnReturn |
true 일 경우 커넥션을 반환할 때 유효 여부를 검사 |
timeBetweenEvictionRunsMillis |
사용되지 않은 커넥션을 추출하는 쓰레드의 실행 주기 지정. (1/1000초 단위, 양수가 아닐경우 실행 안됨) 시간대를 고려하여 값을 알맞게 지정하여 사용되지 않는 커넥션을 풀에서 제거하는 것이 좋다. 보통 10~20분 단위로 검사하도록 지정. |
numTestsPerEvictionRun |
사용되지 않는 커넥션을 몇 개 검사할지 지정. |
minEvictableIdelTimeMillis |
사용되지 않는 커넥션 추출시 지정한 시간 이상 비활성화 상태인 커넥션만 추출. 양수가 아닌 경우 적용 안됨. (1/1000초 단위) |
testWhileIdle |
true 일 경우 비활성화 커넥션 추출시 커넥션 유효 여부를 검사. 유효하지 않은 커넥션은 제거. |