[SpringDB1] DataSource
DataSource 이해
커넥션을 얻는 방법은 앞서 학습한 JDBC DriverManager
를 직접 사용하거나, 커넥션 풀을 사용하는 등 다양한 방법이 존재한다.
DriverManager를 통해 커넥션 획득
DriverManager
를 통해 커넥션을 획득하다가, 커넥션 풀을 사용하는 방법으로 변경하려면 어떻게 해야할까?
DriverManager를 통해 커넥션을 획득하다가 커넥션 풀로 변경시 문제
- 예를 들어 애플리케이션 로직에서
DriverManager
를 사용해서 커넥션을 획득하다가HikariCP
같은 커넥션 풀을 사용하도록 변경하면 커넥션을 획득하는 애플리케이션 코드도 함께 변경해야 한다.- 의존관계가
DriverManager
에서HikariCP
로 변경되기 때문이다.
- 의존관계가
커넥션을 획득하는 방법을 추상화
- 자바에서는 이런 문제를 해결하기 위해
javax.sql.DataSource
라는 인터페이스를 제공한다. DataSource
는 커넥션을 획득하는 방법을 추상화하는 인터페이스이다.- 이 인터페이스의 핵심 기능은 커넥션 조회 하나이다. (다른 일부 기능도 있지만 중요하지 않다.)
DataSource 핵심 기능
public interface DataSource {
// 핵심 기능만 축약
Connection getConnection() throws SQLException;
}
정리
- 대부분의 커넥션 풀은
DataSource
인터페이스를 이미 구현해두었기 때문에, 개발자는 커넥션 풀의 코드를 직접 의존하는 것이 아니라DataSource
인터페이스만 의존하도록 애플리케이션 로직을 작성하면 된다. - 커넥션 풀 구현 기술을 변경하고 싶다면 해당 구현체로 변경만 하면 된다.
DriverManager
는DataSource
인터페이스를 사용하지 않기 때문에DriverManager
는 직접 사용해야 한다. 따라서,DriverManager
를 사용하다가DataSource
기반의 커넥션 풀을 사용하도록 변경한다면 관련 코드를 모두 수정해야 한다.- 이런 문제를 해결하기 위해 스프링은
DriverManager
도DataSource
를 통해서 사용할 수 있도록DriverManagerDataSource
라는DataSource
를 구현한 클래스를 제공한다.
- 이런 문제를 해결하기 위해 스프링은
- 자바는
DataSource
를 통해 커넥션을 획득하는 방법을 추상화했기 때문에, 애플리케이션 로직은DataSource
인터페이스에만 의존하면 된다.
DataSource 예제1 - DriverManager
DriverManager
...
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());
}
}
실행 결과
connection=conn0: url=jdbc:h2:tcp://..jdbc user=SA, class=class
org.h2.jdbc.JdbcConnection
connection=conn1: url=jdbc:h2:tcp://..jdbc user=SA, class=class
org.h2.jdbc.JdbcConnection
DriverManagerDataSource
@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:..jdbc]
DriverManagerDataSource - Creating new JDBC DriverManager Connection to
[jdbc:h2:tcp:..jdbc]
connection=conn0: url=jdbc:h2:tcp://..jdbc user=SA, class=class
org.h2.jdbc.JdbcConnection
connection=conn1: url=jdbc:h2:tcp://..jdbc user=SA, class=class
org.h2.jdbc.JdbcConnection
기존 코드와 비슷하지만 DriverManagerDataSource
는 DataSource
를 통해서 커넥션을 획득할 수 있다.
파라미터 차이
DriverManager
는 커넥션을 획득할 때 마다URL
,USERNAME
,PASSWORD
같은 파라미터를 계속 전달해야 하는 반면에DataSource
를 사용하는 방식은 처음 객체를 생성할 때만 필요한 파라미터를 넘겨두고, 커넥션을 획득할 때는dataSource.getConnection()
만 호출하면 된다.
설정과 사용의 분리
- 설정:
DataSource
를 만들고, 필요한 속성들을 사용해서URL
,USERNAME
,PASSWORD
같은 부분을 입력하는 것을 말한다. 이렇게 설정과 관련된 속성들은 한 곳에 있는 것이 향후 변경에 더 유연하게 대처할 수 있다. -
사용: 설정은 신경쓰지 않고,
DataSource
의getConnection()
만 호출해서 사용하면 된다. - 이 부분이 큰 차이를 만들어내는데, 필요한 데이터를
DataSource
가 만들어지는 시점에 미리 다 넣어두게 되면,DataSource
를 사용하는 곳에서는dataSource.getConnection()
만 호출하면 되므로,URL
,USERNAME
,PASSWORD
같은 속성들에 의존하지 않아도 된다. - 쉽게 얘기해서 Repository는
DataSource
만 의존하고, 이런 속성을 몰라도 된다. - 애플리케이션 개발은 보통 설정은 한 곳에서 하지만, 사용은 수 많은 곳에서 한다.
- 덕분에 객체를 설정하는 부분과 사용하는 부분을 더 명확하게 분리할 수 있다.
DataSource 예제2 - 커넥션 풀
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);
}
- HikariCP 커넥션 풀을 사용한다.
HikariDataSource
는DataSource
인터페이스를 구현하고 있다. - 커넥션 풀 최대 사이즈를 10으로 지정하고, 풀의 이름을
MyPool
이라고 지정했다. - 커넥션 풀에서 커넥션을 생성하는 작업은 애플리케이션 실행 속도에 영향을 주지 않기 위해 별도의 쓰레드에서 작동한다.
실행 결과(로그 수정)
#커넥션 풀 초기화 정보 출력
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/~/jdbc user=SA, class=class
com.zaxxer.hikari.pool.HikariProxyConnection
#커넥션 풀에서 커넥션 획득2
ConnectionTest - connection=HikariProxyConnection@832292933 wrapping conn1:
url=jdbc:h2:tcp://localhost/~/jdbc user=SA, class=class
com.zaxxer.hikari.pool.HikariProxyConnection
MyPool - After adding stats (total=10, active=2, idle=8, waiting=0)
HikariConfig
풀의 이름(MyPool
)과 최대 풀 수(10
)을 확인할 수 있다.
MyPool Connection adder
별도의 쓰레드를 사용해서 커넥션 풀에 커넥션을 채우고 있는 것을 확인할 수 있으며, 이 쓰레드는 커넥션 풀에 커넥션을 최대 풀 수(10
)까지 채운다.
커넥션 풀에서 커넥션 획득
커넥션 풀에서 커넥션을 획득하고 그 결과를 출력. 여기서는 커넥션을 2개 획득하고 반환하지는 않았기 때문에, 풀에 있는 10개의 커넥션 중에 2개를 가지고 있는 상태이다.
DataSource 적용
애플리케이션에 DataSource
적용
MemberRepositoryV1
/**
* 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
를 주입 받아서 사용한다. 이제 직접 만든DBConnectionUtil
을 사용하지 않아도 된다. DataSource
는 표준 인터페이스 이기 때문에DriverManagerDataSource
에서HikariDataSource
로 변경되어도 해당 코드를 변경하지 않아도 된다.
- 외부에서
JdbcUtils
편의 메서드- 스프링은 JDBC를 편리하게 다룰 수 있는
JdbcUtils
라는 편의 메서드를 제공한다. JdbcUtils
를 사용하면 커넥션을 좀 더 편리하게 닫을 수 있다.
- 스프링은 JDBC를 편리하게 다룰 수 있는
MemberRepositoryV1Test
@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 {
// save
// findById
// update : money = 10000 -> 20000
// delete
}
}
DriverManagerDataSource 사용
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
를 사용하면conn0~5
번호를 통해서 항상 새로운 커넥션이 생성되어서 사용되는 것을 확인할 수 있다.
HikariDataSource 사용
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
DriverManagerDataSource
-> HikariDataSource
로 변경해도 MemberRepositoryV1
의 코드는 수정하지 않아도 된다. 그 이유는 MemberRepositoryV1
은 DataSource
인터페이스만 의존하기 때문이다.
이것이 DataSource
를 사용할때의 장점이다.(DI + OCP)
댓글남기기