카테고리 없음

[DB] DataSource 란?

작은별._. 2024. 8. 3. 18:03
728x90

커넥션 획득 방법

커넥션을 얻는 방법은 JDBC DriverManager를 직접 사용하거나, 커넥션 풀을 사용하는 등 다양한 방법이 있습니다.


 

여기서 문제가 존재합니다. JDBC DriverManager를 통해 커넥션을 획득하다가, 커넥션 풀을 사용하는 방법으로 변경한다고 하면 애플리케이션 코드도 함께 변경해야 하게 되는 것이죠. 

위와 같은 문제를 해결하기 위해서 자바에서 javax.sql.DataSource 라는 인터페이스를 제공합니다.


DataSource

Datasource는 커넥션을 획득하는 방법을 추상화하는 인터페이스입니다.


 

Datasource 에는 여러 기능이 있지만, 핵심 기능은 커넥션 조회 라고 할 수 있습니다. 

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

 

  • 대부분의 커넥션 풀은 DataSource 인터페이스를 이미 구현하였습니다. 따라서, 개발자는 커넥션 풀의 코드를 직접 사용, 의존하는 것이 아니라 DataSource 인터페이스에만 의존하도록 애플리케이션 로직을 작성하면 됩니다.
  • 이렇게 DataSource 인터페이스만 의존하게 되면서 커넥션 풀 구현 기술을 변경하고 싶으면 해당 구현체로 갈아 끼우기만 하면 됩니다.
  • 다만 DriverManager는 DataSource 인터페이스를 사용하지 않기 때문에 DriverManager 는 직접 사용해야 합니다. 즉, DriverManager를 사용하다가 DataSource 기반의 커넥션 풀을 사용하도록 변경하면 관련 코드를 다 고쳐야 합니다. 이런 문제를 해결하기 위해 스프링은 DriverManagerDataSource 라는DataSource를 구현한 클래스를 제공하여 DriverManager 도 DataSource를 통해서 사용할 수 있도록 해 줍니다.
  • 즉, DriverManagerDataSource를 통해서 DriverManager를 사용 하다가 커넥션 풀을 사용하도록 코드를 변경해도 애플리케이션 로직은 변경하지 않아도 됩니다.

DataSource 예제 1

DriverManager와 DriverManagerDataSource 이용


package hello.jdbc.connection;

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 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());
    }

    @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());
    }
}

 

 

DriverManager 를 통해서 커넥션을 획득하는 방법과 DataSource를 통해서 커넥션을 획득하는 방법에는 큰 차이가 있습니다.

DriverManager는 커넥션을 획득할 때마다 URL , USERNAME , PASSWORD 같은 파라미터를 계속 전달해야 하지만, DataSource를 사용하는 방식은 처음 객체를 생성할 때만 필요한 파리미터를 넘겨두고, 커넥션을 획득할 때는 단순히 dataSource.getConnection()만 호출하면 된다는 점입니다. (설정과 사용의 분리)

 

설정과 사용의 분리

  • 설정: DataSource를 만들고 필요한 속성들을 사용해서 URL , USERNAME , PASSWORD 같은 부분을 입력하는 부분으로,. 이렇게 설정과 관련된 속성들은 한 곳에 있는 것이 향후 변경에 더 유연하게 대처할 수 있습니다.
  • 사용: 설정은 신경 쓰지 않고, DataSource의 getConnection()만 호출해서 사용하면 됩니다.

 

이 부분이 작아 보이지만 사실은 큰 차이를 만들어 냅니다. 필요한 데이터를 DataSource 가 만들어지는 시점에 미리 다 넣어두게 되면, DataSource를 사용하는 곳에서는 dataSource.getConnection()만 호출하면 되기 때문에, URL , USERNAME , PASSWORD 같은 속성들에 의존하지 않아도 됩니다. 그냥 DataSource 만 주입받아서 getConnection()만 호출하면 됩니다. 

 

실행결과

아래 결과를 통해 DriverManager와 DriverManagerDataSource는 항상 새로운 커넥션을 획득하는 것을 확인할 수 있습니다.

 

DriverManager

16:43:31.539 [Test worker] INFO hello.jdbc.connection.ConnectionTest2 - connection=conn0: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class org.h2.jdbc.JdbcConnection
16:43:31.545 [Test worker] INFO hello.jdbc.connection.ConnectionTest2 - connection=conn1: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class org.h2.jdbc.JdbcConnection

 

DriverManagerDataSource

16:43:31.574 [Test worker] DEBUG org.springframework.jdbc.datasource.DriverManagerDataSource - Creating new JDBC DriverManager Connection to [jdbc:h2:tcp://localhost/~/test]
16:43:31.578 [Test worker] DEBUG org.springframework.jdbc.datasource.DriverManagerDataSource - Creating new JDBC DriverManager Connection to [jdbc:h2:tcp://localhost/~/test]
16:43:31.580 [Test worker] INFO hello.jdbc.connection.ConnectionTest2 - connection=conn2: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class org.h2.jdbc.JdbcConnection
16:43:31.580 [Test worker] INFO hello.jdbc.connection.ConnectionTest2 - connection=conn3: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class org.h2.jdbc.JdbcConnection

DataSource 예제 2

커넥션 풀 사용

위에서 작성한 테스트 코드에 아래 코드를 추가하겠습니다.


 @Test
 void dataSourceConnectionPool() throws SQLException {
      HikariDataSource dataSource = new HikariDataSource();
      dataSource.setJdbcUrl(URL);
      dataSource.setUsername(USERNAME);
      dataSource.setPassword(PASSWORD);
      dataSource.setMaximumPoolSize(10); // 기본값도 10
      dataSource.setPoolName("MyPool");

      useDataSource(dataSource);
      Thread.sleep(1000); // 커넥션 풀에서 커넥션 생성 시간 대기
  }

 

 

커넥션 풀에서 커넥션을 생성하는 작업은 애플리케이션 실행 속도에 영향을 주지 않기 위해 별도의 스레드에서 작동합니다. 별도의 스레드에서 동작하기 때문에 테스트가 먼저 종료되기 때문에 Thread.sleep을 통해 대기 시간을 주어야 스레드 풀에 커넥션이 생성되는 로그를 확인할 수 있습니다.

 

1. 커넥션 풀 초기화 정보 출력


 

~~

 

 

2. 커넥션 풀에서 커넥션 생성 (10개)

아래 결과에서 볼 수 있듯이 커넥션 생성은 MyPool connection adder라는 새로운 스레드에서 수행하고 있습니다.


 

 

3. 커넥션 풀에서 커넥션 획득

커넥션 풀에서 커넥션을 획득한 시점에 커넥션 2개가 생성되어 있는 상태(total=2)이고 이를 가지고 가서 사용하므로 active = 2라는 것을 확인할 수 있습니다.


 


DataSource 적용

아래 코드의 CRUD 부분은 JDBC 실습 포스팅의 CRUD 부분과 동일합니다.


package hello.jdbc.repository.task;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.support.JdbcUtils;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

@Slf4j
@AllArgsConstructor
public class TaskRepositoryV1 {
    private final DataSource dataSource;
    
    // save() ..
    // findById() .. 
    // update () ..
    // delete() ..

    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;
    }
}

 

 JDBC의 DriverManager 코드에서 변경된 부분

  • DataSource 의존관계 주입
    • 직접 만든 DBConnectionUtil을 사용하지 않아도 됩니다.
  • JdbcUtils 편의 메서드
    • 스프링은 JDBC를 편리하게 다룰 수 있는 JdbcUtils라는 편의 메서드를 제공하는데 JdbcUtils을 사용하면 편리하게 커넥션을 닫을 수 있습니다.

 

테스트 코드


package hello.jdbc.repository.task;

import com.zaxxer.hikari.HikariDataSource;
import hello.jdbc.domain.Category;
import hello.jdbc.domain.Task;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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;

class TaskRepositoryV1Test {

    TaskRepositoryV1 repository;

    @BeforeEach
    void setUp() {
        //기본 DriverManager - 항상 새로운 커넥션 획득
        //DriverManagerDataSource dataSource =
        // new DriverManagerDataSource(URL, USERNAME, PASSWORD);


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

        // datasource 주입
        repository = new TaskRepositoryV1(dataSource);
    }

    @Test
    void crudTest() {
        // save
        Task task = new Task("v1", Category.LEISURE, 3);
        repository.save(task);

        // findById
        Task taskById = repository.findById(task.getTaskId());
        assertThat(taskById).isNotNull();

        // update
        repository.updatePriority(task.getTaskId(), 2);
        Task updatedTask = repository.findById(task.getTaskId());
        assertThat(updatedTask.getPriority()).isEqualTo(2);

        // delete
        repository.delete(task.getTaskId());
        assertThatThrownBy(() -> repository.findById(task.getTaskId())).isInstanceOf(NoSuchElementException.class);
    }

}

 

 

위 코드에서 2가지 버전으로 test 해 보았습니다. 먼저 DriverManagerDataSource 사용했을 때 아래와 같은 결과가 반환됩니다.


 

DriverManagerDataSource를 사용하면 conn0~5 번호를 통해서 항상 새로운 커넥션이 생성되어서 사용되는 것을 확인할 수 있습니다.

 

HikariDataSource를 사용했을 때는 아래 결과가 반환됩니다.


 

커넥션 풀 사용 시 conn0 커넥션이 재사용된 것을 확인할 수 있습니다. 테스트는 순서대로 실행되기 때문에 커넥션을 사용하고 다시 돌려주는 것을 반복하므로 conn0 만 사용되고 있습니. 웹 애플리케이션에 동시에 여러 요청이 들어오면 여러 스레드에서 커넥션 풀의 커넥션을 다양하게 가져가게 됩니다.

 

 

위 테스트 코드를 통해 DriverManagerDataSource를 HikariDataSource로 변경해도 TaskRepositoryV1의 코드는 전혀 변경하지 않아도 된다는 것을 확인할 수 있습니다. TaskRepositoryV1는 DataSource 인터페이스에만 의존하기 때문입니다. 이것이 DataSource를 사용하는 장점입니다. (DI + OCP)

728x90
반응형