[스프링 DB 1편 - 데이터 접근 핵심 원리] Thread Pool과 Data Source의 이해

sameul__choi·2022년 12월 29일
0

section2. Thread Pool과 Data Source 이해

커넥션 풀 이해

데이터 베이스 커넥션을 획들 할 때는 복잡한 과정을 거치게 된다.

  1. 애플리케이션 로직은 DB드라이버를 통해 커넥션을 조회
  2. DB 드라이버는 DB와 TCP/IP 커넥션을 연결 (3 way handshake 네트워크 동작 발생)
  3. TCP/IP커넥션 연결이 완료되면 ID,PW, 기타 부가정보를 DB에 전달
  4. DB는 ID, PW를 통해 내부 인증 완료, 내부에 DB세션 생성
  5. DB 커넥션 생성이 완료되었다는 응답을 보냄
  6. DB 드라이버는 커넥션 객체를 생성하여 클라이언트에 반환

이런 방식은 복잡하고 시간도 많이 소모 → 커넥션을 매번 새로 만듦

이 과정에서 리소스를 매번 사용해야하고 고객이 애플리케이션을 사용할 때, SQL을 실행하는 시간 뿐 아니라 커넥션을 새로 만드는 시간이 추가되어 응답속도에 영향을 주게 된다.

이를 해결하기 위한 아이디어가 Connection Pool이다.

애플리케이션이 시작하는 시점에 커넥션 풀은 필요한 만큼 커넥션을 미리 확보하여 풀에 보관한다. 얼마나 보관하는 지는 서비스의 특징과 서버 스펙에 따라 다르다. 애플리케이션 서버 스펙과 DB 스펙에 따라 상이하기 때문에 성능 테스트를 통하여 적절한 커넥션 풀의 수를 정한다. 무한정 DB에 커넥션이 생성되는 것을 막아 DB를 보호하는 효과도 있다.

커넥션 풀에 들어있는 커넥션들은 TCP/IP로 DB와 커넥션이 연결되어 있는 상태로 언제든지 즉시 SQL을 DB에 전달 할 수 있다.

DB 드라이버를 통해 새로운 커넥션을 획득하는 것이 아니라, 이미 생성되어 있는 커넥션을 객체 참조로 가져다 쓰는 것으로 애플리케이션 로직은 커넥션 풀에서 받은 커넥션을 사용, SQL을 DB에 전달, 그 결과를 받아 처리한다.

커넥션을 모두 사용하고 나면 커넥션을 종료하는 것이아니라, 다음에 다시 사용할 수 있도록 해당 커넥션을 그대로 커넥션 풀에 반환하게된다. 종료가 아니라 반환 !

대표적인 커넥션 풀 오픈소스는 commons-dbcp2,tomcat-jdbc pool,HicariCP 등이 있다.

DataSource 이해

앞서 설명한 것처럼 커넥션을 얻는 방법은 JDBC DriverManager를 직접 사용하거나 커넥션 풀을 사용하는 등 다양한 방법이 존재.

만약 JDBC로 개발한 어플리케이션 처럼 DriverManager를 통해서 커넥션을 획득하다가, 커넥션 풀을 사용하는 방법으로 변경하려면 어떻게 해야 할까 ?

어플리케이션 코드도 함께 변경해야 할 것이다. 의존 관계가 DriverManager에서 HikariCP로 변경되기 때문이다. 그리고 둘의 사용법도 다를 것이다.

자바에서는 이런 문제를 해결하기 위해 javax.sql.DataSource 라는 인터페이스를 제공한다.

즉, 커넥션을 획득하는 방법을 추상화 한다.

  • DataSource는 커넥션을 획득하는 방법을 추상화하는 인터페이스이다.
  • 핵심기능은 커넥션 조회 하나이다. (다른 일부기는도 있지만 크게 중요 x)

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

대부분의 커넥션 풀은 DataSource 인터페이스를 이미 구현해 두었다. 따라서 개발자는 커넥션 풀의 코드를 직접 의존하는 것이 아니라 DataSource 인터페이스에만 의존하도록 애플리케이션 로직을 작성하면 된다.

커넥션 풀 구현 기술을 변경하고 싶으면 해당 구현체로 갈아 끼우면 되는 방식이다.

JAVA는 DataSource를 통해 커넥션을 획득하는 방법을 추상화 했다. 이제 애플리케이션 로직은 DataSource 인터페이스만 의존하면된다.

ConnectionTest - DriverManager

package hello.jdbc.connection;
  import lombok.extern.slf4j.Slf4j;
  import org.junit.jupiter.api.Test;
  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 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());
} }

결과 :

ConnectionTest - 데이터소스 드라이버 매니저 추가

@Test
    void dataSourceDriverManager() throws SQLException {
        //DriverManagerDataSource - 항상 새로운 커넥션을 획득
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        // ==> //DataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        useDataSource(dataSource);

        //위와 똑같은데 데이터소스 즉 인터페이스를 통해 가져온다는 차이가 있다.
        // 사실 비슷해 보이는데 큰 차이가 있다.
        // 호출 할때마다 URL, USERNAME, PASSWORD를 입력하지 않아도 된다.
        // 설정과 사용의 분리를 이룰 수 있다.
    }
  • 기존 코드와 비슷하지만 DataSource를 통하여 커넥션을 획득할 수 있다. 기존의 방법과 큰 차이가 있다.
  • DriverManager는 커넥션을 획득 할 때 마다 URL, USERNAME, PASSWORD같은 파라미터를 계속 전달해야 한다. 반면 DataSource를 사용하는 방식이라면 처음 객체를 생성할 때만 필요한 파라미터를 넘겨두고 단순히 dataSource.getConnection()만 호출하면 된다.

이를 통하여 설정과 사용의 분리가 가능해 지는데, 설정과 관련된 속성들이 한곳에 있는 것이 향후 변경에 더 유연하게 대처할 수 있도록 한다.

굉장히 사소한 차이지만 큰 차이를 만들어내는데, 쉽게 이야기하면 Repository는 DataSource만 의존한다. 보통 어플리케이션 개발에서 설정은 한 곳에서 하지만 사용은 수 많은 곳에서 하게 되는데 사용과 설정 부분을 명확히 분리해 낼 수 있는 장점을 가진다.

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

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

실행결과 :

> Task :compileJava UP-TO-DATE
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE
> Task :compileTestJava UP-TO-DATE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test
16:12:48.916 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - MyPool - configuration:
16:12:48.926 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - allowPoolSuspension................................false
16:12:48.926 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - autoCommit................................true
16:12:48.926 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - catalog................................none
16:12:48.926 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - connectionInitSql................................none
16:12:48.927 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - connectionTestQuery................................none
16:12:48.927 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - connectionTimeout................................30000
16:12:48.927 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - dataSource................................none
16:12:48.927 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - dataSourceClassName................................none
16:12:48.927 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - dataSourceJNDI................................none
16:12:48.928 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - dataSourceProperties................................{password=<masked>}
16:12:48.928 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - driverClassName................................none
16:12:48.928 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - exceptionOverrideClassName................................none
16:12:48.928 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - healthCheckProperties................................{}
16:12:48.929 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - healthCheckRegistry................................none
16:12:48.929 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - idleTimeout................................600000
16:12:48.929 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - initializationFailTimeout................................1
16:12:48.929 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - isolateInternalQueries................................false
16:12:48.930 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - jdbcUrl................................jdbc:h2:tcp://localhost/~/test
16:12:48.930 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - keepaliveTime................................0
16:12:48.930 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - leakDetectionThreshold................................0
16:12:48.930 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - maxLifetime................................1800000
16:12:48.930 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - maximumPoolSize................................10
16:12:48.930 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - metricRegistry................................none
16:12:48.930 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - metricsTrackerFactory................................none
16:12:48.931 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - minimumIdle................................10
16:12:48.931 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - password................................<masked>
16:12:48.931 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - poolName................................"MyPool"
16:12:48.931 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - readOnly................................false
16:12:48.931 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - registerMbeans................................false
16:12:48.931 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - scheduledExecutor................................none
16:12:48.931 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - schema................................none
16:12:48.931 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - threadFactory................................internal
16:12:48.931 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - transactionIsolation................................default
16:12:48.932 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - username................................"sa"
16:12:48.932 [Test worker] DEBUG com.zaxxer.hikari.HikariConfig - validationTimeout................................5000
16:12:48.932 [Test worker] INFO com.zaxxer.hikari.HikariDataSource - MyPool - Starting...
16:12:48.947 [Test worker] DEBUG com.zaxxer.hikari.util.DriverDataSource - Loaded driver with class name org.h2.Driver for jdbcUrl=jdbc:h2:tcp://localhost/~/test
16:12:49.031 [Test worker] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn0: url=jdbc:h2:tcp://localhost/~/test user=SA
16:12:49.032 [Test worker] INFO com.zaxxer.hikari.HikariDataSource - MyPool - Start completed.
16:12:49.038 [MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn1: url=jdbc:h2:tcp://localhost/~/test user=SA
16:12:49.038 [Test worker] INFO hello.jdbc.connection.ConnectionTest - connection=HikariProxyConnection@1540894701 wrapping conn0: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class com.zaxxer.hikari.pool.HikariProxyConnection
16:12:49.039 [Test worker] INFO hello.jdbc.connection.ConnectionTest - connection=HikariProxyConnection@203149502 wrapping conn1: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class com.zaxxer.hikari.pool.HikariProxyConnection
16:12:49.134 [MyPool housekeeper] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Pool stats (total=2, active=2, idle=0, waiting=0)
16:12:49.138 [MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn2: url=jdbc:h2:tcp://localhost/~/test user=SA
16:12:49.141 [MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn3: url=jdbc:h2:tcp://localhost/~/test user=SA
16:12:49.144 [MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn4: url=jdbc:h2:tcp://localhost/~/test user=SA
16:12:49.147 [MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn5: url=jdbc:h2:tcp://localhost/~/test user=SA
16:12:49.150 [MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn6: url=jdbc:h2:tcp://localhost/~/test user=SA
16:12:49.153 [MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn7: url=jdbc:h2:tcp://localhost/~/test user=SA
16:12:49.156 [MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn8: url=jdbc:h2:tcp://localhost/~/test user=SA
16:12:49.159 [MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn9: url=jdbc:h2:tcp://localhost/~/test user=SA
16:12:49.159 [MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - After adding stats (total=10, active=2, idle=8, waiting=0)
BUILD SUCCESSFUL in 2s
4 actionable tasks: 1 executed, 3 up-to-date
4:12:50 PM: Execution finished ':test --tests "hello.jdbc.connection.ConnectionTest.dataSourceConnectionPool"'.
  • 풀 이름과 최대 풀 수를 확인할 수 있다.
  • Mypool Connection adder
    • 별도의 쓰레드를 사용하여 커넥션 풀에 커넥션을 채우고 있는 것을 확인할 수 있다.
    • why ? → 어플리케이션을 실행할 때 커넥션 풀을 채울때 까지 마냥 대기하고 있으면 애플리케이션 실행시간이 늦어지게 된다. 따로 스레드를 사용하면 어플리케이션 실행 시간에 영향을 주지 않는다.
  • 마지막 줄에 active = 2 , idle = 8사용준인 커넥션, 대기 상태인 커넥션을 확인 할 수 있다.

0개의 댓글