이처럼 커넥션을 새로 만드는 과정은 매우 복잡하고 시간이 많이 소모되는 일이다. 이로 인해 사용자에게 좋지 않은 경험을 줄 수 있다.
이 같은 문제를 해결하기 위해, 커넥션을 미리 생성해두고 사용하는 커넥션 풀(커넥션을 관리하는 풀)을 사용할 수 있다.
애플리케이션 시작 시점에 필요한 커넥션을 미리 확보하여 풀에 보관. 일반적으로 10개를 확보한다. (서비스마다 다르다)
커넥션 풀에 있는 커넥션은 TCP/IP로 DB와 커넥션이 연결되어있는 상태이기 때문에 언제든지 즉시 SQL을 DB로 전달할 수 있다.
commons-dbcp2
, tomcat-jdbc pool
, HikariCP
HikariCP
를 사용하는 추세.커넥션을 얻는 방법은 다양하다. 만약 DriverManager를 이용해 커넥션을 획득하다가 HikariCP 같은 커넥션 풀을 사용하도록 변경하면 의존관계의 변경으로 코드의 변경이 필요하다.
이 문제를 해결하기 위해 자바에서 DataSource라는 인터페이스를 제공한다.
javax.sql.DataSource
인터페이스DataSource
커넥션을 획득하는 방법을 추상화하는 인터페이스public interface DataSource {
Connection getConnection() thorws SQLException;
}
DataSource
인터페이스를 구현해 둠DriverManager
는 해당 인터페이스를 사용하지 않으므로 직접 구현해야 한다.DriverManagerDataSource
라는 구현체를 제공한다.package hello.jdbc.connection;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import static hello.jdbc.connection.ConnectionConst.*;
@Slf4j
public class ConnectionTest {
@Test
void driverManager() throws SQLException {
Connection con1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
Connection con2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("connection={}, class={}", con1, con1.getClass());
log.info("connection={}, class={}", con2, con2.getClass());
}
}
package hello.jdbc.connection;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import static hello.jdbc.connection.ConnectionConst.*;
@Slf4j
public class ConnectionTest {
@Test
void driverManager() throws SQLException {
Connection con1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
Connection con2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("connection={}, class={}", con1, con1.getClass());
log.info("connection={}, class={}", con2, con2.getClass());
}
@Test
void dataSourceDriverManager() throws SQLException {
//DriverManagerDataSource - 항상 새로운 커넥션 획득
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL,
USERNAME, PASSWORD);
useDataSource(dataSource);
}
private void useDataSource(DataSource dataSource) throws SQLException {
Connection con1 = dataSource.getConnection();
Connection con2 = dataSource.getConnection();
log.info("connection={}, class={}", con1, con1.getClass());
log.info("connection={}, class={}", con2, con2.getClass());
}
}
DriverManagerDataSource - Creating new JDBC DriverManager Connection to
[jdbc:h2:tcp:..test]
DriverManagerDataSource - Creating new JDBC DriverManager Connection to
[jdbc:h2:tcp:..test]
connection=conn0: url=jdbc:h2:tcp://..test user=SA, class=class
org.h2.jdbc.JdbcConnection
connection=conn1: url=jdbc:h2:tcp://..test user=SA, class=class
org.h2.jdbc.JdbcConnection
DataSource
를 통해 커넥션을 획득하고 있다.DriverManager.getConnection(URL, USERNAME, PASSWORD)
DriverManager.getConnection(URL, USERNAME, PASSWORD)
void dataSourceDriverManager() throws SQLException {
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL,
USERNAME, PASSWORD);
useDataSource(dataSource);
}
private void useDataSource(DataSource dataSource) throws SQLException {
Connection con1 = dataSource.getConnection();
Connection con2 = dataSource.getConnection();
log.info("connection={}, class={}", con1, con1.getClass());
log.info("connection={}, class={}", con2, con2.getClass());
}
DriverManager
: 커넥션 획득시 마다 URL, USERNAME, PASSWORD와 같은 파라미터를 계속 전달해야 한다.DataSource
: 처음 객체 생성시에만 필요한 파라미터를 넘겨두고, 커넥션 획득시 단순히 data.getConnection()
만 호출하면 된다.설정: URL, USERNAME, PASSWORD같은 부분을 입력하는 것. 설정과 관련된 속성들은 한 곳에 있는 것이 향후 변경에 더 유연하게 대처할 수 있다.
사용: 설정은 고려하지 않고, getConnection()만 호출하여 사용할 수 있다.
리포지토리는 DataSource
만 의존하고, 속성은 몰라도 된다.
애플리케이션을 개발하다보면 설정은 한 곳에서 하나 사용은 수많은 곳에서 하게 된다.
DatSource
를 통해 커넥션 풀을 사용하는 예제
import com.zaxxer.hikari.HikariDataSource;
@Test
void dataSourceConnectionPool() throws SQLException, InterruptedException {
//커넥션 풀링: HikariProxyConnection(Proxy) -> JdbcConnection(Target)
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
dataSource.setMaximumPoolSize(10);
dataSource.setPoolName("MyPool");
useDataSource(dataSource);
Thread.sleep(1000); //커넥션 풀에서 커넥션 생성 시간 대기
}
HikariDataSource
는 DataSource
인터페이스를 구현MyPool
Thread.sleep(1000)
을 통해 대기시간을 주어야 쓰레드 풀에 커넥션이 생성되는 로그를 확인할 수 있다.#커넥션 풀 초기화 정보 출력
HikariConfig - MyPool - configuration:
HikariConfig - maximumPoolSize................................10
HikariConfig - poolName................................"MyPool"
#커넥션 풀 전용 쓰레드가 커넥션 풀에 커넥션을 10개 채움
[MyPool connection adder] MyPool - Added connection conn0: url=jdbc:h2:.. user=SA
[MyPool connection adder] MyPool - Added connection conn1: url=jdbc:h2:.. user=SA
[MyPool connection adder] MyPool - Added connection conn2: url=jdbc:h2:.. user=SA
[MyPool connection adder] MyPool - Added connection conn3: url=jdbc:h2:.. user=SA
[MyPool connection adder] MyPool - Added connection conn4: url=jdbc:h2:.. user=SA
...
[MyPool connection adder] MyPool - Added connection conn9: url=jdbc:h2:.. user=SA
#커넥션 풀에서 커넥션 획득1
ConnectionTest - connection=HikariProxyConnection@446445803 wrapping conn0:
url=jdbc:h2:tcp://localhost/~/test user=SA, class=class
com.zaxxer.hikari.pool.HikariProxyConnection
#커넥션 풀에서 커넥션 획득2
ConnectionTest - connection=HikariProxyConnection@832292933 wrapping conn1:
url=jdbc:h2:tcp://localhost/~/test user=SA, class=class
com.zaxxer.hikari.pool.HikariProxyConnection
MyPool - After adding stats (total=10, active=2, idle=8, waiting=0
#커넥션 풀 초기화 정보 출력
HikariConfig - MyPool - configuration:
HikariConfig - maximumPoolSize................................10
HikariConfig - poolName................................"MyPool"
풀의 이름과 최대 풀 수 확인가능
#커넥션 풀 전용 쓰레드가 커넥션 풀에 커넥션을 10개 채움
[MyPool connection adder] MyPool - Added connection conn0: url=jdbc:h2:.. user=SA
[MyPool connection adder] MyPool - Added connection conn1: url=jdbc:h2:.. user=SA
[MyPool connection adder] MyPool - Added connection conn2: url=jdbc:h2:.. user=SA
[MyPool connection adder] MyPool - Added connection conn3: url=jdbc:h2:.. user=SA
[MyPool connection adder] MyPool - Added connection conn4: url=jdbc:h2:.. user=SA
...
[MyPool connection adder] MyPool - Added connection conn9: url=jdbc:h2:.. user=SA
MyPool - After adding stats (total=10, active=2, idle=8, waiting=0
active=2
, 풀에서 대기 상태인 커넥션 idle=8
을 확인할 수 있다.package hello.jdbc.repository;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.support.JdbcUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.NoSuchElementException;
/**
* JDBC - DataSource 사용, JdbcUtils 사용
*/
@Slf4j
public class MemberRepositoryV1 {
private final DataSource dataSource;
public MemberRepositoryV1(DataSource dataSource) {
this.dataSource = dataSource;
}
//save()...
//findById()...
//update()....
//delete()....
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
private Connection getConnection() throws SQLException {
Connection con = dataSource.getConnection();
log.info("get connection={}, class={}", con, con.getClass());
return con;
}
}
DataSource
의존관계 주입DataSource
주입받아서 사용한다.JdbcUtils
편의 메서드package hello.jdbc.repository;
import com.zaxxer.hikari.HikariDataSource;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import java.sql.SQLException;
import java.util.NoSuchElementException;
import static hello.jdbc.connection.ConnectionConst.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@Slf4j
class MemberRepositoryV1Test {
MemberRepositoryV1 repository;
@BeforeEach
void beforeEach() throws Exception {
//기본 DriverManager - 항상 새로운 커넥션 획득
//DriverManagerDataSource dataSource =
// new DriverManagerDataSource(URL, USERNAME, PASSWORD);
//커넥션 풀링: HikariProxyConnection -> JdbcConnection
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
repository = new MemberRepositoryV1(dataSource);
}
@Test
void crud() throws SQLException, InterruptedException {
log.info("start");
//save
Member member = new Member("memberV0", 10000);
repository.save(member);
//findById
Member memberById = repository.findById(member.getMemberId());
assertThat(memberById).isNotNull();
//update: money: 10000 -> 20000
repository.update(member.getMemberId(), 20000);
Member updatedMember = repository.findById(member.getMemberId());
assertThat(updatedMember.getMoney()).isEqualTo(20000);
//delete
repository.delete(member.getMemberId());
assertThatThrownBy(() -> repository.findById(member.getMemberId()))
.isInstanceOf(NoSuchElementException.class);
}
}
get connection=**conn0**: url=jdbc:h2:.. user=SA class=class org.h2.jdbc.JdbcConnection
get connection=**conn1**: url=jdbc:h2:.. user=SA class=class org.h2.jdbc.JdbcConnection
get connection=**conn2**: url=jdbc:h2:.. user=SA class=class org.h2.jdbc.JdbcConnection
get connection=**conn3**: url=jdbc:h2:.. user=SA class=class org.h2.jdbc.JdbcConnection
get connection=**conn4**: url=jdbc:h2:.. user=SA class=class org.h2.jdbc.JdbcConnection
get connection=**conn5**: url=jdbc:h2:.. user=SA class=class org.h2.jdbc.JdbcConnection
-DriverManagerDataSource
사용 시, 항상 새로운 커넥션이 생성되어 사용된다.
get connection=HikariProxyConnection@xxxxxxxx1 wrapping conn0: url=jdbc:h2:...
user=SA
get connection=HikariProxyConnection@xxxxxxxx2 wrapping conn0: url=jdbc:h2:...
user=SA
get connection=HikariProxyConnection@xxxxxxxx3 wrapping conn0: url=jdbc:h2:...
user=SA
get connection=HikariProxyConnection@xxxxxxxx4 wrapping conn0: url=jdbc:h2:...
user=SA
get connection=HikariProxyConnection@xxxxxxxx5 wrapping conn0: url=jdbc:h2:...
user=SA
get connection=HikariProxyConnection@xxxxxxxx6 wrapping conn0: url=jdbc:h2:...
user=SA
conn0
커넥션이 재사용되고 있다.conn0
만 사용된다.DI+OCP 구현으로, 이후 구현체가 바뀌더라도 코드의 변경을 최소화할 수 있다!
connection을 close()하더라도 hikari connection이 감싸고 있어서 반환하는 로직이 안에 들어가있다.