[Spring] Connection Pool, DataSource
Connection Pool 이란?
이전 포스팅에서는 DriverManager를 통해서 커넥션을 얻어오는 과정을 알아봤다.
이러한 방식은 매번 새로운 커넥션을 생성하기 때문에 커넥션을 맺는 시간, 비용 등등.. 비효율적이라고 할 수 있다.
그래서 고안해낸 방식이 바로 Connection Pool이다.
위 사진처럼 애플리케이션이 시작하는 시점에 커넥션 풀은 필요한 만큼(기본 10개) 커넥션을 미리 확보해서 풀에 보관한다.
커넥션 풀에 들어 있는 커넥션은 TCP/IP로 DB와 커넥션이 연결외어 있는 상태이므로 언제든지 즉시 SQL을 DB에 전달할 수 있다.
애플리케이션은 이제 DriverManager를 통해서 매번 커넥션을 획득하지 않고, 커넥션 풀에 미리 생성되어 있는 커넥션을 가져다 쓰기만 하면 된다.
사용이 끝나면 다시 커넥션을 풀에 반환한다. 단순하게 커넥션을 close() 하는게 아니라 살아있는 상태 그대로 풀에 반환하는 것임을 명심하자.
이렇게 커넥션 풀을 사용하면 좋은 점이 또 뭐가있을까?
커넥션 풀은 서버당 최대 커넥션 수를 제한할 수 있다. DriverManager를 사용한다면 요청이 들어오는대로 커넥션을 생성하기 때문에 DB에 부담이 많이갈 수 있는데, 이걸 최대 커넥션 수를 제한하고 커넥션 풀에서 꺼내쓰게 하므로 과부하를 방지할 수 있는 것이다.
이러한 커넥션 풀은 직접 Set이나 List 형식으로 만들 수 있겠지만, 사용도 편리하고 성능도 뛰어난 오픈소스 커넥션 풀이 많다.
commons-dbcp2, tomcat-jdbc pool, HikariCP 등이 있지만 제일 많이 사용하는 것은 HikariCP이다.
스프링부트 2.0부터는 기본 커넥션 풀로 HikariCP를 사용하고 있다. 그만큼 성능, 안전성 측면에서 검증이 되었다는 말이 아닐까?
DataSource, 커넥션 획득의 추상화
자, 그럼 우리는 지금까지 커넥션을 획득하는 방법에 대해서 2가지를 배웠다.
- DriverManager를 통해 획득하는 방법
- Connection Pool을 통해 획득하는 방법
도식화 해보면 다음과 같다.
근데 조금 문제가 있다. DriverManager를 통해 신규 커넥션을 얻는 방식을 사용하다가 HikariCP 커넥션 풀을 사용하는 방식으로 변경하려면, 애플리케이션 로직에서 커넥션을 획득하는 부분을 고쳐야한다. -> 추상화가 안되어있다.
자바에서는 커넥션을 획득하는 방법을 추상화하기 위해 DataSource라는 인터페이스를 제공하기 시작했다.
이 DataSource 인터페이스는 단순히 getConnection() 기능만 존재한다.
DataSource는 인터페이스이기 때문에, 커넥션 풀을 사용하고 싶다면 구현체로 HikariCP or DBCP2 등을 선택하면 된다.
참고로, 기존의 DriverManager은 DataSource랑 전혀 관계가 없는 기술이다.
그렇기 때문에 DataSource로 커넥션 풀을 사용하다가 DriverManager을 통해 커넥션을 직접 획득하는 방식으로 바꾸고 싶으면 애플리케이션 로직을 아예 바꿔줘야 한다.
이는 굉장히 번거롭기 때문에 스프링에서는 DriverManager를 상속받고, 동시에 DataSource를 통해 추상화가 가능하도록 DriverManagerDataSource라는 것을 만들었다 .
마지막으로 DataSource의 장점을 하나 짚으면서 마무리하겠다.
기존 DriverManager를 사용해 커넥션을 획득하는 부분을 잠깐 살펴보자.
public static Connection getConnection() {
try {
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("get connection={}, class={}", connection, connection.getClass());
return connection;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
커넥션을 획득할 때 마다 연결 정보들 (접속 URL, 유저 아이디, 패스워드) 를 입력해줘야 한다.
반면에 DataSource를 사용하는 방식을 보자.
@Test
void dataSourceConnectionPool() throws SQLException, InterruptedException {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
dataSource.setMaximumPoolSize(10);
dataSource.setPoolName("MyPool");
useDataSource(dataSource);
Thread.sleep(1000);
}
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());
}
dataSource를 생성하고, dataSource 자체에 필요한 정보들을 한번에 기록해둔다.
그 뒤, 커넥션이 필요하면 dataSource.getConnection()으로 커넥션을 꺼내오기만 한다. 불필요한 부가정보를 다시 입력하지 않는다.
사소한 차이일 수 있지만 이게 의미하는게 뭘까?
설정과 사용이 분리됐다는 것을 의미한다.
만약 이렇게 설정과 사용이 분리되지 않으면, 설정이 바뀌면 모든 사용된 코드를 전부 변경해야한다.
실제 애플리케이션에는 위와 같이 커넥션을 사용해 데이터를 처리하는 리포지토리가 존재할텐데,
리포지토리는 단순히 외부에서 주입되는 DataSource에만 의존하면 되고 이러한 연결 정보는 몰라도 된다.
JdbcUtils
JdbcUtils는 스프링에서 JDBC를 다루기 편한, 다양한 편의 메서드를 제공한다.
현재 직접 만든 close()라는 메서드를 단 3줄로 줄일 수 있다.
기존 코드
private void close(Connection con, Statement stmt, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
log.info("error");
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
log.info("error");
}
}
if (con != null) {
try {
con.close();
} catch (SQLException e) {
log.info("error");
}
}
}
JdbcUtils 사용
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
내부적으로는 어떻게 구성되어있을까? 하나만 살펴보자.
직접 짰던 코드와 거의 유사하지만 조금 더 치밀한 예외처리가 되어있는 모습이다.
출처 : 인프런, 김영한의 스프링 DB 1편 - 데이터 접근 핵심 원리