5 분 소요


DataSource 이해

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

DriverManager를 통해 커넥션 획득
19

  • DriverManager를 통해 커넥션을 획득하다가, 커넥션 풀을 사용하는 방법으로 변경하려면 어떻게 해야할까?

DriverManager를 통해 커넥션을 획득하다가 커넥션 풀로 변경시 문제
20

  • 예를 들어 애플리케이션 로직에서 DriverManager를 사용해서 커넥션을 획득하다가 HikariCP 같은 커넥션 풀을 사용하도록 변경하면 커넥션을 획득하는 애플리케이션 코드도 함께 변경해야 한다.
    • 의존관계가 DriverManager에서 HikariCP로 변경되기 때문이다.

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

  • 자바에서는 이런 문제를 해결하기 위해 javax.sql.DataSource라는 인터페이스를 제공한다.
  • DataSource커넥션을 획득하는 방법을 추상화하는 인터페이스이다.
  • 이 인터페이스의 핵심 기능은 커넥션 조회 하나이다. (다른 일부 기능도 있지만 중요하지 않다.)

DataSource 핵심 기능

public interface DataSource {

    // 핵심 기능만 축약
    Connection getConnection() throws SQLException;
}

정리

  • 대부분의 커넥션 풀은 DataSource 인터페이스를 이미 구현해두었기 때문에, 개발자는 커넥션 풀의 코드를 직접 의존하는 것이 아니라 DataSource 인터페이스만 의존하도록 애플리케이션 로직을 작성하면 된다.
  • 커넥션 풀 구현 기술을 변경하고 싶다면 해당 구현체로 변경만 하면 된다.
  • DriverManagerDataSource 인터페이스를 사용하지 않기 때문에 DriverManager는 직접 사용해야 한다. 따라서, DriverManager를 사용하다가 DataSource 기반의 커넥션 풀을 사용하도록 변경한다면 관련 코드를 모두 수정해야 한다.
    • 이런 문제를 해결하기 위해 스프링은 DriverManagerDataSource를 통해서 사용할 수 있도록 DriverManagerDataSource라는 DataSource를 구현한 클래스를 제공한다.
  • 자바는 DataSource를 통해 커넥션을 획득하는 방법을 추상화했기 때문에, 애플리케이션 로직은 DataSource 인터페이스에만 의존하면 된다.


DataSource 예제1 - DriverManager

DriverManager

...

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

실행 결과

connection=conn0: url=jdbc:h2:tcp://..jdbc user=SA, class=class 
org.h2.jdbc.JdbcConnection
connection=conn1: url=jdbc:h2:tcp://..jdbc user=SA, class=class 
org.h2.jdbc.JdbcConnection

DriverManagerDataSource

    @Test
    void dataSourceDriverManager() throws SQLException {
        // DriverManagerDataSource - 항생 새로운 커넥션 획득
        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());
    }

실행 결과

DriverManagerDataSource - Creating new JDBC DriverManager Connection to 
[jdbc:h2:tcp:..jdbc]
DriverManagerDataSource - Creating new JDBC DriverManager Connection to 
[jdbc:h2:tcp:..jdbc]
connection=conn0: url=jdbc:h2:tcp://..jdbc user=SA, class=class 
org.h2.jdbc.JdbcConnection
connection=conn1: url=jdbc:h2:tcp://..jdbc user=SA, class=class 
org.h2.jdbc.JdbcConnection

기존 코드와 비슷하지만 DriverManagerDataSourceDataSource를 통해서 커넥션을 획득할 수 있다.

파라미터 차이

  • DriverManager는 커넥션을 획득할 때 마다 URL, USERNAME, PASSWORD 같은 파라미터를 계속 전달해야 하는 반면에 DataSource를 사용하는 방식은 처음 객체를 생성할 때만 필요한 파라미터를 넘겨두고, 커넥션을 획득할 때는 dataSource.getConnection()만 호출하면 된다.

설정과 사용의 분리

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

  • 이 부분이 큰 차이를 만들어내는데, 필요한 데이터를 DataSource가 만들어지는 시점에 미리 다 넣어두게 되면, DataSource를 사용하는 곳에서는 dataSource.getConnection()만 호출하면 되므로, URL, USERNAME, PASSWORD같은 속성들에 의존하지 않아도 된다.
  • 쉽게 얘기해서 Repository는 DataSource만 의존하고, 이런 속성을 몰라도 된다.
  • 애플리케이션 개발은 보통 설정은 한 곳에서 하지만, 사용은 수 많은 곳에서 한다.
  • 덕분에 객체를 설정하는 부분과 사용하는 부분을 더 명확하게 분리할 수 있다.


DataSource 예제2 - 커넥션 풀

HikariDataSource

@Test
void dataSourceConnectionPool() throws SQLException, InterruptedException {
    // 커넥션 풀링: HikariProxyConnection(Proxy) -> JdbcConnection(Target)
    HikariDataSource dataSource = new HikariDataSource();
    dataSource.setJdbcUrl(URL);
    dataSource.setUsername(USERNAME);
    dataSource.setPassword(PASSWORD);
    dataSource.setMaximumPoolSize(10); // 기본값
    dataSource.setPoolName("MyPool");

    useDataSource(dataSource);
}
  • HikariCP 커넥션 풀을 사용한다. HikariDataSourceDataSource 인터페이스를 구현하고 있다.
  • 커넥션 풀 최대 사이즈를 10으로 지정하고, 풀의 이름을 MyPool 이라고 지정했다.
  • 커넥션 풀에서 커넥션을 생성하는 작업은 애플리케이션 실행 속도에 영향을 주지 않기 위해 별도의 쓰레드에서 작동한다.

실행 결과(로그 수정)

#커넥션 풀 초기화 정보 출력
HikariConfig - MyPool - configuration:
HikariConfig - maximumPoolSize................................10
HikariConfig - poolName................................"MyPool"

#커넥션 풀 전용 쓰레드가 커넥션 풀에 커넥션을 10개 채움
[MyPool connection adder] MyPool - Added connection conn0: url=jdbc:h2:.. 
user=SA
[MyPool connection adder] MyPool - Added connection conn1: url=jdbc:h2:.. 
user=SA
[MyPool connection adder] MyPool - Added connection conn2: url=jdbc:h2:.. 
user=SA
[MyPool connection adder] MyPool - Added connection conn3: url=jdbc:h2:.. 
user=SA
[MyPool connection adder] MyPool - Added connection conn4: url=jdbc:h2:.. 
user=SA
...
[MyPool connection adder] MyPool - Added connection conn9: url=jdbc:h2:.. 
user=SA

#커넥션 풀에서 커넥션 획득1
ConnectionTest - connection=HikariProxyConnection@446445803 wrapping conn0: 
url=jdbc:h2:tcp://localhost/~/jdbc user=SA, class=class 
com.zaxxer.hikari.pool.HikariProxyConnection

#커넥션 풀에서 커넥션 획득2
ConnectionTest - connection=HikariProxyConnection@832292933 wrapping conn1: 
url=jdbc:h2:tcp://localhost/~/jdbc user=SA, class=class 
com.zaxxer.hikari.pool.HikariProxyConnection

MyPool - After adding stats (total=10, active=2, idle=8, waiting=0)

HikariConfig
풀의 이름(MyPool)과 최대 풀 수(10)을 확인할 수 있다.

MyPool Connection adder
별도의 쓰레드를 사용해서 커넥션 풀에 커넥션을 채우고 있는 것을 확인할 수 있으며, 이 쓰레드는 커넥션 풀에 커넥션을 최대 풀 수(10)까지 채운다.

커넥션 풀에서 커넥션 획득
커넥션 풀에서 커넥션을 획득하고 그 결과를 출력. 여기서는 커넥션을 2개 획득하고 반환하지는 않았기 때문에, 풀에 있는 10개의 커넥션 중에 2개를 가지고 있는 상태이다.

[참고]
HikariCP 커넥션 풀 공식 사이트


DataSource 적용

애플리케이션에 DataSource 적용

MemberRepositoryV1

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

    private final DataSource dataSource;

    public MemberRepositoryV1(DataSource dataSource) {
        this.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;
    }
}
  • DataSource 의존관계 주입
    • 외부에서 DataSource를 주입 받아서 사용한다. 이제 직접 만든 DBConnectionUtil을 사용하지 않아도 된다.
    • DataSource는 표준 인터페이스 이기 때문에 DriverManagerDataSource 에서 HikariDataSource로 변경되어도 해당 코드를 변경하지 않아도 된다.
  • JdbcUtils 편의 메서드
    • 스프링은 JDBC를 편리하게 다룰 수 있는 JdbcUtils라는 편의 메서드를 제공한다.
    • JdbcUtils를 사용하면 커넥션을 좀 더 편리하게 닫을 수 있다.

MemberRepositoryV1Test

@Slf4j
class MemberRepositoryV1Test {

    MemberRepositoryV1 repository;

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

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

        repository = new MemberRepositoryV1(dataSource);
    }

    @Test
    void crud() throws SQLException, InterruptedException {
        // save
        // findById
        // update : money = 10000 -> 20000
        // delete
    }
}

DriverManagerDataSource 사용

get connection=conn0: url=jdbc:h2:.. user=SA class=class 
org.h2.jdbc.JdbcConnection
get connection=conn1: url=jdbc:h2:.. user=SA class=class 
org.h2.jdbc.JdbcConnection
get connection=conn2: url=jdbc:h2:.. user=SA class=class 
org.h2.jdbc.JdbcConnection
get connection=conn3: url=jdbc:h2:.. user=SA class=class 
org.h2.jdbc.JdbcConnection
get connection=conn4: url=jdbc:h2:.. user=SA class=class 
org.h2.jdbc.JdbcConnection
get connection=conn5: url=jdbc:h2:.. user=SA class=class 
org.h2.jdbc.JdbcConnection
  • DriverManagerDataSource를 사용하면 conn0~5 번호를 통해서 항상 새로운 커넥션이 생성되어서 사용되는 것을 확인할 수 있다.

HikariDataSource 사용

get connection=HikariProxyConnection@xxxxxxxx1 wrapping conn0: url=jdbc:h2:... 
user=SA
get connection=HikariProxyConnection@xxxxxxxx2 wrapping conn0: url=jdbc:h2:... 
user=SA
get connection=HikariProxyConnection@xxxxxxxx3 wrapping conn0: url=jdbc:h2:... 
user=SA
get connection=HikariProxyConnection@xxxxxxxx4 wrapping conn0: url=jdbc:h2:... 
user=SA
get connection=HikariProxyConnection@xxxxxxxx5 wrapping conn0: url=jdbc:h2:... 
user=SA
get connection=HikariProxyConnection@xxxxxxxx6 wrapping conn0: url=jdbc:h2:... 
user=SA
  • 커넥션 풀 사용시 conn0 커넥션이 재사용 된 것을 확인할 수 있다.
  • 테스트는 순서대로 실행되기 때문에 커넥션을 사용하고 다시 돌려주는 것을 반복한다. 따라서, conn0만 사용된다.

DI

DriverManagerDataSource -> HikariDataSource로 변경해도 MemberRepositoryV1의 코드는 수정하지 않아도 된다. 그 이유는 MemberRepositoryV1DataSource 인터페이스만 의존하기 때문이다.
이것이 DataSource를 사용할때의 장점이다.(DI + OCP)


<출처 : 인프런 - 스프링 DB 1편 : 데이터 접근 핵심 원리(김영한)>

댓글남기기