커넥션 풀과 데이터 소스

zwon·2023년 9월 8일
0

Java

목록 보기
3/4

커넥션 풀과 데이터 소스에 대해 알아보자.
JDBC 포스팅을 보면 DB 드라이버를 통해 항상 커넥션을 획득하게 된다.

그런데 커넥션을 획득하는 과정이 단순하지 않다.
1. Connection을 확대해보면 다음과 같다.

이렇게 DB 드라이버가 커넥션 생성이 완료되었다는 응답을 받으면 커넥션 객체를 생성해서 반환한다.

이처럼 커넥션을 생성하는 과정은 단순하지 않고 시간이 많이 소모된다.
그래서 매번 커넥션을 획득하다보면 응답 속도에 영향을 줄 수 있어 사용자에게 좋지 않은 서비스를 제공할 수 있다.

그래서 이러한 문제를 해결하기 위해 미리 커넥션을 생성해두고 사용하는 커넥션 풀이라는 개념이 등장했다.

커넥션 풀

  • 미리 커넥션을 생성하고 생성한 커넥션들을 관리하는 풀
  • Appication을 시작하는 시점에 커넥션 풀은 필요한 커넥션을 미리 확보해서 보관한다. 디폴트값은 보통 10개이다.

  • 이렇게 미리 커넥션을 생성해두기떄문에 드라이버를 통해서 커넥션을 획득할 필요가 없고 이제 Application Logic은 커넥션 풀에서 바로 커넥션을 꺼내서 사용할 수 있다.

  • 그리고 이 커넥션들은 모두 DB와 연결되어 있는 커넥션이기때문에 바로 쿼리문을 DB로 전송할 수 있다.

  • 그리고 커넥션을 사용 후 다시 커넥션 풀에 반환해주면 되기 떄문에 재활용할 수 있다.

    • 이때 커넥션을 반환해줄 떄에는 커넥션이 살아있는 상태로 반환해줘야 함.
  • 커넥션 풀은 오픈소스 커넥션 풀을 자주 사용하며 주로 hikariCP를 주로 사용함.

    • 참고로 스프링 부티2.0부터는 htkariCP 커넥션 풀을 디폴트로 제공해준다.

DataSource

  • 항상 나오는 레파토리로 데이터소스에 대해 이해해보자.
    • 이거로 작성하고 있었는데 딴걸로 바꾸고싶으면? -> 추상화
  • 드라이버를 통해서 커넥션을 획득하는 방법으로 로직을 작성하고 있었는데, 커넥션 풀 방식을 도입해야한다고 하면 로직을 모두 다시 작성해야하는 번거로움이 있다.
  • 그래서 커넥션 풀을 획득하는 방법을 DataSource라는 것을 통해서 추상화를 할 수 있다.

DataSource Interface

public interface DataSource  extends CommonDataSource, Wrapper {

  Connection getConnection() throws SQLException;

  Connection getConnection(String username, String password)
    throws SQLException;
  ...
  • getConnection()을 사용하여 커넥션 조회를 할 수 있다.
  • 대부분의 커넥션 풀도 DataSource가 이미 구현해두고 있어 개발자는 DataSource에 의존해서 로직을 작성하면 된다.
    • 구현체만 바꾸면 됨
  • 근데 DriverManager는 DataSource 인터페이스를 사용하지 않아서 DriverManager는 직접 사용해야하는데 스프링은 DriverManager도 DataSource를 쓸 수 있게 해주기위해서 DriverManegerDataSource라는 구현체를 제공한다.
    • 당연히 DriverManegerDataSource는 DataSource 인터페이스를 구현한 클래스

코드로 더 자세히 알아보자


H2 DB 연결을 위한 기본 정보

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

DriverManager로 connection 획득

  • 아까 위에서 DB 드라이버로 커넥션 획득 시 매번 커넥션이 생성된다고 했다.
  • 코드로 확인해보자.
  @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());
  }
  • 결과를 보면 서로 다른 커넥션들이 생성되었다.

DriverManegerDataSource로 connection 획득

  @Test
  void dataSourceDriverManager() throws SQLException{
    DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
    Connection con1 = dataSource.getConnection();
    Connection con2 = dataSource.getConnection();
    log.info("connection={}, class={}", con1, con1.getClass());
    log.info("connection={}, class={}", con2, con2.getClass());
  }
  • DriverManegerDataSource는 처음 객체를 생성할때만 DB에 필요한 정보들을 넘겨주고 커넥션을 획득할때는 필요한 정보들을 넘겨줄 필요없이 객체를 사용하기만 하면된다.
  • 마찬가지로 DataSource를 구현한 DriverManeger를 사용하기 때문에 결과는 다음과 같이 서로 다른 커넥션이 생성된다.

DataSource와 커넥션 풀

@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");

    Connection con1 = dataSource.getConnection();
    Connection con2 = dataSource.getConnection();
    log.info("connection={}, class={}", con1, con1.getClass());
    log.info("connection={}, class={}", con2, con2.getClass());
    Thread.sleep(1000);
  }
  • 여기서는 hikariCP커넥션 풀을 사용하기기위해 HikariDataSource를 사용했다.
  • dataSource.setJdbcUrl(URL), dataSource.setUsername(USERNAME), dataSource.setPassword(PASSWORD)를 통해서 필요한 정보들을 세팅해주었음.
  • setMaximumPoolSize(...)를 통해 풀 최대 사이즈를 정할 수 있다.
  • 마지막에 Threa.sleep(1000)을 해준 이유는 커넥션 풀에서 커넥션을 생성하는 작업은 실행 속도에 영항을 주지 않기 위해서 별도의 쓰레드에서 동작한다.
    그래서 먼저 종료되기때문에 Thread.sleep(1000)를 통해 커넥션 생성 시간을 대기하도록해서 로그로 확인 가능하게 하였다.
  • 별도의 쓰레드를 사용하는 이유는 커넥션 풀에 커넥션을 채워넣는것은 생각보다 오래 걸리는 작업이기때문이다.

로그를 확인해보자.
너무 길어서 약간 수정했다.

[myPool connection adder] Added connection conn2: url=jdbc:h2:tcp://localhost/~/test user=SA
[myPool connection adder] Added connection conn3: url=jdbc:h2:tcp://localhost/~/test user=SA
[myPool connection adder] Added connection conn4: url=jdbc:h2:tcp://localhost/~/test user=SA
[myPool connection adder] Added connection conn5: url=jdbc:h2:tcp://localhost/~/test user=SA
[myPool connection adder] Added connection conn6: url=jdbc:h2:tcp://localhost/~/test user=SA
[myPool connection adder] Added connection conn7: url=jdbc:h2:tcp://localhost/~/test user=SA
[myPool connection adder] Added connection conn8: url=jdbc:h2:tcp://localhost/~/test user=SA
[myPool connection adder] Added connection conn9: url=jdbc:h2:tcp://localhost/~/test user=SA
  • myPool connection adder이 위에서 말한 별도의 쓰레드이다.
  • conn0과 conn1은 우리가 위 코드에서 2개의 커넥션을 획득해서 2~9까지만 나와있는 것이다.

참고로 만약 커넥션 풀에 커넥션이 아직 생성되지 않았을 때, 커넥션을 획득할려고 하면 약간 대기하면서 커넥션을 획득한다.
그리고 만약 커넥션 풀이 가득찼을 때, 모든 커넥션을 다 사용하고있는데 또 커넥션 획득을 요구하는 경우에는 언제까지 대기할 것인지, 얼마정도 기다렸을 때 예외를 터뜨릴 것인지 등을 설정하는게 중요하다.


본 포스팅은 스프링 DB 1편를 공부하면서 정리한 글입니다.

profile
Backend 관련 지식을 정리하는 Back과사전

0개의 댓글

관련 채용 정보