3. 커넥션풀과 데이터소스
2025. 5. 19. 16:38ㆍSpring/DB접근
[목차]
1. JDBC Driver를 통한 Connection 생성 과정
2. Connection 획득 방식 비교
1) DriverManager 방식
2) 커넥션 풀 방식
3) 두 방식 비교
3. 커넥션 풀 사용 과정: 풀 초기화 → 커넥션 획득 → 반납
1) 커넥션 풀 초기화
2) 커넥션 풀에서 커넥션 획득
3) 커넥션 사용 후 커넥션 풀에 반환
4. 커넥션 풀 오픈소스: hikariCP
5. DataSource
1) 커넥션을 획득하는 다양한 방법
2) DataSource 인터페이스
3) DataSource 예제
6. DataSource 활용하여 기존 애플리케이션 수정
1. JDBC Driver를 통한 Connection 생성 과정
- 애플리케이션 서버와 데이터베이스가 통신하기 위해서는 JDBC의 Connection의 구현 객체 필요하다.
- Connection 객체는 JDBC Driver가 connect()를 통해 직접 생성하여 반환해준다.
- JDBC Driver의 데이터베이스 Connection 생성 과정은 아래와 같다
1. 드라이버가 DB와 TCP/IP로 연결 (3-way handshake 발생)
→ 2. ID, PW 등 인증 정보를 DB에 전달
→ 3. DB가 내부 인증을 완료하고, DB 세션을 생성
→ 4. 커넥션 생성이 완료되었다는 응답을 보냄
→ 5. 드라이버는 Connection 객체를 생성해서 클라이언트(애플리케이션)에 반환
- 애플리케이션 서버와 데이터베이스 간의 통신을 통해 커넥션을 새로 만드는 과정은
네트워크 통신 + 인증 + 세션 생성까지 포함되므로 비용이 크고 시간이 많이 걸림
2. Connection 획득 방식 비교
1) DriverManager 방식
- 기존 JDBC에서는 DriverManager를 통해 Connection 객체를 획득했다.
- DriverManager는 getConnection(..) 요청을 받을 때마다
데이터베이스 커넥션을 만들어 새로운 Connection 객체를 반환한다. - 이 방식은 위의 무거운 커넥션 생성 과정을 요청마다 반복하기 때문에, 리소스 낭비 + 성능 저하로 이어진다.
2) 커넥션 풀 방식
- 이러한 비효율을 해결하기 위해 커넥션 풀(Connection Pool)이 등장했다.
- 애플리케이션 실행 시점에 미리 일정 수의 DB 커넥션을 생성해 풀(pool)에 저장해 둔다.
- → 이후 DB 연결이 필요한 상황이 오면, 풀에 있는 커넥션을 꺼내서 재사용한다.
- → 사용이 끝나면 반납하여 다시 풀에 넣는다.
- 매번 커넥션을 새로 생성하는 것이 아니라 커넥션 풀에 이미 생성되어 있는 커넥션을 객체 참조로 가져다 쓰기만 하면 된다.
따라서 성능이 향상되고, DB 자원을 효율적으로 관리할 수 있게 된다.
3) 두 방식 비교
항목 | DriverManager 방식 | 커넥션 풀 방식 |
커넥션 생성 시점 | 요청마다 새로 생성 | 앱 시작 시 미리 생성 후 재사용 |
내부 흐름 | 매번 JDBC 드라이버의 connect() 호출 | 최초 한 번만 connect() 호출 |
성능 | 느림 (매번 네트워크 + 세션 생성) | 빠름 (객체 재사용) |
리소스 사용 | 비효율적, 낭비 많음 | 효율적, 풀에서 관리 |
사용 방법 | DriverManager.getConnection() | DataSource.getConnection() |
3. 커넥션 풀 사용 과정: 풀 초기화 → 커넥션 획득 → 반납
1) 커넥션 풀 초기화
- 애플리케이션을 시작하는 시점에 커넥션 풀은 필요한 만큼 커넥션을 미리 확보해서 풀에 보관한다.
- 적절한 커넥션 풀 숫자는 서비스의 특징과 애플리케이션 서버 스펙, DB 서버 스펙에 따라 다르기 때문에 성능 테 스트를 통해서 정해야 한다. 기본값은 보통 10개이다.
- 커넥션 풀은 서버당 최대 커넥션 수를 제한할 수 있다.
따라서 DB에 무한정 연결이 생성되는 것을 막아주어서 DB를 보호하는 효과도 있다. - 커넥션 풀에 들어 있는 커넥션은 TCP/IP로 DB와 커넥션이 연결되어 있는 상태이기 때문에
언제든지 즉시 SQL을 DB에 전달할 수 있다.

2) 커넥션 풀에서 커넥션 획득
- 애플리케이션 로직에서 DB 드라이버를 통해서 새로운 커넥션을 획득하는 것이 아니라,
이제는 커넥션 풀을 통해 이미 생성되어 있는 커넥션을 객체 참조로 그냥 가져다 쓰기만 하면 된다. - 커넥션 풀에 커넥션을 요청하면 커넥션 풀은 자신이 가지고 있는 커넥션 중에 하나를 반환한다.
3) 커넥션 사용 후 커넥션 풀에 반환
- 애플리케이션 로직은 커넥션 풀에서 받은 커넥션을 사용해서 SQL을 데이터베이스에 전달하고 그 결과를 받아서 처리한다.
- 커넥션을 모두 사용하고 나면 이제는 커넥션을 종료하는 것이 아니라,
다음에 다시 사용할 수 있도록 해당 커넥션 을 그대로 커넥션 풀에 반환하면 된다. - 여기서 주의할 점은 커넥션을 종료하는 것이 아니라 커넥션이 살아있는 상태로 커넥션 풀에 반환해야 한다는 것이다.
4. 커넥션 풀 오픈소스: hikariCP
- 커넥션 풀은 얻는 이점이 매우 크기 때문에 실무에서는 항상 기본으로 사용한다.
- 커넥션 풀은 개념적으로 단순해서 직접 구현할 수도 있지만,
사용도 편리하고 성능도 뛰어난 오픈소스 커넥션 풀이 많기 때문에 오픈소스를 사용하는 것이 좋다. - 대표적인 커넥션 풀 오픈소스는 commons-dbcp2, tomcat-jdbc pool, HikariCP등이 있다.
- 성능과 사용의 편리함 측면에서 최근에는 hikariCP를 주로 사용한다.
- 스프링 부트 2.0 부터는 기본 커넥션 풀 로 hikariCP를 제공한다.
- 실무에서도 레거시 프로젝트가 아닌 이상 대부분 hikariCP를 사용한다.
- 성능, 사용의 편리함, 안전성 측면에서 이미 검증이 되었기 때문에 커넥션 풀을 사용 할 때는 그냥 hikariCP를 사용하면 된다.
5. DataSource
1) 커넥션을 획득하는 다양한 방법
- 커넥션을 획득하는 방법은 DriverManager 사용, 커넥션 풀 사용 등 다양하게 존재한다.
- 그런데 DriverManager를 통해서 커넥션을 획득하다가, 커넥션 풀을 사용하는 방법으로 변경하려면 어떻게 해야할까?
- 만약 애플리케이션 로직에서 DriverManager를 사용해서 커넥션을 획득하다가 HikariCP같은 커넥션 풀을 사용하도록 변경하면
커넥션을 획득하는 애플리케이션 코드도 함께 변경해야 한다. 의존관계가 DriverManager에서 HikariCP로 변경되기 때문이다. - 따라서 커넥션 획득 방법을 변경할 때 어플리케이션 코드를 변경해야하는 OCP 위반 문제를 해결하기 위해
자바에서는 javax.sql.DataSource라는 인터페이스를 제공한다.
2) DataSource 인터페이스
public interface DataSource {
Connection getConnection() throws SQLException;
}
- DataSource 는 커넥션을 획득하는 방법을 추상화하는 인터페이스이다.
이 인터페이스의 핵심 기능은 커넥션 조회 하나이다. (다른 일부 기능도 있지만 크게 중요하지 않다.) - 대부분의 커넥션 풀은 DataSource 인터페이스를 이미 구현해두었다.
따라서 개발자는 DBCP2 커넥션 풀, HikariCP 커넥션 풀의 코드를 직접 의존하는 것이 아니라
DataSource 인터페이스에만 의존하도록 애플리케이션 로직을 작성하면 된다. - DriverManager 는 DataSource 인터페이스를 사용하지 않는다.
단, 스프링은 DriverManager도 DataSource를 통해서 사용할 수 있도록
DriverManagerDataSource 라는 DataSource를 구현한 클래스를 제공한다. - 정리하면 자바는 OCP 위반 문제를 해결하기 위해 DataSource 를 통해 커넥션을 획득하는 방법을 추상화했다.
이제 애플리케이션 로직은 DataSource 인터페이스에만 의존하면 된다.
덕분에 DriverManagerDataSource 를 통해서 DriverManager 를 사용 하다가 커넥션 풀을 사용하도록 코드를 변경해도
애플리케이션 로직은 변경하지 않아도 된다.
3) DataSource 예제
@Slf4j
public class ConnectionTest {
//1. 기존의 DriverManager로 커넥션 획득
//사용과 설정이 한 곳에 있음
@Test
void useDriverManager() 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. DataSource 인터페이스로 감싸서 커넥션 획득
//설정과 사용의 분리가 명확하기 때문에 사용 메서드의 코드를 변경할 필요 없음
//2-1. 사용 메서드
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());
}
//2-2. 설정 메서드
@Test //DriverManagerDataSource - 항상 새로운 커넥션을 획득
void dataSourceDriverManager() throws SQLException {
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
useDataSource(dataSource);
}
@Test //HikariDataSource - 커넥션 풀링
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);
}
}
- DriverManager의 한계
- DriverManager는 구체 클래스이기 때문에, 이를 사용하는 코드는 구현체에 직접 의존하게 된다.
→ 즉, 커넥션 생성 방식을 변경하려면 코드 자체를 수정해야 한다. - 또한 커넥션을 얻을 때마다 URL, USERNAME, PASSWORD 등 접속 정보를 직접 전달해야 한다.
→ 사용자 코드가 설정 정보에 강하게 결합되어 있다.
- DriverManager는 구체 클래스이기 때문에, 이를 사용하는 코드는 구현체에 직접 의존하게 된다.
- DataSource 방식의 장점
- DataSource는 인터페이스이기 때문에
사용 코드가 구현체에 직접 의존하는 것이 아니라 DataSource 인터페이스에만 의존하면 된다. - 또한 설정(접속 정보 등록)과 사용(커넥션 획득)을 명확히 분리할 수 있다.
접속 정보는 객체 생성 시 한 번만 설정하면 되고, 사용자는 단순히 dataSource.getConnection()만 호출하면 된다
- DataSource는 인터페이스이기 때문에
- DI + OCP 원칙
DriverManager 방식 | DataSource 방식 | |
DI 원칙 (의존성 주입) |
DriverManager 구현체에 직접 의존 + 설정 정보(URL, ID, PW)에 직접 의존 → 설정과 사용이 섞여 있음 |
사용 코드는 DataSource 인터페이스에만 의존 DataSource와 설정 정보는 외부에서 주입받음 → 설정은 한 곳에서, 사용할 땐 주입받아서 사용만 함 (사용시에는 아무것도 몰라도 됨, 정확한 DI 구조) |
OCP 원칙 (개방-폐쇄원칙) |
URL이나 계정, 커넥션 획득 방식 변경 시 → 기존 코드를 열어서 수정해야 함 (OCP 위반) |
useDataSource()는 그대로 두고 설정에서 DataSource 구현체만 변경 가능 → 확장(설정 변경)에 열려 있고, 기존 코드 변경은 없음 |
6. DataSource 활용하여 기존 애플리케이션 수정
@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 connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
Connection con = dataSource.getConnection();
log.info("get connection={}, class={}", con, con.getClass());
return con;
}
}
- DataSource 의존관계 주입 (DI)
- 외부 설정에서 DataSource 를 주입 받아서 사용한다.
- 따라서 이제 직접 만든 DBConnectionUtil 을 사용하지 않아도 된다.
- DataSource는 표준 인터페이스이기 때문에
DriverManagerDataSource에서 HikariDataSource로 변경되어도 해당 코드를 변경하지 않아도 된다.
- JdbcUtils 편의 메서드
- 스프링은 JDBC를 편리하게 다룰 수 있는 JdbcUtils라는 편의 메서드를 제공한다.
- JdbcUtils 을 사용하면 커넥션을 좀 더 편리하게 닫을 수 있다.
@Slf4j
class MemberRepositoryV1Test {
MemberRepositoryV1 repository;
//외부에서 DataSource 의존관계 주입
@BeforeEach
void beforeEach() throws Exception {
//1. 기본 DriverManager: 항상 새로운 커넥션 획득
//DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
//2. 커넥션 풀링: 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 {
log.info("start");
//save
Member member = new Member("memberV0", 10000);
repository.save(member);
//findById
Member memberById = repository.findById(member.getMemberId());
assertThat(memberById).isNotNull();
//update: money: 10000 -> 20000
repository.update(member.getMemberId(), 20000);
Member updatedMember = repository.findById(member.getMemberId());
assertThat(updatedMember.getMoney()).isEqualTo(20000);
//delete
repository.delete(member.getMemberId());
assertThatThrownBy(() -> repository.findById(member.getMemberId()))
.isInstanceOf(NoSuchElementException.class);
}
}
- MemberRepositoryV1은 외부 설정 단계에서 DataSource 의존관계 주입이 필요하다.
- DriverManagerDataSource에서 → HikariDataSource로 커넥션 획득 방식을 변경해도
- MemberRepositoryV1 의 코드는 전혀 변경하지 않아도 된다.
- MemberRepositoryV1 는 DataSource 인터페이스에만 의존하기 때문이다.
- 이것이 DataSource 를 사용하는 장점이다.