본문 바로가기
BackEnd/Spring

[SpringDB1] 커넥션 풀과 데이터소스

by 개발 Blog 2026. 6. 21.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

 

커넥션을 매번 만들 때의 부담

데이터베이스 커넥션 하나를 새로 만드는 과정은 다음과 같다.

① 앱이 DB 드라이버로 커넥션 조회
② 드라이버가 DB와 TCP/IP 연결 (3-way handshake 등 네트워크 동작)
③ ID/PW와 부가정보를 DB에 전달
④ DB가 인증을 거쳐 내부에 DB 세션 생성
⑤ DB가 커넥션 생성 완료 응답
⑥ 드라이버가 커넥션 객체를 만들어 앱에 반환

이 과정은 복잡하고 시간이 든다. 더 큰 문제는 SQL 실행 시간뿐 아니라 커넥션을 새로 만드는 시간이 매 요청마다 추가되어 응답 속도에 영향을 준다는 점이다. 데이터베이스마다 다르지만 MySQL 계열은 수 ms, 더 오래 걸리는 데이터베이스도 있다.

커넥션 풀

이 문제를 해결하는 방법이 커넥션 풀이다. 커넥션을 미리 만들어 풀에 보관해 두고 빌려 쓴다.

앱 ──① 빌림──→ ┌──────── 커넥션 풀 (기본 10개) ────────┐
   ←─② 반납──  │ conn0  conn1  conn2  ...  conn9   │
              │ 모두 이미 DB와 TCP 연결된 상태          │
              └──────────────────┬────────────────┘
                                 │ 연결 유지
                                 ▼
                                DB
  • 애플리케이션을 시작하는 시점에 커넥션을 필요한 만큼(기본 10개) 미리 확보해 풀에 보관한다.
  • 풀 안의 커넥션은 이미 DB와 TCP/IP로 연결된 상태라 언제든 즉시 SQL을 전달할 수 있다.
  • 애플리케이션은 드라이버로 새 커넥션을 만드는 게 아니라, 풀에 있는 커넥션을 객체 참조로 가져다 쓴다.
  • 다 쓴 커넥션은 종료하는 게 아니라 살아있는 상태로 풀에 반환한다.

적절한 풀 크기는 서비스 특징과 서버, DB 스펙에 따라 다르므로 성능 테스트로 정한다. 풀은 서버당 최대 커넥션 수를 제한할 수 있어 DB에 무한정 연결이 생성되는 것을 막아 DB를 보호하는 효과도 있다.

 

직접 구현할 수도 있지만 성능 좋은 오픈소스가 많으므로 오픈소스를 쓴다. 대표적으로 commons-dbcp2, tomcat-jdbc, HikariCP가 있고, 최근에는 HikariCP를 주로 쓴다. 스프링 부트 2.0부터 기본 커넥션 풀로 HikariCP를 제공한다.

DataSource

커넥션을 얻는 방법은 DriverManager 직접 사용, DBCP2 풀, HikariCP 풀 등 다양하다. 문제는 DriverManager로 커넥션을 얻다가 HikariCP 같은 커넥션 풀로 바꾸면 애플리케이션 코드도 함께 변경해야 한다는 점이다. 의존 관계가 바뀌고 사용법도 다르기 때문이다.

앱 로직 ──✕ 코드 변경 필요──→ DriverManager  →  HikariCP

자바는 이 문제를 해결하기 위해 javax.sql.DataSource 인터페이스를 제공한다. 커넥션을 획득하는 방법을 추상화한 인터페이스이고, 핵심 기능은 커넥션 조회 하나다.

public interface DataSource {
    Connection getConnection() throws SQLException;
}
                  앱 로직 (Repository)
                       │ getConnection() 호출
                       ▼
                  DataSource (인터페이스)
                   ▲      ▲      ▲
            구현    │      │      │   구현
      ┌────────────┘      │      └────────────┐
DriverManager      HikariDataSource         DBCP2
DataSource           (커넥션 풀)

대부분의 커넥션 풀은 DataSource를 이미 구현해 두었다. 따라서 애플리케이션 로직은 특정 풀 구현체가 아니라 DataSource 인터페이스에만 의존하면 되고, 풀 기술을 바꾸려면 구현체만 갈아끼우면 된다.

 

다만 DriverManager는 DataSource를 구현하지 않으므로, 스프링이 DriverManagerDataSource라는 구현 클래스를 제공해 DataSource처럼 쓸 수 있게 한다.

설정과 사용의 분리

// DriverManager - 호출할 때마다 파라미터 전달
DriverManager.getConnection(URL, USERNAME, PASSWORD);

// DataSource - 생성 시 한 번만 주입, 이후엔 getConnection()만
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
dataSource.getConnection();

DriverManager는 커넥션을 획득할 때마다 URL, USERNAME, PASSWORD 같은 파라미터를 계속 전달해야 한다. 반면 DataSource를 사용하는 방식은 처음 객체를 생성할 때만 필요한 파라미터를 넘겨두고, 커넥션을 획득할 때는 getConnection()만 호출하면 된다.

 

DataSource를 만들 때 URL, USERNAME, PASSWORD 같은 속성을 입력하는 것이 설정이고, 이후 getConnection()만 호출하는 것이 사용이다.

 

설정 관련 속성을 한 곳에 모아두면 변경에 유연하게 대처할 수 있다. 덕분에 리포지토리는 DataSource만 주입받으면 되고 URL이나 비밀번호 같은 속성은 몰라도 된다.

 

설정은 한 곳에서 하지만 사용은 여러 곳에서 하게 되므로, 설정과 사용을 분리하면 코드가 깔끔해진다.

DataSource 예제 - DriverManager

@Test
void driverManager() throws SQLException {
    // 순수 DriverManager - 호출마다 URL/USER/PW 전달
    Connection con1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
    Connection con2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
}

@Test
void dataSourceDriverManager() throws SQLException {
    // 스프링의 DriverManagerDataSource - DataSource로 추상화
    DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
    useDataSource(dataSource);
}

private void useDataSource(DataSource dataSource) throws SQLException {
    Connection con1 = dataSource.getConnection();
    Connection con2 = dataSource.getConnection();
}

DriverManagerDataSource는 스프링이 제공하는 클래스로, DataSource를 통해 커넥션을 획득할 수 있다.

기존 DriverManager와 달리 생성 시점에만 접속 정보를 넘기고 사용할 때는 getConnection()만 호출한다.

DataSource 예제 - 커넥션 풀

@Test
void dataSourceConnectionPool() throws SQLException, InterruptedException {
    HikariDataSource dataSource = new HikariDataSource();  // DataSource 구현체
    dataSource.setJdbcUrl(URL);
    dataSource.setUsername(USERNAME);
    dataSource.setPassword(PASSWORD);
    dataSource.setMaximumPoolSize(10);
    dataSource.setPoolName("MyPool");
    useDataSource(dataSource);
    Thread.sleep(1000);  // 풀 채우는 별도 스레드 로그를 보려고 대기
}

HikariCP 커넥션 풀을 사용한다. HikariDataSource는 DataSource 인터페이스를 구현하고 있다. 커넥션 풀 최대 사이즈를 10으로, 풀 이름을 MyPool로 지정했다.

 

실행 결과를 보면 MyPool connection adder라는 별도 스레드가 풀에 커넥션 10개를 채운다. 커넥션 풀을 채우는 작업은 상대적으로 오래 걸리는데, 애플리케이션 실행 시 이걸 기다리면 시작이 느려지므로 별도 스레드에서 처리해 실행 시간에 영향을 주지 않는다.

 

커넥션 2개를 빌리고 반환하지 않으면 사용 중인 커넥션 active=2, 대기 상태 커넥션 idle=8로 나온다.

DataSource 적용

기존 코드를 유지하기 위해 복사해서 MemberRepositoryV1을 만든다.

@Slf4j
public class MemberRepositoryV1 {

    private final DataSource dataSource;

    public MemberRepositoryV1(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    // save / findById / update / delete 는 V0과 동일

    private void close(Connection con, Statement stmt, ResultSet rs) {
        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(stmt);
        JdbcUtils.closeConnection(con);
    }

    private Connection getConnection() throws SQLException {
        Connection con = dataSource.getConnection();
        log.info("get connection={}, class={}", con, con.getClass());
        return con;
    }
}

외부에서 DataSource를 주입받아 사용한다. 직접 만든 DBConnectionUtil을 더 이상 쓰지 않는다. DataSource는 표준 인터페이스이므로 DriverManagerDataSource에서 HikariDataSource로 변경되어도 이 코드는 변경하지 않아도 된다.

 

또한 스프링이 제공하는 JdbcUtils를 사용하면 커넥션을 편리하게 닫을 수 있다.

@BeforeEach
void beforeEach() {
    // 커넥션 풀링
    HikariDataSource dataSource = new HikariDataSource();
    dataSource.setJdbcUrl(URL);
    dataSource.setUsername(USERNAME);
    dataSource.setPassword(PASSWORD);
    repository = new MemberRepositoryV1(dataSource);
}

테스트에서 DataSource를 주입한다. 실행 결과를 비교하면 DriverManagerDataSource는 conn0, conn1, conn2처럼 항상 새로운 커넥션을 생성하고, HikariDataSource는 conn0을 반복 재사용한다. 테스트는 순차로 실행되어 커넥션을 빌리고 반환하기를 반복하므로 conn0만 사용된다.

 

DriverManagerDataSource를 HikariDataSource로 변경해도 MemberRepositoryV1의 코드는 전혀 변경하지 않아도 된다. MemberRepositoryV1은 DataSource 인터페이스에만 의존하기 때문이다. 이것이 DataSource를 사용하는 장점이다(DI + OCP).

정리

  • 커넥션 생성은 부담이 크므로 커넥션 풀로 미리 만들어 재사용한다. 실무에서는 HikariCP를 기본으로 쓴다.
  • 커넥션을 획득하는 방법을 DataSource로 추상화하면 구현을 교체해도 애플리케이션 코드는 변경하지 않는다.
  • 설정과 사용을 분리하면 리포지토리는 DataSource만 주입받고 접속 정보는 몰라도 된다.