커넥션 풀 이해
데이터베이스 커넥션 획득 과정
- 애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회한다.
- DB 드라이버는 DB와 TCP/IP 커넥션을 연결한다. (3 way handshake 등)
- DB 드라이버는 TCP/IP 커넥션이 연결되면 ID, PW등 필요한 부가 정보를 DB에 전달한다.
- DB는 ID,PW를 통해 내부 인증을 완료하고, 내부에 DB 세션을 생성한다.
- DB는 커넥션 생성이 완료되었다는 응답을 받는다.
- DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환한다.
이렇게 커넥션을 새로 만드는 것은 과정도 복잡하고 시간이 많이 소모되는 작업이다. 이런 문제를 해결하는 아이디어가 바로 커넥션을 미리 생성해두고 사용하는 커넥션 풀 방법이다.
- 애플리케이션 시작 시점에 커넥션 풀은 필요한 만큼 미리 커넥션을 확보해서 풀에 보관한다. 보관하는 갯수는 서비스의 특징과 서버 스펙에 따라 다르지만 보통 기본값은 10개이다.
- 커넥션 풀에 들어있는 커넥션은 TCP/IP로 DB와 커넥션이 연결되어 있는 상태이기 때문에 언제든지 즉시 SQL을 DB에 전달할 수 있다.
- 다음 그림과 같이 커넥션 풀에서 이미 생성되어 있는 커넥션을 객체 참조로 가져다 쓴다. 커넥션을 모두 사용하고 나면 커넥션을 종료하는 것이 아닌, 다음에 다시 사용할 수 있도록 커넥션 풀에 다시 반환한다.
커넥션 풀로 얻는 이점이 매우 크기 때문에 실무에서 기본으로 사용한다. 대표적인 커넥션 풀 오픈소스는 commons-dbcp2, tomcat-jdbc pool, HikariCP 등이 있다. 스프링 부트에선 HikariCP를 기본으로 제공한다.
DataSource 이해
- DriverManager로 커넥션을 획득하다가 커넥션 풀로 변환할 때 의존관계 또는 애플리케이션 코드를 변경해야 한다. 커넥션을 획들하는 방법을 공통 추상화하여 제공하는 것이 DataSource이다.
DataSource 예제 - DriverManager
ConnectionTest - 드라이버 매니저
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());
}
}
- 커넥션 호출을 2번 했을 때 모두 정상적으로 반환되는 것을 확인할 수 있다.
ConnectionTest - 데이터소스 드라이버 매니저 추가
@Test
void dataSourceDriverManager() throws SQLException {
// DriverManagerDataSource - 항상 새로운 커넥션 획득
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
userDataSource(dataSource);
}
private void userDataSource(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
는DataSource
를 통해서 커넥션을 획득할 수 있다.
드라이버 매니저와, 데이터 소스 매니저와 비슷해보이지만 다르다. 그것은 바로 설정과 사용의 분리이다.
설정 : DataSource를 만들고 필요한 속성들을 사용해서 URL, USERNAME 등을 입력하는 것이다.
사용 : 설정을 신경쓰지 않고
DataSource
의 getConnection()만 호출해서 사용하면 된다.즉, 리포지토리는
DataSource
만 의존하고, 다른 속성을 몰라도 된다.
DataSource 예제2 - 커넥션 풀
ConnectionTest - 데이터소스 커넥션 풀 추가
@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");
userDataSource(dataSource);
Thread.sleep(1000); // 커넥션 풀에서 커넥션 생성 대기 시간
}
- 스프링 부트에서 제공하는 HikariCP 커넥션풀을 사용한다. 생성 대기 시간을 주어야 커넥션이 생성되는 로그를 볼 수 있다.
실행 결과
- 다음과 같이 커넥션 풀 로그 생성을 볼 수 있다.
connection adder
를 통해 애플리케이션 실행과 별개로 별도의 쓰레드를 사용해서 커넥션 풀을 채운다. - 최대 설정한 풀의 갯수보다 많은 커넥션을 가져오면, 빌 때까지 대기하다가 예외를 반환한다.
DataSource 적용
MemberRepositoryV1
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;
@Slf4j
public class MemberRepositoryV1 {
private final DataSource dataSource;
public MemberRepositoryV1(DataSource dataSource) {
this.dataSource = dataSource;
}
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeConnection(con);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeResultSet(rs);
}
private Connection getConnection() throws SQLException {
Connection con = dataSource.getConnection();
log.info("get connection={}, class={}", con, con.getClass());
return con;
}
/*
CRUD는 V0와 동일 (생략)
*/
}
DataSource
의존관계 주입 - 표준 인터페이스이기 때문에 커넥션 획득 방식이 변경되어도 코드를 변경하지 않아도 된다.JdbcUtils
: 스프링은 JDBC를 편리하게 다룰 수 있는JdbcUtils
라는 편의 메서드를 제공한다.
MemberRepositoryV1Test
@Slf4j
class MemberRepositoryV1Test {
MemberRepositoryV1 repository;
@BeforeEach
void beforeEach() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
repository = new MemberRepositoryV1(dataSource);
}
/*
CRUD는 VO와 동일 (생략)
*/
}
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
- 다음과 같이 커넥션 풀을 사용할 경우 커넥션이 재사용되는 것을 볼 수 있다.
DriverManagerDataSource, HikariDataSource 등 구현체를 변경해도 MemberRepositoryV1의 코드는 전혀 변경하지 않아도 된다. 그 이유는 MemberRepositoryV1은 DataSource 인터페이스에만 의존하기 때문이다. (DI + OCP)
본 포스팅은 인프런 - 김영한님의 '스프링 DB 1편 - 데이터 접근 핵심 원리'를 정리한 내용입니다.