본문 바로가기

Web Sever 개발과 CS 기초/스프링

ConnectionPool에 대한 이해와 DataSource 인터페이스로 커넥션을 획득하는 방법 통일하기

관련 내용

해당 프로젝트 깃허브 

DB외에 ConnectionPool을 사용하는 곳 - HTTPClient CP

 

<Spring DB를 사용하기 위한 JDBC, Connection 기초>

  • JDBC 기술이 어떤 역할을 하는 지, 커넥션은 어떻게 획득하는 지, SQL 쿼리와 JDBC 사용법은 어떻게 되는지에 대한 설명

[백엔드/스프링] - 스프링 DB 사용을 위한 JDBC에 대한 이해와 사용 방법

  • (현재 글)커넥션 풀에 대한 이해와, DataSource 인터페이스를 사용한, 커넥션 얻어오기 통일화 방법

[백엔드/스프링] - ConnectionPool에 대한 이해와 DataSource 인터페이스로 커넥션을 획득하는 방법 통일하기

개요 목적

시간이 오래 걸리는 DB Connection 획득 문제를 해결하는 ConnectionPool에 대해서 알아본다.

그리고 커넥션 풀 회사마다 커넥션을 획득 하는 방법이 전부 다르다. 그 방법과 코드를 통일 시켜준느 DataSource 인터페이스에 대해서 알아본다. DataSource 인터페이스를 사용하면 DriverManager에서 커넥션을 획득하는 방법도 함께 통일 시킬 수 있다.

개발 환경

  • SpringBoot(gradle) -2.7.7
  • h2 - 2.1.4.214

커넥션 풀에 대한 이해

DB Connection Pool을 잘 이해하기 위해, 복잡한 DB connection 획득에 대해서 먼저 알아보자.

어플리케이션은 DB 관련 로직을 수행할 때, DB Connection을 획득해야 한다. 예를 들어, 회원 가입 로직을 실행 할 때, 회원 가입자 목록 DB Table에 접근할 Connection이 필요한 것이다. DB Connection을 획득하기 위해서는 먼저,

  • 어플리케이션은 DB드라이버를 통해 DB와 TCP/IP 커넥션 연결을 한다.
  • 그리고 DB 드라이버를 통해 id, password 인증 정보를 보낸다.
  • DB는 인증 정보를 확인해서 DB 연결이 완료 되었다는 응답을 보낸다.
  • 마지막으로 응답을 받은 DB 드라이버에서 커넥션 객체를 생성한다.

위와 같은 복잡한 절차로 Connection을 획득은 많은 자원과 시간이 든다. 매번 쿼리마다 새로운 커넥션을 획득한다면 서비스 반응 속도가 느려질 것이다. 그래서 해결 방법으로 DB Connection Pool을 사용한다.

(어플리케이션이 시작하는 시점에서 DB드라이버를 통해서 커넥션을 미리 생성해서 커넥션 풀에 담아 놓는다. 커넥션들은 TCP/IP가 연결되어 있는 상태이기 때문에 즉시 SQL를 DB로 전달 할 수 있다.)

SQL 쿼리 실행마다 DB 드라이버를 통해서 새로운 커넥션을 획득하는 것이 아니라. 커넥션 풀을 통해 이미 생성되어 있는 커넥션을 객체 참조로 그냥 가져다 쓴다. 그리고 쿼리가 끝나면, 다음에 다시 사용할 수 있도록, 커넥션 풀에 반납한다.

실무에서 가장 많이 사용하는 것은 HikariCP이다.

//HikariCP 설정 요소 알아보기
✅autoCommit(default:true)
connection이 종료나 pool에 반환할 때, connection에 속해있는 transaction을 commit 할지를 결정한다.
⏳connectionTimeout(default:30000)
풀에서 커넥션 연결을 기다리는 최대 시간을 제어한다. 
이 시간을 초과하면 SQLException이 발생한다. 허용 가능한 최저 연결 제한 시간은 250m이다. 
⏳idleTimeout
커넥션 풀에서 아직 연결되지 않고 유휴 상태로 있는 커넥션의 유지시간을 설정한다.
idleTimeout시간이 초과된 커넥션은 연결 해제된다.
⏳keepaliveTime
keepaliveTime에 도달하면 주기적으로 커넥션의 유효성을 검증한다.
커녁션이 잘 살아 있는지 확인한다.
⏳maxLifetime(default: 1800000(30분))
이 속성은 풀에서 커넥션의 최대 수명을 제어한다.
🔢minimumIdle(defualt: maximumPoolSize와 동일)
이 속성은 풀에서 유지하려는 최소 유휴 연결 수를 제어한다.
🔢maximumPoolSize(defaullt: 10)
이 속성은 유휴 및 사용 중인 연결을 모두 포함하여 풀이 도달할 수 있는 커넨션 최대 크기를 제어한다.
🔤poolName
이 등록 정보는 연결 풀에 대한 사용자 정의 이름을 나타낸다.

DataSource에 대한 이해-커넥션을 획득하는 방법 통일

DataSource의 필요성

이전 글에서는 DriverManager에서 커넥션 획득하는 방법에 대해서 알아보았고, 이번 글에서는 커넥션 풀을 활용한 빠른 커넥션 획득에 대해서 알아보았다. 여기서 문제는 커넥션을 획득하는 방법에 따라서 코드가 달라진다는 것이다. 예를 들어, 성능이 느려, DriverManager를 통한 직접 습득이 아니라, 커넥션 풀을 사용하기로 결정하면, 커넥션을 획득하는 부분의 코드를 전부다 찾아서 고쳐야 하는 번거로움이 발생한다.

이런 문제를 해결하는 것이 커넥션을 획득하는 방법을 추상화 하는 DataSource 인터페이스이다. 이제 어떤 커넥션 풀을 쓰더라도 DataSource에만 의존하도록 코드를 작성하면, 커넥션 풀을 교체할 때, 해당 구현체로 갈아 끼우기만 하면 된다.

DriverManager 는 DataSource 인터페이스를 사용하지 않는다. 이런 문제를 해결하기 위해 스프링은 DriverManager 도 DataSource 를 통해서 사용할 수 있도록 DriverManagerDataSource 라는DataSource 를 구현한 클래스를 제공한다. 덕분에 DriverManagerDataSource 를 통해서 DriverManager 를 사용하다가 커넥션 풀을 사용하도록 코드를 변경해도 애플리케이션 로직은 변경하지 않아도 된다.

DataSource - DriverManager 기능 사용

DriverManager를 통해서 커넥션을 획득하는 방법을 DataSource 인터페이스를 사용해서 구현하는 방법에 대해서 알아보자.

KIMHWANG\jdbc\src\test\java\hello\jdbc\connection\ConnectionTest.java

@Slf4j
public class ConnectionTest {

    @Test
    void DataSource_DriverManager() throws SQLException{
        //기존 DriverManager를 기존의 방식
        Connection con1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        log.info("connection={}, class={}", con1, con1.getClass());

        //DataSource 구현체 DriverManagerDataSource를 사용해서 커넥션 획득
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL,
            USERNAME, PASSWORD);
        Connection con2 = dataSource.getConnection();
        log.info("connection={}, class={}", con2, con2.getClass());
    }
}
//같은 H2 커넥션을 획득하는 것을 확인 할 수 있다.
INFO hello.jdbc.connection.ConnectionTest - connection1=conn0: url=jdbc:h2:tcp://localhost/~/test user=SA, 
class1=class org.h2.jdbc.JdbcConnection

INFO hello.jdbc.connection.ConnectionTest - connection2=conn1: url=jdbc:h2:tcp://localhost/~/test user=SA, 
class2=class org.h2.jdbc.JdbcConnection

(DriverManager는 DataSource인터페이스를 사용하지 않아서, Spring에서 DriverManger도 DataSource를 통해서 커넥션을 획득할 수 있도록, DriverManagerDataSource 라는DataSource 를 구현한 클래스를 제공한다. → 그래서 DriverManagerDataSource 객체를 만들어서 DataSource의 메소드인 getConnection을 사용해 커넥션을 획득한다.)

 

이 둘의 가장 큰 차이점

DriverManager는 커넥션을 획득할 때마다, URL, USERNAME, PASSWORD를 전달해 생성해야 하지만,

DataSource는 datasource 객체를 하나 만들면, 해당 객체에서 getConnection을 통해 커넥션을 획득할 수 있다. 덕분에 객체를 설정하는 부분과 사용하는 부분을 명확하게 분리 할 수 있어서, 유지 보수에 좋다.

DataSource - ConnectionPool 사용

이번에는 DataSource 를 통해 커넥션 풀을 사용하는 코드를 알아보자

KIMHWANG\jdbc\src\test\java\hello\jdbc\connection\ConnectionTest.java

import com.zaxxer.hikari.HikariDataSource;

@Slf4j
public class ConnectionTest {

    @Test
    void dataSourceConnectionPool() throws SQLException, InterruptedException {
        //HikariCP - DataSource 인터페이스 구현, 커넥션 획득을 위한 HikariDataSource 생성
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(URL);
        dataSource.setUsername(USERNAME);
        dataSource.setPassword(PASSWORD);
        dataSource.setMaximumPoolSize(10); //풀 내 커넥션 개수 정하기
        dataSource.setPoolName("MyPool"); //이름 지정

        //커넥션 획득하려고 하면, 커넥션을 얻기 위해 커넥션 풀을 채운다. 그리고 커넥션을 획득한다.
        Connection con1 = dataSource.getConnection();
        Connection con2 = dataSource.getConnection();
        log.info("connection={}, class={}", con1, con1.getClass());
        log.info("connection={}, class={}", con2, con2.getClass());
        
	//커넥션 풀에 10개의 커넥션이 찰 수 있도록 시간 벌어주기.
        Thread.sleep(2000);
    }
}
INFO com.zaxxer.hikari.HikariDataSource - MyPool - Starting...
DEBUG com.zaxxer.hikari.util.DriverDataSource - Loaded driver with class name org.h2.Driver for jdbcUrl=jdbc:h2:tcp://localhost/~/test
DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn0: url=jdbc:h2:tcp://localhost/~/test user=SA
INFO com.zaxxer.hikari.HikariDataSource - MyPool - Start completed.
INFO hello.jdbc.connection.ConnectionTest - connection=HikariProxyConnection@1250499735 wrapping conn0: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class com.zaxxer.hikari.pool.HikariProxyConnection
DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn1: url=jdbc:h2:tcp://localhost/~/test user=SA
INFO hello.jdbc.connection.ConnectionTest - connection=HikariProxyConnection@173791568 wrapping conn1: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class com.zaxxer.hikari.pool.HikariProxyConnection
DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Pool stats (total=2, active=2, idle=0, waiting=0)
DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn2: url=jdbc:h2:tcp://localhost/~/test user=SA
DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn3: url=jdbc:h2:tcp://localhost/~/test user=SA
DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn4: url=jdbc:h2:tcp://localhost/~/test user=SA
DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn5: url=jdbc:h2:tcp://localhost/~/test user=SA
DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn6: url=jdbc:h2:tcp://localhost/~/test user=SA
DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn7: url=jdbc:h2:tcp://localhost/~/test user=SA
DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn8: url=jdbc:h2:tcp://localhost/~/test user=SA
DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn9: url=jdbc:h2:tcp://localhost/~/test user=SA
DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - After adding stats (total=10, active=2, idle=8, waiting=0)

 


<Reference>

인프런 김영한 DB 강의