제육's 휘발성 코딩
article thumbnail
반응형

커넥션 풀 이해

데이터베이스 커넥션 획득 과정

 

image

  • 애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회한다.
  • DB 드라이버는 DB와 TCP/IP 커넥션을 연결한다. (3 way handshake 등)
  • DB 드라이버는 TCP/IP 커넥션이 연결되면 ID, PW등 필요한 부가 정보를 DB에 전달한다.
  • DB는 ID,PW를 통해 내부 인증을 완료하고, 내부에 DB 세션을 생성한다.
  • DB는 커넥션 생성이 완료되었다는 응답을 받는다.
  • DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환한다.

이렇게 커넥션을 새로 만드는 것은 과정도 복잡하고 시간이 많이 소모되는 작업이다. 이런 문제를 해결하는 아이디어가 바로 커넥션을 미리 생성해두고 사용하는 커넥션 풀 방법이다.

 

 

image

  • 애플리케이션 시작 시점에 커넥션 풀은 필요한 만큼 미리 커넥션을 확보해서 풀에 보관한다. 보관하는 갯수는 서비스의 특징과 서버 스펙에 따라 다르지만 보통 기본값은 10개이다.

 

image

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

 

image

  • 다음 그림과 같이 커넥션 풀에서 이미 생성되어 있는 커넥션을 객체 참조로 가져다 쓴다. 커넥션을 모두 사용하고 나면 커넥션을 종료하는 것이 아닌, 다음에 다시 사용할 수 있도록 커넥션 풀에 다시 반환한다.

커넥션 풀로 얻는 이점이 매우 크기 때문에 실무에서 기본으로 사용한다. 대표적인 커넥션 풀 오픈소스는 commons-dbcp2, tomcat-jdbc pool, HikariCP 등이 있다. 스프링 부트에선 HikariCP를 기본으로 제공한다.

 

DataSource 이해

image

  • DriverManager로 커넥션을 획득하다가 커넥션 풀로 변환할 때 의존관계 또는 애플리케이션 코드를 변경해야 한다. 커넥션을 획들하는 방법을 공통 추상화하여 제공하는 것이 DataSource이다.

 

DataSource 예제 - DriverManager

 

ConnectionTest - 드라이버 매니저

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());
  }
}
  • 커넥션 호출을 2번 했을 때 모두 정상적으로 반환되는 것을 확인할 수 있다.

 

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

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

private void userDataSource(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를 통해서 커넥션을 획득할 수 있다.

드라이버 매니저와, 데이터 소스 매니저와 비슷해보이지만 다르다. 그것은 바로 설정과 사용의 분리이다.

설정 : DataSource를 만들고 필요한 속성들을 사용해서 URL, USERNAME 등을 입력하는 것이다.

사용 : 설정을 신경쓰지 않고 DataSource의 getConnection()만 호출해서 사용하면 된다.

즉, 리포지토리는 DataSource만 의존하고, 다른 속성을 몰라도 된다.

 

DataSource 예제2 - 커넥션 풀

 

ConnectionTest - 데이터소스 커넥션 풀 추가

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

  userDataSource(dataSource);
  Thread.sleep(1000); // 커넥션 풀에서 커넥션 생성 대기 시간
}
  • 스프링 부트에서 제공하는 HikariCP 커넥션풀을 사용한다. 생성 대기 시간을 주어야 커넥션이 생성되는 로그를 볼 수 있다.

 

실행 결과

image

  • 다음과 같이 커넥션 풀 로그 생성을 볼 수 있다.connection adder 를 통해 애플리케이션 실행과 별개로 별도의 쓰레드를 사용해서 커넥션 풀을 채운다.
  • 최대 설정한 풀의 갯수보다 많은 커넥션을 가져오면, 빌 때까지 대기하다가 예외를 반환한다.

 

DataSource 적용

 

MemberRepositoryV1

package hello.jdbc.repository;

import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.support.JdbcUtils;

import javax.sql.DataSource;
import java.sql.*;
import java.util.NoSuchElementException;


@Slf4j
public class MemberRepositoryV1 {

    private final DataSource dataSource;

    public MemberRepositoryV1(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    private void close(Connection con, Statement stmt, ResultSet rs) {
        JdbcUtils.closeConnection(con);
        JdbcUtils.closeStatement(stmt);
        JdbcUtils.closeResultSet(rs);
    }

    private Connection getConnection() throws SQLException {
        Connection con = dataSource.getConnection();
        log.info("get connection={}, class={}", con, con.getClass());
        return con;
    }

    /*
        CRUD는 V0와 동일 (생략)
    */
}
  • DataSource 의존관계 주입 - 표준 인터페이스이기 때문에 커넥션 획득 방식이 변경되어도 코드를 변경하지 않아도 된다.
  • JdbcUtils : 스프링은 JDBC를 편리하게 다룰 수 있는 JdbcUtils라는 편의 메서드를 제공한다.

 

MemberRepositoryV1Test

@Slf4j
class MemberRepositoryV1Test {

  MemberRepositoryV1 repository;

  @BeforeEach
  void beforeEach() {
    HikariDataSource dataSource = new HikariDataSource();
    dataSource.setJdbcUrl(URL);
    dataSource.setUsername(USERNAME);
    dataSource.setPassword(PASSWORD);

    repository = new MemberRepositoryV1(dataSource);
  }

  /*
      CRUD는 VO와 동일 (생략)
  */
}

 

DriverManagerDataSource 결과

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
  • 다음과 같이 커넥션 풀을 사용할 경우 커넥션이 재사용되는 것을 볼 수 있다.

DriverManagerDataSource, HikariDataSource 등 구현체를 변경해도 MemberRepositoryV1의 코드는 전혀 변경하지 않아도 된다. 그 이유는 MemberRepositoryV1은 DataSource 인터페이스에만 의존하기 때문이다. (DI + OCP)


본 포스팅은 인프런 - 김영한님의 '스프링 DB 1편 - 데이터 접근 핵심 원리'를 정리한 내용입니다.

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^