8 min read

<DB> Connection Pool과 DataSource

Connection Pool

저희는 JDBC를 이용해서 자바 어플리케이션에서 DB와 통신을 하고 데이터를 주고 받는 방법을 알아보았습니다.그러면 이제 조금 더 이론적인 내용에 대해 알아보겠습니다.아래의 코드에서 DriverManager를 통해서 Connection을 얻어오는 방식으로 DB와 통신을 진행하였습니다.다들 아시다시피 쿼리를 보낼 때마다 새로운 커넥션을 생성하고 연결하고 커넥션을 해제하는 과정을 거쳐야합니다.이러한 번거로운 절차를 돌파하기 위해 등장한 개념이 Connection Pool입니다.Pool이라 개념은 서블릿에서 요청을 처리하기 위해 사용할때 등장한 Thread Pool과 동일합니다.즉,쓰레드 여러개를 미리 만들어 두고 쓰레드가 필요할 때 가져가고 반납하는 기능을 수행합니다. Connection Pool은 커넥션이 필요할 경우 커넥션을 꺼내가도록 하고 다 쓰고나면 반납받도록 하는 기능을 수행합니다.

@Slf4j
public class DBConnectionUtil {
    public static Connection getConnection() {
        try {
            Connection connection = DriverManager.getConnection(ConnectionConst.URL, ConnectionConst.USERNAME, ConnectionConst.PASSWORD);
            log.info("Get connection={},class={}", connection, connection.getClass());
            return connection;
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        }
    }
}

만약 Connection Pool을 사용하지 않고 기존의 방식대로 DB와 통신한다면 아래의 복잡한 절차를 매번 거쳐야합니다.

하지만 Connection Pool을 사용하게 될 경우 아래와 같이 미리 TCP/IP 통신을 연결한 상태의 커넥션을 Pool에 담아두고 필요할때마다 요청한 곳이 바로바로 획득할 수 있도록 해줍니다.

그러면 커넥션 풀은 어느시점에 생성되는 걸까요?이는 어플리케이션을 동작시키면 자연스레 사용하는 DB도 시작됩니다.이 시점에서 DB는 커넥션 풀에 커넥션을 생성해서 필요한 갯수만큼 넣어둡니다.(MySql에서 기본 커넥션 수는 10개입니다)

그리고 만약 커넥션을 모두 사용하고 나서 반납할 시점이 올 경우 해당 커넥션을 TCP/IP 통신을 끊는 것이 아니라 다른 요청에도 사용가능하도록 해당 커넥션을 그대로 커넥션 풀에 반납합니다.

이러한 커넥션 풀은 다양한 오픈소스들로 존재합니다.(commons-dbcp2,tomcat-jdbc pool,hikari cp 등)

참고로 스프링부트는 공식적으로 HikariCP를 지원합니다.

Data Source

이번에는 Data Source에 대해서 알아보겠습니다.우선 저희가 DB 통신을 하기 위한 커넥션을 얻는 방법은 두가지를 배웠습니다.

  1. Driver Manager -> 매번 새로운 Connection 생성
  2. Connection Pool -> Connection 미리 만들어둠

만약 아래의 그림과 같이 개발자가 DriverManager를 사용하다가 Connection Pool을 도입하다가 또는 그 반대의 상황이 일어날 경우 어떻게 효율적으로 대처할 수 있을까요?즉,커넥션을 획득하는 방법의 변경이 일어날 경우 기존의 어플리케이션 로직을 수정하지 않고 어떻게 이를 해결할 수 있을까요?

해당 문제를 돌파하기 위해 내놓은 방법이 DataSource 인터페이스입니다.결론적으로 커넥션을 획득하는 방법을 추상화 시키는것입니다.아래는 실제 자바 패키지내의 인터페이스입니다.

위와 같은 인터페이스들을 DriverManager과 Connection Pool이 구현을 하면 개발자 입장에서는 getConnection()메서드만 사용해서 커넥션을 연결할 수 있습니다.도식화하면 아래의 그림과 같습니다.

DataSource라는 인터페이스의 구현체는 언제는 갈아끼울수 있으며 동시에 로직에는 아무런 영향을 주지 않는 다형성을 이용한 완벽한 구조라 볼 수 있습니다.

직접 사용해보기

간단한 테스트 코드를 통해 저희가 배운 DataSource와 Connection Pool을 사용해 봅시다.

참고로 H2 데이터베이스를 활용해 실습을 진행합니다.

제가 사용하는 H2 DB에 필요한 정보입니다.

public abstract class ConnectionConst {
    public static final String URL = "jdbc:h2:tcp://localhost/~/jdbc";
    public static final String USERNAME = "sa";
    public static final String PASSWORD = "";
}

우선 driverManager() 테스트,즉 기존에 저희가 디비를 연결하기 위해 사용했던 방식으로 테스트 해보겠습니다.이는 아시다시피 요청이 올때마다 새로운 커넥션을 생성해서 데이터를 교환합니다.아래의 테스트 결과를 보시면 매번 새로운 커넥션을 생성하는것을 확인할 수 있습니다.

import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

import static hello.jdbc.connection.ConnectionConst.*;

@Slf4j
public class ConnectionTest {
    @Test
    void driverManager() throws SQLException {
        Connection connection1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        Connection connection2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        log.info("connection : {}, class = {}", connection1, connection1.getClass());
        log.info("connection : {}, class = {}", connection2, connection2.getClass());
    }
}

두번째 테스트는 dataSource를 구현한 DriverManager를 사용해보겠습니다.이 또한 위와 마찬가지로 항상 새로운 커넥션을 생성하는것을 확인할 수 있습니다.다만 위의 방식와 차이점은 설정과 사용의 분리가 이루어져있다는 것입니다.쉽게 말하면 매번 커넥션을 요청할때마다 디비 설정정보를 보내지 않아도 된다는것입니다.

	@Test
    void dataSourceDriverManager() throws SQLException {
        //DriverManagerDataSource => 항상 새로운 커넥션 획득,다만 스프링에서 제공하는 DriverManager
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        useDataSource(dataSource);

    }

private void useDataSource(DataSource dataSource) throws SQLException {
        Connection connection1 = dataSource.getConnection();
        Connection connection2 = dataSource.getConnection();

        log.info("connection : {}, class = {}", connection1, connection1.getClass());
        log.info("connection : {}, class = {}", connection2, connection2.getClass());
    }

마지막으로 Connection Pool을 한번 사용해봅시다.우선 sleep을 통해 1초정도의 딜레이를 주는 이유를 먼저 말씀드리겠습니다.현재 저희가 동작시키는 테스트 코드는 워낙 시간이 얼마 걸리지 않기에 커넥션 풀에 10개의 커넥션이 미처 전부 채워지기 전에 코드가 끝나버리기에 그 전에 로그를 찍어보기 위해 딜레이를 주고 있습니다.

@Test
    void dataSourceConnectionPool() throws SQLException, InterruptedException {
        //Connection Pooling by Hikari CP
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(URL);
        dataSource.setUsername(USERNAME);
        dataSource.setPassword(PASSWORD);
        dataSource.setMaximumPoolSize(10);
        dataSource.setPoolName("Brido's Pool");

        useDataSource(dataSource);
        Thread.sleep(1000);
    }

굉장히 많은 로그들이 발생합니다.현재 로그에서는 저희가 0번과 1번의 커넥션을 사용하고 있음을 확인할 수 있습니다.다만 반납하는 절차를 넣어주지 않아서 종료될때까지 커넥션이 점유되고 있음을 확인 할 수 있습니다.

Connection Pool 에러내기

자,그러면 dataSourceConnectionPool() 메서드의 setMaximumPoolSize()에 1개의 커넥션만 넣어두고 해당 코드를 실행시켜 봅시다.14:47:08에 로그가 뜬 다음 30초뒤에 예외가 터집니다.이는 Hikari의 디폴트 Connection Timeout이 30초이기 그 때 발생합니다.우선 아래의 예외는 간단하게 커넥션이 필요한데 커넥션 풀에 가용가능한 커넥션이 30초내로 생기지 않아서 예외를 발생시킨것입니다.

어플리케이션을 개발하다보면 생각보다 히카리와 관련된 다양한 에러들을 접하기에 미리 알려드리고 싶어 발생시켜 보았습니다.히카리에 대한 포스팅도 추후에 진행할 예정입니다.

Ref : 김영한 - 스프링 DB 1