데이터베이스의 커넥션을 획득할때는 아래와 같은 복잡한 과정이 필요하다.
1) 애플리케이션 로직에서 DB 드라이버를 통해 커넥션을 조회한다.
2) DB드라이버는 DB와 TCP/IP 커넥션을 연결한다.(TCP/IP 연결을 위한 네트워크 동작 발생)
3) DB 드라이버는 TCP/IP 커넥션이 연결되면 ID, PW와 기타 부가정보를
DB에 전달한다.
4) DB는 ID, PW를 통해 내부 인증을 완료하고 내부에 DB 세션을 생성한다.
5) DB는 커넥션 생성이 완료되었다는 응답을 보낸다.
6) DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환한다.
따라서, 커넥션을 새롭게 만드는데는 굉장히 많은 리소스가 투입된다.
이러한 문제점들 때문에 커넥션 풀의 개념이 등장했다.
커넥션 풀은 커넥션들을 미리 만들어놓고 필요할때마다 꺼내서 쓰고 다시 반납하는 개념이다.
애플리케이션을 시작하는 시점에 커넥션 풀은 필요한 만큼 커넥션을 미리 확보해서 풀에 보관한다.
커넥션 풀에 들어있는 커넥션들은 TCP/IP로 DB와 커넥션이 연결되어 있는 상태이기 때문에 언제든지 즉시 SQL을 DB에 전달할 수 있다.
애플리케이션 로직에서는 새로운 커넥션을 매번 생성하는게 아니라 커넥션 풀에 이미 생성되어있는 커넥션을 객체 참조로 가져다 쓰기만 하면 된다.
커넥션을 모두 사용하고 나면 이제는 커넥션을 종료하는 것이 아니라, 다음에 다시 사용할 수 있도록 해당 커넥션을 그대로 커넥션 풀에 반환하면 된다.
여기에서 주의해야할점은 커넥션을 종료하는것이 아니라 커넥션이 살아있는 상태로 커넥션 풀에 반환해야하는것이다.
참고)
대표적인 커넥션 풀 오픈소스는 commons-dbcp2, tomcat-jdbc, HikariCP 등이 존재하며, 스프링부트 2.0 부터는 기본 커넥션 풀로 HikariCP를 제공한다.
스프링에서 커넥션을 얻는 방법은 JDBC의 DriverManager를 직접 사용하거나, 커넥션 풀을 사용하는 등 다양한 방법이 존재한다.
JDBC로 개발한 애플리케이션 처럼 DriverManager를 통해서 커넥션을 획득하다가 커넥션 풀을 사용하는 방법으로 변경하기 위해서는 애플리케이션 코드도 함께 변경해야한다. 왜냐하면 DriverManager에서 HikariCP로 의존관계도 변경되고 사용 방법도 다르기 때문이다.
그래서 스프링에서는 커넥션을 획득하는 방법도 추상화했다.
DriverManager와 HikariCP간의 커넥션을 획득하는 방식이 다르기 때문에 자바에서는 javax.sql.DataSource 라는 인터페이스를 제공한다.
DataSource는 커넥션을 획득하는 방법을 추상화하는 인터페이스이다.
DataSource의 핵심 기능은 단순한 커넥션 조회 하나이다.
public interface DataSource {
Connection getConnection() throws SQLException;
}
DriverManager를 그대로 이용한 코드와 DataSource를 활용한 예시 코드를 비교해보면 아래와 같다.
@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());
}
우선 DriverManager를 그대로 사용하던지 DriverManagerDataSource를 사용하던지 커넥션 풀을 사용하는건 아니기 때문에 항상 새로운 커넥션을 생성하는 결과를 얻을 수 있다.
아래의 예시 코드는 HikariCP 커넥션 풀을 이용하는 DataSource를 구현한 커넥션 풀 사용 예시이다.
@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); //커넥션 풀에서 커넥션 생성 시간 대기
}
스프링은 JDBC를 편리하게 다룰 수 있는 JdbcUtils라는 편의 메서드를 제공한다.
JdbcUtils를 사용하면 커넥션을 좀 더 편리하게 닫을 수 있다.
예시 코드)
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
커넥션 풀을 사용하면서 JdbcUtils를 이용해서 리소스를 종료하면 완전한 종료가 아닌 커넥션풀에 반납하는 개념으로 사용된다고 한다. 굳!
아래의 강의를 공부하여 정리한 내용입니다.
김영한님의 SpringDB1-커넥션풀과데이터소스