커넥션 풀, 데이터 소스

이정원·2024년 12월 7일
post-thumbnail

1.Connection Pool


데이터베이스 커넥션을 획득할 때는 다음과 같은 복잡한 과정을 거친다.

1.DB 드라이버를 통해 커넥션 요청: 애플리케이션에서 데이터베이스 드라이버를 사용해 커넥션을 요청한다.
2.DB와 TCP/IP 연결 설정: 네트워크를 통해 데이터베이스 서버와 TCP/IP 연결을 수립한다.
3.사용자 인증 정보 전달: 사용자 ID, 비밀번호, 기타 필요한 부가 정보를 데이터베이스에 전달한다.
4.내부 인증 및 세션 생성: 데이터베이스는 전달받은 정보를 검증하고, 내부적으로 DB 세션을 생성한다.
5.커넥션 생성 완료: 데이터베이스가 커넥션 생성 과정을 완료하고 응답을 반환합니다.
6.커넥션 객체 반환: 생성된 커넥션 객체를 클라이언트(애플리케이션)에 반환한다.

이렇게 커넥션을 새로 만드는 것은 과정도 복잡하고 시간도 많이 많이 소모되는 일이다. DB는 물론이고 애플리케이션 서버에서도 TCP/IP 커넥션을 새로 생성하기 위한 리소스를 매번 사용해야 한다. 진짜 문제는 고객이 애플리케이션을 사용할 때, SQL을 실행하는 시간 뿐만 아니라 커넥션을 새로 만드는 시간이 추가되기 때문에 결과적으로 응답 속도에 영향을 준다. 이것은 사용자에게 좋지 않은 경험을 줄 수 있다.

이런 문제를 한번에 해결하는 아이디어가 바로 커넥션을 미리 생성해두고 사용하는 커넥션 풀이라는 방법이다. 커넥션 풀은 이름 그대로 커넥션을 관리하는 풀(수영장 풀을 상상하면 된다.)이다.

애플리케이션을 시작하는 시점에 커넥션 풀은 필요한 만큼 커넥션을 미리 확보해서 풀에 보관한다. 애플리케이션 로직은 이제 DB 드라이버를 통해 연결을 맺는것이 아닌 커넥션 풀에 객체를 참조해서 가져다 쓰기만 하면 된다. 사용을 완료하고 연결이 살아있는 상태로 반환한다. 실무에서 대부분 hikariCP를 사용한다.

2.DataSource

앞서 JDBC로 개발한 애플리케이션 처럼 DriverManager를 통해서 커넥션을 획득하다가,커넥션 풀을 사용하는 방법으로 변경하려면 애플리케이션 코드도 변경해야한다. 자바에서는 이런 문제를 해결하기 위해 DataSource라는 인터페이스를 제공한다.

DataSource는 커넥션을 획득하는 방법을 추상화하는 인터페이스다. 이 인터페이스의 핵심 기능은 커넥션 조회 하나이다.

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

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

결과

실제 DB에서 연결된 커넥션은 각각 다르다.

이번엔 Spring이 제공하는 DriverManagerDataSource를 사용해보자.
(기존 DriverManager는 확장 불가)

 @Test
    void dataSourceDriverManager() throws SQLException {
        //DriverManagerDataSource - 항상 새로운 커넥션 획득
        DataSource 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는 커넥션을 획득할때마다 URL,USERNAME,PASSWORD같은 파라미터를 계속 전달해야 했다. 반면에 DataSource는 처음 객체를 생성할때 파라미터로 넘겨주고 getConnection()만 호출하면 된다.

설정과 사용의 분리
Repository는 DataSource 객체를 주입받아 이를 의존하기만 하면 된다. 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);
    }

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

별도의 Thread를 사용하여 MyPool connection adder가 필요한 커넥션을 채운다. 만약 커넥션 풀의 개수보다 많게 요청을 한다면 Block이 되고 time out이되어 예외가 생긴다.

이번엔 애플리케이션에 DataSource를 적용해보자.

 @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를 외부에서 주입받기 때문에 직접 만든 DBConnectionUtil을 사용하지 않아도 되고 DriverManagerDataSource에서 HikariDataSource로 변경되어도 해당 코드를 변경하지 않아도 된다.
테스트

MemberRepositoryV1 repository;

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


로그를 확인해보면 항상 DB에 새로운 커넥션을 맺는것을 확인할수 있다. 이 방법은 성능이 떨어진다. 커넥션 풀을 사용해보자.

//커넥션 풀링
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
repository=new MemberRepositoryV1(dataSource);


결과를 확인해보면 같은 connection이 5번 호출된것을 볼수 있다. 이유는 각 메서드(save,findById...)마다 연결을 요청하고,커넥션 풀을 사용할 경우 close 함수에서 실제 connection을 닫지 않고 Pool에 반환을 하게 된다. 이후 다시 연결을 요청할때 같은 connection을 재사용 하기 때문이다. 웹 어플리케이션에선 다수의 사용자에게 여러 쓰레드에 다양한 connection을 할당한다.

0개의 댓글