2. 커넥션 풀과 데이터 소스 이해

ChoiJaewoo·2026년 2월 23일

Spring DB 1편

목록 보기
2/6
  • 커넥션 풀 이해
  • DataSource 이해
  • DataSource 예제 1 - DriverManger
  • DataSource 예제 2 - 커넥션 풀
  • DataSource 적용

커넥션 풀 이해

데이터베이스 커넥션을 획득할 때는 다음과 같은 복잡한 과정을 거친다

  1. 애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회
  2. DB 드라이버는 DB 와 TCP/IP 커넥션을 연결
  3. DB 드라이버는 TCP/IP 커넥션이 연결되면, ID,PW와 부가 정보를 DB로 전달한다
  4. DB는 ID, PW를 통해 내부 인증을 완료하고, 내부에 DB 세션을 생성한다.
  5. DB는 커넥션 생성이 완료되었다는 응답을 보낸다
  6. DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환한다.

→ 문제점?

  • 커넥션을 항상 새로만드는 것은 복잡하고, 시간이 많이 소모된다.
  • DB 와 DB드라이버 (=애플리케이션) 에서 커넥션을 새로 생성하기 위한 리소스를 매번 사용해야 한다.
  • 유저가 애플리케이션을 사용하여 DB 처리 요청이 발생하면 SQL문을 실행하는 시간 + 커넥션을 새로 만드는데 발생하는 부가적인 시간이 발생한다. 즉 응답 속도가 느려지고 이는 사용자 경험을 저해한다.

커넥션 풀

이를 해결하기 위해 커넥션들을 미리 만들어두고 사용하는 커넥션 풀이라는 방법이다. 커넥션 풀은 이름 그래도 커넥션을 관리하는 풀 이다.

  • 커넥션 풀 사용 예시 1
    • 애플리케이션 로직에서 이제는 DB드라이버를 통해 새로운 커넥션을 획득하지 않는다.
    • 이제는 커넥션 풀에서 이미 생성되어 있는 커넥션을 객체 참조로 그냥 가져다 사용하면 된다.
    • 커넥션 풀에 커넥션을 요청하면, 커넥션 풀은 자신이 가지고 있는 커넥션 중에 하나를 반환한다.
  • 커넥션 풀 사용 예시 2
    • 애플리케이션 로직은 커넥션 풀에서 받은 커넥션을 사용해서 SQL을 데이터베이스에 전달하고, 그 결과를 받아서 처리한다.
    • 커넥션을 모두 사용하고 나면, 이제는 커넥션을 종료하지 않고, 다음에 다시 사용할 수 있도록 해당 커넥션을 그대로 커넥션 풀에 반환한다. 중요한 것은 살아있는 상태로 반환한다!

커넥션 풀 오픈소스

  • commons-dbcp2
  • tomcat-jdbc pool
  • HikariCP
    • 스프링부트 2.0 부터 기본 커넥션 풀을 hikariCP를 제공한다.

DataSource 이해

커넥션을 이전의 코드처럼 JDBC DriverManager 를 직접 사용하여 커넥션을 생성할 수 도 있고, HikariCP 커넥션 풀에서 커넥션을 받아 사용할 수 도 있다.

이때 기존의 코드로는 커넥션을 획득하는 애플리케이션 코드도 변경되어야 한다. 의존관계가 DriverManager → HikariCP 로 변경되었기 때문이다. 따라서 커넥션을 획득하는 방법을 추상화 하여 해결할 수 있다.

커넥션을 획득하는 방법을 추상화 하기

  • 자바에서는 이런 문제를 해결하기 위해 jakarta.sql.DataSource 인터페이스를 제공한다.
  • DataSource 는 커넥션을 획득하는 방법을 추상화한다.
  • 이 인테페이스의 핵심 기능은 커넥션 조회 이다.

DataSource - interface

public interface DataSource {
    Connection getConnection() throws SQLException;
}

getConnection() 을 사용하면 된다!

  • 기존의 DriverManager 는 DataSource 인터페이스를 사용하지 않는다. 따라서 DriverManager는 직접 사용해야 한다. 따라서 DriverManager를 사용하다가 DataSource 기반의 커넥션 풀을 사용하도록 변경하려면, 관련 코드를 다 고쳐야 한다. 이런 문제를 해결하기 위해 스프링은 DriverManager 도 DataSource 를 통해서 사용할 수 있도록 DriverManagerDataSource 라는 DataSource 를 구현한 클래스를 제공한다.

즉, DriverManagerDataSource → 항상 새로운 커넥션을 획득한다.

DataSource 예제 1 - DriverManager

@Test
void dataSourceDriverManager() throws SQLException {
    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());
}

Code 설명 :

  1. DataManagerDataSource → 새로운 커넥션을 생성하여 사용하는 dataSource

  2. dataSource.getConnection(); → dataSource (DataManagerDataSource) 의 커넥션을 받아온다.

    DataManagerDataSource 이기 때문에, 새로운 커넥션을 반환 받는다.

DataSource 예제 2 - 커넥션 풀

@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());
}
  • HikariCP 커넥션 풀을 사용하기 위해 HikariDataSource dataSource = new HikariDataSource(); 를 사용한다.
  • HikariDataSource 는 Builder pattern 이 적용되어 있다. 필요한 설정 정보를 set 해주고 사용하면 된다.

DataSource 적용

애플리케이션에 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;

/**
 * JDBC - DataSource 사용, Jdbc Utils 사용
 */
@Slf4j
public class MemberRepositoryV1 {

    private final DataSource dataSource;

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

    public Member save(Member member) throws SQLException {
        String sql = "insert into member(member_id, money) values (?,?)";

        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, member.getMemberId());
            preparedStatement.setInt(2, member.getMoney());
            preparedStatement.executeUpdate();
            return member;
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(connection, preparedStatement, null);
        }
    }

    public Member findById(String memberId) throws SQLException {
        String sql = "select * from member where member_id = ?";

        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;

        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, memberId);

            resultSet = preparedStatement.executeQuery();

            if (resultSet.next()) {
                Member member = new Member();
                member.setMemberId(resultSet.getString("member_id"));
                member.setMoney(resultSet.getInt("money"));
                return member;
            } else {
                throw new NoSuchElementException("member not found memberId = " + memberId);
            }

        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(connection, preparedStatement, resultSet);
        }
    }

    public void update(String memberId, int money) throws SQLException {
        String sql = "update member set money=? where member_id=?";

        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1, money);
            preparedStatement.setString(2, memberId);
            int resultSize = preparedStatement.executeUpdate();
            log.info("resultSize={}", resultSize);
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(connection, preparedStatement, null);
        }
    }

    public void delete(String memberId) {
        String sql = "delete from member where member_id = ?";
        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try{
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, memberId);
            preparedStatement.executeUpdate();
            log.info("delete memberId={}", memberId);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    private void close(Connection connection, Statement statement, ResultSet resultSet) {
        JdbcUtils.closeResultSet(resultSet);
        JdbcUtils.closeStatement(statement);
        JdbcUtils.closeConnection(connection);
    }

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

💡중요 Point

  • 생성자 주입으로 dataSource 를 받고, 이를 getConnection() 메서드에서 dataSource.getConnection 결과 커넥션을 반환하여 이를 사용한다.
  • 나머지는 모두 V0 과 동일하나, 커넥션을 닫는 과정을 편리하게 사용할 수 있는 편의 메서드가 제공된다.
    • JdbcUtils 편의 메서드
      • 스프링은 JDBC 를 편리하게 다룰 수 있는 JdbcUtils 라는 편의 메서드를 제공한다.
      • JdbcUtils을 사용하면 커넥션을 좀 더 편리하게 닫을 수 있다.

Test

package hello.jdbc.repository;

import com.zaxxer.hikari.HikariDataSource;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
import java.util.NoSuchElementException;
import static hello.jdbc.connection.ConnectionConst.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

@Slf4j
class MemberRepositoryV1Test {

    MemberRepositoryV1 repository;

    @BeforeEach
    void beforeEach() {
        //기본 DriverManager - 항상 새로운 커넥션 획득

//        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);

        //커넥션 풀링
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(URL);
        dataSource.setUsername(USERNAME);
        dataSource.setPassword(PASSWORD);

        repository = new MemberRepositoryV1(dataSource);
    }

    @Test
    void crud() throws SQLException {
        Member memberV0 = new Member("memberV100", 10000);
        repository.save(memberV0);

        //FindById
        Member findMember = repository.findById(memberV0.getMemberId());
        log.info("findMember = {}", findMember);

        log.info("member != findMember {}", memberV0 == findMember);
        assertThat(findMember).isEqualTo(memberV0);

        //update money 10000 -> 20000
        repository.update(memberV0.getMemberId(), 20000);
        Member updatedMember = repository.findById(memberV0.getMemberId());
        assertThat(updatedMember.getMoney()).isEqualTo(20000);

        //delete
        repository.delete(memberV0.getMemberId());

        //등록 했다가 바로 삭제하는 것이기 때문에 볼 수 없다.
        //그럼 검증을 어떻게 해야 하는가?
        assertThatThrownBy(() -> repository.findById(memberV0.getMemberId())).isInstanceOf(NoSuchElementException.class);

    }
}

💡중요 Point

애노테이션 “@beforeEach” 로 각 테스트가 시작 전 실행되도록 한다. 무엇을? 👇🏻

  1. 커넥션 풀 을 생성. → HikariCP 를 통해 생성한다.
    HikariDataSource dataSource = new HikariDataSource();
  2. repository 인스턴스 생성.

0개의 댓글