DB Connection을 획득하기 위한 추상화, DataSource

Woo0·2024년 3월 12일
post-thumbnail

DataSource란

커넥션을 얻는 방법은 JDBC DriverManager를 직접 사용하거나, 커넥션 풀을 사용하는 등 다양한 방법이 존재한다. DriverManager를 통해서 커넥션을 획득하다가, 커넥션 풀을 사용하는 방법으로 변경하려며 어떻게 해야할까?

-> 커넥션을 획득하는 방법을 추상화하면 된다.

  • Java에서는 이를 위해 javax.sql.DataSource라는 인터페이스를 제공한다.
  • DataSource는 커넥션을 획득하는 방법을 추상화 하는 인터페이스이다.
public interface DataSource {
	Connection getConnection() throws SQLException;
}

DataSource 정리

  • 대부분의 커넥션 풀은 DataSource 인터페이스를 이미 구현해두었다. 따라서 DBCP2 커넥션 풀, HikariCP 커넥션 풀의 코드를 직접 의존하는 것이 아니라 DataSource 인터페이스에만 의존하도록 작성하면 된다.

  • DriverManager는 DataSource 인터페이스를 사용하지 않는다. DriverManager는 직접 사용해야 하지만 스프링은 DriverManager도 DataSource를 통해서 사
    용할 수 있도록 DriverManagerDataSource라는 DataSource를 구현한 클래스를 제공한다.

DriverManager

먼저 기존에 개발했던 DriverManager를 통해서 커넥션을 획득하는 방법을 확인해보자.

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

각자 다른 2개의 connection을 획득했다. 이번에는 스프링이 제공하는 DataSource가 적용된 DriverManager인 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());
}

  • 기존 코드와 비슷하지만 DriverManagerDataSourceDataSource를 통해서 커넥션을 획득할 수 있다.
  • DriverManager는 커넥션을 획득할 때 마다 파라미터를 계속 전달해야 하지만, DataSource를 사용하는 방식은 처음 객체를 생성할 때만 필요한 파리미터를 넘겨두고, 커넥션을 획득할 때는 단순히 dataSource.getConnection()만 호출하면 된다.
  • 객체를 설정하는 부분과, 사용하는 부분을 좀 더 명확하게 분리해봤다.

커넥션 풀

이번에는 DataSource를 통해 커넥션 풀을 사용해보자.

@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);
    Thread.sleep(1000);
}
  • HikariCP 커넥션 풀을 사용한다. HikariDataSource는 DataSource 인터페이스를 구현하고 있다.
  • 커넥션 풀에서 커넥션을 생성하는 작업은 애플리케이션 실행 속도에 영향을 주지 않기 위해 별도의 쓰레드에서 작동한다.
  • 별도의 쓰레드에서 동작하기 때문에 테스트가 먼저 종료되는데 쓰레드 풀에 커넥션이 생성되는 로그를 확인하기 위해 Thread.sleep을 통해 대기 시간을 주었다. 코드를 실행시키고 로그를 확인해보자.

DataSource 적용

@Slf4j
public class MemberRepositoryV1 {

    private final DataSource dataSource;

	//의존관계 주입
    public MemberRepositoryV1(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    //save()...
 	//findById()...
 	//update()....
 	//delete()....
 
 	//JdbcUtils 사용
 	private void close(Connection con, Statement stmt, ResultSet rs) {
		JdbcUtils.closeResultSet(rs);
 		JdbcUtils.closeStatement(stmt);
 		JdbcUtils.closeConnection(con);
 	}
 
 	//DataSource 사용
 	private Connection getConnection() throws SQLException {
 		Connection con = dataSource.getConnection();
 		log.info("get connection={}, class={}", con, con.getClass());
 		return con;
 	}
}

DataSource : 표준 인터페이스이기 때문에 DriverManagerDataSource에서 HikariDataSource로 변경되어도 해당 코드를 변경하지 않아도 됨, 대신 DataSource 의존관계 주입이 필요
JdbcUtils : JDBC를 편리하게 다룰 수 있는 편의 메서드 제공, 커넥션을 좀 더 편리하게 닫을 수 있음

@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 {
        //save()...
        //findById()...
        //update()....
        //delete()....
    }
}

  • 커넥션 풀 사용시 conn0 커넥션이 재사용 된 것을 확인할 수 있는데 테스트는 순서대로 실행되면서 커넥션을 사용하고 다시 돌려주는 것을 반복하기 때문이다.
  • 웹 애플리케이션에 동시에 여러 요청이 들어오면 여러 쓰레드에서 커넥션 풀의 커넥션을 다양하게 가져가는 상황을 확인할 수 있다.
  • MemberRepositoryV1는 DataSource 인터페이스에만 의존하기 때문에 DriverManagerDataSource -> HikariDataSource로 변경해도 MemberRepositoryV1의 코드는 전혀 변경하지 않아도 된다.

-> DI+OCP, 이것이 DataSoure를 사용하는 장점이다.


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

profile
실패를 두려워하지 않는 백엔드 개발자가 되기 위해 노력하고 있습니다.

0개의 댓글