데이터베이스 프로그래밍 기초(7) : 커넥션 풀

de_sj_awa·2021년 5월 22일
0

지금까지 살펴본 예제들은 모두 데이터베이스 작업이 필요할 때 커넥션을 생성해서 사용했다. 이 방식을 사용하면 JSP 페이지를 실행할 때마다 커넥션을 생성하고 닫는 데 시간이 소모되기 때문에 동시 접속자가 많은 웹 사이트에서는 전체 성능이 낮아진다. 성능 문제를 해결하기 위해서 사용하는 일반적인 방식은 커넥션 풀 기법을 사용하는 것이다. 여기서는 커넥션 풀이 무엇인지 간단하게 설명하고 커넥션 풀을 구현하는 방법을 살펴보도록 하겠다.

1. 커넥션 풀이란?

먼저 커넥션 풀이 무엇인지 살펴보자. 커넥션 풀 기법이란 데이터베이스와 연결된 커넥션을 미리 만들어서 풀(pool) 속에 저장해 두고 있다가 필요할 때 커넥션을 풀에서 가져다쓰고 다시 풀에 반환하는 기법을 의미한다.

커넥션 풀 기법은 위의 그림과 같이 풀 속에 데이터베이스와 연결한 커넥션을 미리 생성해놓는다. 데이터베이스 커넥션이 필요하면, 커넥션을 새로 생성하는 것이 아니라 풀 속에 미리 생성된 커넥션을 가져다가 사용하고, 사용이 끝나면 커넥션을 풀에 반환한다. 풀에 반환된 커넥션은 다음에 다시 사용된다.

커넥션 풀의 특징은 다음과 같다.

  • 풀 속에 미리 커넥션이 생성되어 있기 때문에 커넥션을 생성하는 데 드는 연결 시간을 줄일 수 있다.
  • 커넥션을 계속해서 재사용하기 때문에 생성되는 커넥션 수가 일정하게 유지된다.

커넥션 풀을 사용하면 커넥션을 생성하고 닫는 데 필요한 시간이 소모되지 않기 때문에 그만큼 어플리케이션의 실행 속도가 빨라진다. 또한, 한 번에 생성될 수 있는 커넥션 수를 제어하기 때문에 동시 접속자 수가 몰려도 웹 어플리케이션이 쉽게 다운되지 않는다. 커넥션 풀을 사용하면 전체적인 웹 어플리케이션의 성능과 처리량이 향상되므로 많은 웹 어플리케이션이 커넥션 풀을 기본으로 사용하고 있다.

다양한 커넥션 풀 라이브러리가 존재하는데, 이 절에서는 오픈 소스 프로젝트인 DBCP API를 이용해서 커넥션 풀을 제공하는 방법을 살펴보자.

2. DBCP를 이용해서 커넥션 풀 사용하기

자카르타 프로젝트의 DBCP2 API를 사용할 때에는 다음과 같은 과정을 거치면 된다.

  1. DBCP 관련 jar 파일과 JDBC 드라이버 jar 파일 설치하기
  2. 커넥션 풀 초기화하기
  3. 커넥션 풀로부터 커넥션 사용하기

이 세 가지 절차에 대해서 차례대로 살펴보자.

1. 필요한 jar 파일 복사하기

DBCP API를 사용하기 위해서는 다음과 같은 라이브러리가 필요하다.

  • Commons DBCP API 관련 jar 파일
  • Commons DBCP API가 사용하는 Commons Pool API의 jar 파일
  • 로그 기록에 사용하는 Commons Logging 관련 jar 파일

위의 라이브러리들은 http://commons.apache.org/ 사이트에서 다운로드할 수 있다.

이 세 파일의 압축을 풀어 세 개의 jar 파일을 WEB-INF/lib 디렉터리에 복사한다.

2. 커넥션 풀 초기화 서블릿 클래스

커넥션 풀 초기화 코드는 다소 복잡하다. 일단 코드를 먼저 보자.

package jdbc;

import java.sql.DriverManager;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import org.apache.commons.dbcp2.ConnectionFactory;
import org.apache.commons.dbcp2.DriverManagerConnectionFactory;
import org.apache.commons.dbcp2.PoolableConnection;
import org.apache.commons.dbcp2.PoolableConnectionFactory;
import org.apache.commons.dbcp2.PoolingDriver;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

public class DBCPInit extends HttpServlet {

    @Override
    public void init() throws ServletException {
        loadJDBCDriver();
        initConnectionPool();
    }
	
    // 커넥션 풀이 내부에서 사용할 JDBC 드라이버를 로딩한다.
    // 예제의 경우 MySQL에 연결하므로 MySQL용 JDBC 드라이버를 로딩한다.
    private void loadJDBCDriver() {
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException ex) {
            throw new RuntimeException("fail to load JDBC Driver", ex);
        }
    }

    private void initConnectionPool() {
        try {
            String jdbcUrl =
                    "jdbc:mysql://localhost:3306/jsptest?" +
                            "useUnicode=true&characterEncoding=utf8";
            String username = "jspexam";
            String pw = "jsppw";
			
            // 커넥션 풀이 새로운 커넥션을 생성할 때 사용할 커넥션 팩토리를 생성한다.
            // MySQL에 연결할 때 사용할 JDBC URL, DB 계정, 암호를 생성자로 지정한다.
            ConnectionFactory connFactory =
                    new DriverManagerConnectionFactory(jdbcUrl, username, pw);
			
            // PoolableConnection을 생성하는 팩토리를 생성한다. DBCP는 커넥션 풀에 커넥션을 보관할 때 PoolableConnection을 사용한다. 
            // 이 클래스는 내부적으로 실제 커넥션을 담고 있으며, 커넥션 풀을 관리하는 데 필요한 기능을 추가로 제공한다.
            // 예를 들어 close() 메서드를 실행하면 실제 커넥션을 종료하지 않고 풀에 커넥션을 반환한다.
            PoolableConnectionFactory poolableConnFactory =
                    new PoolableConnectionFactory(connFactory, null);
            // 커넥션이 유효한지 여부를 검사할 때 사용할 쿼리를 지정한다.
            poolableConnFactory.setValidationQuery("select 1");
            
			// 커넥션 풀의 설정 정보를 생성한다.
            // 유휴 커넥션 검사 주기
            // 풀에 보관 중인 커넥션이 유효할 때 검사할지 여부
            // 커넥션 최소 개수
            // 커넥션 최대 개수
            GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
            poolConfig.setTimeBetweenEvictionRunsMillis(1000L * 60L * 5L);
            poolConfig.setTestWhileIdle(true);
            poolConfig.setMinIdle(4);
            poolConfig.setMaxTotal(50);
			
            // 커넥션 풀을 생성한다.
            // 생성자는 PoolableConnection을 생성할 때 사용할 팩토리와
            // 커넥션 풀 설정을 파라미터로 전달받는다.
            GenericObjectPool<PoolableConnection> connectionPool =
                    new GenericObjectPool<>(poolableConnFactory, poolConfig);
            // PoolableConnectionFactory에도 커넥션 풀을 연결한다.
            poolableConnFactory.setPool(connectionPool);
	    // 커넥션 풀을 제공하는 JDBC 드라이버를 등록한다.
            Class.forName("org.apache.commons.dbcp2.PoolingDriver");
            PoolingDriver driver =
                    (PoolingDriver) DriverManager.getDriver("jdbc:apache:commons:dbcp:");
            // 커넥션 풀 드라이버에 위에서 생성한 커넥션 풀을 등록한다. 
            // "chap14"를 커넥션 풀 이름으로 주었는데, 이 경우 프로그램에서 사용하는 JDBC URL은 
            // "jdbc:apace:commons:dbcp:chap14"가 된다.
            driver.registerPool("chap14", connectionPool);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

코드가 다소 복잡한데, 요약하면 다음과 같다.

  • 실제 커넥션을 생성할 ConnectionFactory를 생성한다.
  • 커넥션 풀로 사용할 PoolableConnection을 생성하는 PoolableConnectionFactory를 생성한다.
  • 커넥션 풀의 설정 정보를 생성한다.
  • 커넥션 풀을 사용할 JDBC 드라이버를 생성한다.

커넥션의 유효시간
회사 내부에서 사용하는 그룹웨어가 있다고 하자. 이 그룹웨어는 내부적으로 20개의 커넥션을 미리 생성한 커넥션 풀을 사용하고 있다. 새벽 시간대에 이 그룹웨어를 사용하는 사람은 아무도 없다. 그런데, DBMS에 설정된 커넥션의 유효시간이 10분이다. 이렇게 되면 새벽 시간 동안 모든 커넥션이 10분 넘게 사용되지 않으므로 DBMS는 커넥션 풀에 있는 모든 커넥션의 연결을 끊는다. 커넥션 풀의 모든 연결이 끊긴 상태에서 그룹웨어에 접속하면 DB에 연결할 수 없기 때문에 익셉션이 발생한다.
이런 문제를 방지하기 위해 커넥션 풀은 커넥션이 유효한지(즉, 연결이 끊어지지 않았는지) 여부를 검사하는 기능을 제공하고 있다. 일정 주기로 풀에 있는 커넥션을 검사하기도 하고, 커넥션을 풀에서 가져올 때 검사하기도 한다. 커넥션이 유효한지 여부를 검사할 때 흔히 사용하는 방법이 쿼리를 실행하는 것이다. 위의 예제 코드의 "select 1" 쿼리를 사용해서 커넥션을 검사하도록 했다.

3. 커넥션 풀 초기화 서블릿 설정

커넥션 풀을 초기화하기 위한 서블릿을 만들었으므로, 이제 웹 어플리케이션을 구동할 때 이 서블릿을 실행하도록 설정할 차례이다. 앞서 DriverLoader와 동일하게 web.xml 파일에 설정하면 된다. 아래와 같은 코드를 web.xml 파일에 추가해주면 된다.

<servlet>
    <servlet-name>DBCPInit</servlet-name>
    <servlet-class>jdbc.DBCPInit</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>

위와 같이 코드를 추가하면 웹 어플리케이션이 시작할 때 DBCPInit 서블릿 클래스가 자동으로 시작되고 init() 메서드가 호출된다.

4. 커넥션 풀로부터 커넥션 사용하기

DBCP가 제공하는 PoolingDriver는 커넥션 풀을 위한 JDBC 드라이버이다. PoolingDriver를 통해 커넥션 풀로부터 커넥션을 가져오려면 다음 형식의 JDBC URL을 사용하면 된다.

jdbc:apache:commons:dbcp:풀이름

풀 이름은 PoolingDriver에 커넥션 풀을 등록할 때 지정한다. 예를 들어, 앞서 DBCPInit 클래스를 보면 다음과 같은 코드가 있다.

Class.forName("org.apache.commons.dbcp2.PoolingDriver");
PoolingDriver driver = (PoolingDriver) DriverManager.getDriver("jdbc.apache:commmons:dbcp:");
driver.registerPool("jsptest", connectionPool);

위 코드를 보면 커넥션 풀을 등록할 때, 풀의 이름으로 "jsptest"를 사용했다. 따라서, 위 커넥션 풀로부터 커넥션을 구하려면 다음과 같은 JDBC URL을 사용하면 된다.

Connection conn = null;
...
try{
    String jdbcUrl = "jdbc:apache:commons:dbcp:jsptest";
    // 커넥션 풀에서 커넥션을 구함
    conn = DriverManager.getConnection(jdbcUrl);
    ...
}finally{
    ...
    // 커넥션을 풀에 반환함
    if(conn != null) try { conn.close(); } catch(SQLException ex) {}
}

이 코드를 보면 JDBC API 기반의 커넥션 풀을 사용한다고 해서 특별히 코드가 달라지는 부분이 없다는 것을 알 수 있다. 일반 경우와 마찬가지로 DriverManager.getConnection() 메서드를 사용해서 커넥션을 구해오고, 커넥션을 사용하면 close() 메서드로 사용한 커넥션을 닫는다.

커넥션 풀에서 구한 Connection의 close() 메서드를 호출하면 DB와의 연결을 끊지 않고 해당 커넥션을 풀에 반환한다.

실제로 커넥션 풀을 사용하는 완전한 예제는 다음과 같다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ 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>회원 목록</title>
</head>
<body>

MEMBER 테이블의 내용
<table width="100%" border="1">
    <tr>
        <td>이름</td><td>아이디</td><td>이메일</td>
    </tr>
    <%

        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;

        try{
            String jdbcDriver = "jdbc:apache:commons:dbcp:jsptest";
            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("NAME")%></td>
        <td><%=rs.getString("MEMBERID")%></td>
        <td><%=rs.getString("EMAIL")%></td>
    </tr>
    <%
            }
        }finally {
            if(rs != null) try{ rs.close(); } catch (SQLException ex){}
            if(stmt != null) try{ stmt.close(); } catch (SQLException ex){}
            if(conn != null) try{ conn.close(); } catch (SQLException ex){}
        }
    %>
</table>

</body>
</html>

viewMemberUsingPool.jsp를 실행하면 아래 그림처럼 MEMBER 테이블로부터 조회한 결과가 출력되는 것을 확인할 수 있다.

5. 커넥션 풀 속성 설명

앞에서 살펴본 DBCPInit 클래스를 보면 커넥션 풀을 설정할 때 GenericObjectPoolConfig를 사용했다. 이 클래스는 커넥션 풀의 크기, 커넥션의 검사 주기 등을 설정할 수 있는 메서드를 제공하고 있다. 예를 들어, 커넥션 풀의 최대 크기를 지정할 수 있는 setMaxTotal() 메서드를 제공한다. setMaxTotal() 메서드를 포함해 커넥션 풀의 개수와 대기 시간을 설정하기 위한 GenericObjectPoolConfig 클래스의 설정 메서드는 아래 표와 같다.

메서드 설명 기본값
setMaxTotal(int) 풀이 관리하는 커넥션의 최대 개수를 설정한다. 음수면 제한이 없다. 8
setMaxIdle(int) 커넥션 풀이 보관할 수 있는 최대 유휴 커넥션 개수를 지정한다. 음수면 제한이 없다. 8
setMinIdle(int) 커넥션 풀이 유지할 최소 유휴 커넥션 개수를 지정한다. 이 값이 maxIdle 보다 크면 maxIdle을 minIdle 값으로 사용한다. 0
setBlockWhenExhausted(boolean) 풀이 관리하는 커넥션이 모두 사용중인 상태에서 커넥션을 요청할 때 풀에 커넥션이 반환될 때까지 대기할지 여부를 지정한다. true면 대기하고, false면 NoSuchElementException을 발생한다. true
setMaxWaitMillis(long) blockWhenExhausted가 true일 때, 최대 대기 시간을 설정한다. 음수면 풀에서 커넥션을 구할 수 있을 때까지 대기한다. 단위는 밀리초이다. -1L

커넥션 풀은 사용되지 않는 유휴 커넥션을 제거하는 기능을 제공한다. 이 기능을 사용하면 주기적으로 커넥션을 검사하기 때문에, DBMS에서 연결을 끊기 전에 먼저 커넥션을 풀에서 제거할 수 있다. 관련 메서드는 아래 표와 같다.

메서드 설명 기본값
setTimeBetweenEvictionRunsMillis(long) 풀에 있는 유휴 커넥션 검사 주기를 설정한다. 양수가 아니면 검사하지 않는다. 단위는 밀리초이다. -1L
setNumTestsPerEvictionRun(int) 각 주기 때마다 검사할 커넥션의 개수를 설정한다. 음수면 유휴 커넥션 개수를 검사 개수의 절대 값으로 나눈다. 3
setMinEvictableIdleTimeMillis(long) 풀에 머물 수 있는 최소 유휴 시간을 설정한다. 커넥션을 검사할 때 이 시간을 초과한 커넥션이 제거 대상이 된다. 이 시간이 양수가 아니면 유휴 시간으로 검사하지 않는다. 단위는 밀리초이다. 1800000L(30분)
setTestWhileIdle(boolean) true이면 유휴 커넥션이 유휴한지 검사한다. false
setTestOnBorrow(boolean) true이면 커넥션 풀에서 커넥션을 가져올 때 유효한지 검사한다. false
setTestOnReturn(boolean) true이면 커넥션을 풀에 반환할 때 유효한지 검사한다. false

커넥션 풀에 있는 커넥션 중 오랜 시간 동안 사용되지 않는 커넥션은 DBMS와 연결이 끊길 가능성이 높다. 연결이 끊긴 커넥션을 사용하면 프로그램 실행 도중 오류가 발생하기 때문에 주기적으로 커넥션 풀에 있는 커넥션을 검사해서 사전에 제거해주는 것이 좋다. DBCP는 다음의 두 가지 기준으로 풀에 있는 커넥션을 검사한다.

  1. 커넥션이 최소 유휴 시간보다 오래 풀에 있는 경우(minEvictableIdleTimeMillis)
  2. 커넥션이 유효한지 여부 확인(testWhileIdle)

이 두 속성은 기본적으로 검사 주기(timeBetweenEvictionRunsMillis)가 양수일 때 적용된다. 검사 주기가 양수가 아니면 유휴 커넥션 제거 처리 자체를 하지 않기 때문에, 검사 주기 값을 설정해야 유효 커넥션을 제거한다.

testWhileIdle이 true면 PoolableConnectionFactory와 커넥션 검사 기능을 사용한다. 앞서 DBCPInit을 보면 다음 코드를 찾을 수 있는데, 이 코드에서 setValidateQuery() 메서드가 커넥션이 유효한지 검사할 때 사용할 쿼리를 지정한다.

PoolableConnectionFactory poolableConnFactory =
            new PoolableConnectionFactory(connFactory, null);
poolableConnFactory.setValidationQuery("select 1");

PoolableConnectionFactory 클래스의 커넥션 검사 기능과 관련된 설정 메서드는 아래 표와 같다.

메서드 설명 기본값
setMaxConnLifetimeMillis(long) 커넥션의 최대 사용 시간을 설정한다. 커넥션을 생성한 뒤 최대 사용 시간이 지나면 유효하지 않은 것으로 간주한다. 양수가 아니면 적용하지 않는다. 단위는 밀리초이다. -1L
setValidationQuery(String) 커넥션을 검사할 때 사용할 쿼리를 설정한다.

GenericObjectPoolConfig 클래스에서 설정하는 풀의 몇 가지 속성은 성능에 큰 영향을 미치기 때문에 웹 어플리케이션의 사용량에 따라서 알맞게 지정해야 한다.

  • maxTotal : 사이트의 최대 커넥션 사용량을 기준으로 지정한다. 이 값이 불필요하게 커질 경우 커넥션 개수가 비대하게 늘어나 DBMS가 수용할 수 있는 수준을 넘어서면 오히려 전체 성능에 좋지 않은 영향을 끼칠 수 있다.
  • minIdle : 사용되지 않는 커넥션의 최소 개수를 0으로 지정하면 풀에 저장된 커넥션 개수가 0이 될 수 있다. 이 경우 커넥션이 필요할 때 다시 커넥션을 생성하게 된다. 따라서 커넥션의 최소 개수는 시스템 사용이 가장 적은 시간을 기준으로 설정한다.
  • timeBetweenEvictionRunsMillis : 이 값을 설정해서 주기적으로 유휴 커넥션을 풀에서 제거하는 것이 좋다. 커넥션의 동시 사용량은 보통 새벽에 최저이고 낮 시간대에 최대에 이르게 된다. 이 두 시간대에 필요한 커넥션의 개수 차이는 수십에서 수백 개에 이른다. 이때 최대 상태에 접어들었다가 최소 상태로 가게 되면 풀에서 사용되지 않는 커넥션의 개수가 점차 증가한다. 따라서 DB와의 연결이 끊기기 전에 유휴 커넥션을 미리 풀에서 제거해주는 것이 좋다. 보통 10분 ~ 30분 단위로 유휴 커넥션을 검사하도록 지정하는 것이 좋다.
  • testWhileIdle : 유휴 커넥션을 검사할 때 유효하지 않은 커넥션도 검사해서 연결이 끊긴 커넥션을 사전에 제거하는 것이 좋다.
  • maxWaitMillis : 커넥션 풀에 커넥션이 없으면 일정 시간 대기 후 익셉션을 발생시키는 것이 좋다. 커넥션을 대기하느라 수십 초 이상 대기하면 사용자는 응답이 없으므로 브라우저의 새로 고침을 누르거나 DB를 사용하는 다른 페이지로 이동해서 다시 풀에서 가져오기 위해 대기하게 된다. 사용자가 아무 응답 없이 대기하는 것 보다 1초 미만의 길지 않은 시간 안에 커넥션을 구하지 못하면 익셉션을 발생시켜서 알맞은 안내 화면을 보여주는 것이 더 좋다.

참고

  • 최범균의 JSP2.3 웹 프로그래밍
profile
이것저것 관심많은 개발자.

0개의 댓글