안녕하세요. 이번 포스팅에서는 Connection Pool, DataSource에 대해 알아보겠습니다.
JDBC와 DriverManager에 관한 개념은 이전 글에서 확인이 가능합니다.

데이터베이스 커넥션을 획득할 때는 아래와 같은 복잡한 과정을 거칩니다.
1. DB 드라이버를 통해 커넥션을 조회 시도
2. DB 드라이버는 DB와 TCP/IP 커넥션 연결(3 way handshake 발생)
3. DB 드라이버는 TCP/IP 커넥션이 연결되면 ID, PW 등 DB에 정보를 전달
4. DB는 받은 정보로 내부 인증을 완료하고 내부에 DB 세션을 생성 후 커넥션 생성 완료 응답을 보냄
5. DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환
SQL문을 실행하여 데이터베이스와 통신을 할 때 한 번씩 커넥션을 획득해야 하는데 위와 같은 과정을 반복적으로 거치므로 사용자 입장에서는 속도가 느린 어플리케이션을 사용하게 될 수도 있습니다.
이러한 문제를 해결하기 위해 나온 아이디어가 커넥션 풀(Connection Pool)입니다.
커넥션 풀(Connection Pool)
커넥션 풀은 커넥션을 관리하는 풀(공간)입니다.
커넥션 풀은 애플리케이션을 시작하는 시점에 필요한 만큼 커넥션을 미리 획득해서 풀에 보관을 합니다.
기본값은 보통 10개이며 커넥션을 몇 개 보관하는지는 서비스의 특징과 서버 스펙에 따라 달라지므로 주의깊게 설정해야 합니다.
커넥션 풀에 들어 있는 커넥션은 데이터베이스와 TCP/IP로 이미 연결이 되어 있는 상태이기 때문에 SQL 호출이 발생할 때마다 즉시 SQL을 DB에 전달할 수 있습니다.
커넥션을 획득할 때 거치는 일련의 과정을 이미 모두 거친 후, 풀에 등록해놓은 것을 사용하는 것이기 때문에 SQL을 호출하는 속도가 빠릅니다.
커넥션을 사용하고 나면 커넥션을 종료하는 것이 아니라, 다음에 다시 사용할 수 있도록 사용했던 커넥션을 커넥션 풀에 다시 반납을 합니다.

커넥션 풀의 종류
커넥션 풀의 종류는 HikariCP, commons-dbcp2, tomcat-jdbc pool 등의 오픈소스가 존재하지만 요즘 대부분은 레거시 프로젝트가 아닌 이상 HikariCP를 사용합니다.
HikariCP는 스프링부트가 기본 커넥션풀로 채택했을 뿐만 아니라 성능, 사용의 편리함, 안정성 측면에서 여러 기관과 개발자들에게 검증된 커넥션 풀입니다.
커넥션 풀 예제
* 해당 예제는 DataSource 개념을 사용하므로 아래 DataSource 내용을 알아야 이해하실 수 있습니다.
// ConnectionConst.java
public abstract class ConnectionConst {
public static final String URL = "jdbc:h2:tcp://localhost/~/test";
public static final String USERNAME = "sa";
public static final String PASSWORD = "";
}
@Test
void dataSourceConnectionPool() throws SQLException, InterruptedException {
// 커넥션 풀링
HikariDataSource dataSource = new HikariDataSource();
// DataSource dataSource = new HikariDataSource(); JDBC 설정할 때는 업캐스팅으로 인한 불가능
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
dataSource.setMaximumPoolSize(10);
dataSource.setPoolName("My Pool");
Connection conn1 = dataSource.getConnection();
Connection conn2 = dataSource.getConnection();
log.info("connection={}, class={}", conn1, conn1.getClass());
log.info("connection={}, class={}", conn2, conn2.getClass());
Thread.sleep(3000);
}
예제에서 사용한 풀은 HikariCP를 사용했습니다.

커넥션 풀링을 하기 위해서는 먼저 HikariDataSource 객체를 만들고 JDBC 정보를 세팅해줍니다.(URL, USERNAME, PASSWORD)
뿐만 아니라 풀 사이즈와 풀 네임도 설정할 수 있습니다.
맨 아래에 Thread.sleep 3초를 설정한 이유는 아래와 같습니다.
커넥션 풀에 커넥션을 채울 때 별도의 쓰레드를 사용해서 채웁니다.
따라서 테스트를 돌릴 때는 메인 쓰레드 1개, 커넥션 풀링을 하는 쓰레드 1개가 동작을 하게 됩니다.
그런데 Junit 테스트는 메인 쓰레드가 종료되면 아예 테스트가 종료되어 커넥션 풀링을 하는 도중에 테스트가 종료될 수 있습니다.
이러한 문제를 방지하기 위해 메인쓰레드가 3초 후에 종료될 수 있도록 Thread.sleep(3000) 코드를 추가했습니다.

HikariDataSource Max Pool Size를 10으로 설정했기 때문에 총 10개의 커넥션을 획득하고 풀에 담은 것을 확인할 수 있습니다.
또한 예제 코드에서 getConnection 메서드를 2번 호출해서 사용중이므로 total=10, active=2, idle=8, waiting=0 상태를 확인할 수 있습니다.
DataSource
앞선 포스팅에서 처럼 DriverManager를 사용하여 SQL을 호출할 때마다 커넥션을 조회하는 방법으로 소스코드를 작성했다고 가정해보겠습니다.
그러다 커넥션 풀이라는 좋은 방법이 나왔으니 이를 사용하려고 하면 DriverManager와 관련된 모든 소스코드를 커넥션 풀 방식으로 변경해야 합니다.
이러한 번거로움을 줄이기 위해 자바에서 javax.sql.DataSource 인터페이스를 제공합니다.
DataSource는 커넥션을 획득하는 방법을 추상화한 인터페이스입니다.
DataSource가 인터페이스라는 것은 DataSource를 구현한 여러 구현체(HikariDataSource 등)들은 커넥션을 획득할 수 있는 메서드를 가지고 있다는 의미입니다.
DataSource의 핵심 기능은 커넥션 조회입니다.
소스코드를 보면 getConnection 메서드가 존재합니다.

HikariCP 등 대부분의 커넥션 풀은 DataSource 인터페이스를 이미 구현해두었습니다.
따라서 개발자는 DataSource 인터페이스에만 의존하는 로직을 작성하면 됩니다.
만약 commons-dbcp2를 사용하다가 HikariCP로 변경하고 싶다면 HikariCP 구현체로 갈아끼우면 됩니다.
DriverManagerDataSource
DriverManager는 DataSource 인터페이스를 사용하지 않습니다.
그래서 DriverManager는 직접 사용해야 한다는 문제점이 있는데 이러한 문제를 해결하기 위해 스프링은 DataSource 인터페이스를 구현한 DriverManagerDataSource라는 클래스를 제공합니다.

만약 자바에서 커넥션 풀을 사용하지 않는다면 DriverManager을 사용하지 않고 DriverManagerDataSource를 사용하는게 추후 확장성 측면에서 바람직하다고 생각합니다.
DriverManagerDataSource 예제
// ConnectionConst.java
public abstract class ConnectionConst {
public static final String URL = "jdbc:h2:tcp://localhost/~/test";
public static final String USERNAME = "sa";
public static final String PASSWORD = "";
}
기존 DriverManager가 커넥션을 조회할 때 사용하는 소스코드
// ConnectionTest.java
@Test
void driverManager() throws SQLException {
Connection conn1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
Connection conn2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("connection={}, class={}", conn1, conn1.getClass());
log.info("connection={}, class={}", conn2, conn2.getClass());
}
DriverManagerDataSource를 사용한 소스코드
// ConnectionTest.java
@Test
void driverManagerDataSource() throws SQLException {
// DriverManagerDataSource 항상 새로운 커넥션 획득
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
Connection conn1 = dataSource.getConnection();
Connection conn2 = dataSource.getConnection();
log.info("connection={}, class={}", conn1, conn1.getClass());
log.info("connection={}, class={}", conn2, conn2.getClass());
}
아래 결과는 driverManager, driverManagerDataSource를 실행한 동일한 결과입니다.

DriverManagerDataSource를 생성할 때 필요한 정보를 한 번만 등록하면 되므로 getConnection 메서드의 파라미터가 없어도 커넥션을 획득할 수 있는 것을 확인할 수 있습니다.
해당 내용은 김영한 강사님의 강의를 듣고 정리한 내용입니다.