데이터베이스 커넥션을 매번 획득
문제
고객이 애플리케이션을 사용할 때, SQL을 실행하는 시간 뿐만 아니라 커넥션을 새로 만드는 시간이 추가되기 때문에 결과적으로 응답 속도에 영향을 준다. 이것은 사용자에게 좋지 않은 경험을 줄 수 있다.
이런 문제를 한번에 해결하는 아이디어가 바로 커넥션을 미리 생성해두고 사용하는 커넥션 풀이라는 방법이다.
커넥션 풀 사용
이점
commons-dbcp2
, tomcat-jdbc pool
, HikariCP
등이 있다.커넥션을 얻는 방법은 앞서 학습한 JDBC DriverManager
를 직접 사용하거나, 커넥션 풀을 사용하는 등 다양한 방법이 존재한다.
커넥션을 획득하는 다양한 방법
DriverManager
를 통해 커넥션 획득하다가 커넥션 풀로 변경 시 애플리케이션 코드도 함께 변경해야 한다.커넥션을 획득하는 방법을 추상화
javax.sql.DataSource
라는 인터페이스를 제공한다.DataSource
는 커넥션을 획득하는 방법을 추상화 하는 인터페이스이다.먼저 기존에 개발했던 DriverManager
를 통해서 커넥션을 획득하는 방법을 확인해보자.
ConnectionTest
package com.example.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 com.example.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());
}
}
ConnectionTest - 데이터소스 드라이버 매니저 추가
@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
는 커넥션을 획득할 때 마다 URL, USERNAME, PASSWORD 같은 파라미터를 계속 전달해야 한다. 반면에 DataSource
를 사용하는 방식은 처음 객체를 생성할 때만 필요한 파리미터를 넘겨두고, 커넥션을 획득할 때는 단순히 dataSource.getConnection()
만 호출하면 된다.설정과 사용의 분리
DataSource
를 만들고 필요한 속성들을 사용해서 URL , USERNAME , PASSWORD 같은 부분을 입력하는 것을 말한다. 이렇게 설정과 관련된 속성들은 한 곳에 있는 것이 향후 변경에 더 유연하게 대처할 수 있다.DataSource
의 getConnection()
만 호출해서 사용하면 된다.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");
useDataSource(dataSource);
Thread.sleep(1000); // 커넥션 풀에서 커넥션 생성 시간 대기
}
HikariConfig
- HikariCP
관련 설정을 확인할 수 있다. 풀의 이름(MyPool
)과 최대 풀 수(10)을 확인할 수 있다.
MyPool connection adder
이번에는 애플리케이션에 DataSource
를 적용해보자.
기존 코드를 복사해서 새로 만들었다. V0 -> V1
MemberRepositoryV1
private final DataSource dataSource;
private Connection getConnection() throws SQLException {
Connection con = dataSource.getConnection();
log.info("get connection={}, class={}", con, con.getClass());
return con;
}
// 변경
private void close(Connection con, Statement stmt, ResultSet rs) {
// 간편
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
// 나머지 동일
MemberRepositoryV1Test (DriverManager - 항상 새로운 커넥션 획득)
package com.example.jdbc.repository;
import com.example.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
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 com.example.jdbc.connection.ConnectionConst.*;
@Slf4j
public class MemberRepositoryV1Test {
MemberRepositoryV1 repository;
@BeforeEach
void beforeEach(){
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
repository = new MemberRepositoryV1(dataSource);
}
@Test
void crud() throws SQLException {
//save
Member member = new Member("memberV0", 10000);
repository.save(member);
//findById
Member findMember = repository.findById(member.getMemberId());
log.info("findMember={}", findMember);
Assertions.assertThat(findMember).isEqualTo(member);
//update: money: 10000 -> 20000
repository.update(member.getMemberId(), 20000);
Member updatedMember = repository.findById(member.getMemberId());
Assertions.assertThat(updatedMember.getMoney()).isEqualTo(20000);
//delete
repository.delete(member.getMemberId());
Assertions.assertThatThrownBy(() -> repository.findById(member.getMemberId())).isInstanceOf(NoSuchElementException.class);
}
}
DriverManagerDataSource
를 사용하면 conn0~5 번호를 통해서 항상 새로운 커넥션이 생성되어서 사용되는 것을 확인할 수 있다. (스크린 샷에서는 conn0 - conn1)MemberRepositoryV1Test(HikariDataSource 커넥션 풀링)
@BeforeEach
void beforeEach(){
// DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
repository = new MemberRepositoryV1(dataSource);
}
DriverManagerDataSource
를HikariDataSource
로 변경해도MemberRepositoryV1
의 코드는 전혀 변경하지 않아도 된다.MemberRepositoryV1
는DataSource
인터페이스에만 의존하기 때문이다.
참고
김영한: 스프링 DB 1편 - 데이터 접근 핵심 원리(인프런)
Github - https://github.com/b2b2004/Spring_DB