Spring/Spring 김영한

[Spring DB 1편] (2) 커넥션풀과 데이터소스 이해

민돌v 2022. 9. 30. 11:37

 

인프런 김영한 - 스프링 DB 1 강의를 듣고 정리한 글 입니다.

 

 

 

목차

  1. Connection Pool 이란
  2. DataSource 란
  3. DataSourc 예제

 


 

1.  커넥션 풀 이해

이전 게시물의 방법대로 JDBC 를 사용한다면 데이터베이스에 접근할 때 마다 매번 커넥션을 획득해야하고 아래와 같은 불필요한 커넥션 과정을 거쳐야합니다.

 

커넥션 획득 과정

db 커넥셕 얻는 과정

  1. 어플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회한다.
  2. DB 드라이버는 DB 와 TCP/IP 커넥션을 연결한다. 물론 이 과정에서 3 way handshake 같은 TCP/IP 연결을 위한 네트워크 동작이 발생한다.
  3. DB 드라이버는 TCP/IP 커넥션이 연결되면 ID, PW 와 기타 부가정보를 DB 에 전달한다.
  4. DB는 ID, PW 를 통해 내부 인증을 완료하고, 내부에 DB 세션을 생성한다.
  5. DB는 커넥션 생성이 완료되었다는 응답을 보낸다.
  6. DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환한다.

 

커넥션의 문제점

  • 데이터베이스에 접근할 떄마다, 커녁션 생성/삭제 과정이 추가된다면, 고객사용 서비스에서 "SQL 처리 시간 + 커녁션 생성 시간"이 추가되기 때문에 응답 속에데 영향을 줍니다.

 

 

커넥션 풀

  • 이러한 문제를 해결하기 위해, 커넥션을 미리 생성해두고 사용하는 커넥션 풀 방법을 이용합니다.
  • 커넥션 풀이란, 이름 그대로 커넥션을 관리하는 풀(수영장) 입니다.

 

커넥셕 풀 초기화 및 연결 상태

(좌) 커넥션 초기화 / (우) 커넥션 연결상태

  1. 어플리케이션을 시작하는 시점에 커넥션 풀은 필요한 만큼의 커넥션을 미리 확보해서 커넥션 풀에 보관합니다.
  2. 보통 커넥션의 개수는, 서비스의 특징과 스펙에 따라 다르지만 보통 Default 개수는 10개 입니다.
  3. 커넥션 풀에 들어있는 커넥션은 TCP/IP로 DB와 커넥션이 연결되어 있는 상태이기 때문에 언제든지 즉시 SQL을 DB에 전달할 수 있습니다.

 

커넥션 풀 사용 과정

  • 🚀 애플리케이션 로직에서 이제는 DB 드라이버를 통해서 새로운 커넥션을 획득하는 것이 아니라 커넥션 풀을 통해 생성되어 있는 커넥션을 객체 참조로 가져다 사용하기만 하면 됩니다.

커넥션풀 생명주기

  1. 사용자가 커넥션 풀에 커넥션을 요청합니다.
  2. 커넥션 풀은 자신이 가지고 있는 커넥션이 있다면 커넥션 중에 하나를 반환합니다.
  3. 애플리케이션 로직은 커넥션 풀에서 받은 커넥션을 사용해서 SQL을 데이터베이스에 전달하고 그 결과를 받아서 처리합니다.
  4. 로직에서 커넥션을 다 사용했다면, 종료하지 않고(재사용할 수 있도록) 커넥션 풀에 반납합니다.

 

 

커넥션 풀 정점 및 정리

  1. 적절한 커넥션 풀 숫자는 각 상황에 따라 다르기 떄문에 성능 테스트를 통해서 정해야한다.
  2. 사용자는 최초에 커넥션 풀에 담긴 커넥션만 사용할 수 있기 때문에, 무한정으로 DB 에 연결이 생성되는 것을 막아 DB 를 보호해주는 장점도 있습니다.
  3. 실무에서는 통상적으로 커넥셔풀을 기본으로 사용한다고 합니다.
  4. 커넥션풀을 직접 구현할수도 있지만 이미 구현되어있는 오픈소스가 많아서 가져다 사용하면됩니다.
    • commons-dbcp2, tomcat-jdbc pool, HikariCP 등등..
    •  대표적으로 아래와 같은 오픈소스들이 있지만, 성능과 사용의 편리함 측면에서 hikariCP를 주로 사용합니다.
  5. 스프링 부터 2.0 부터는 Default 커넥션 풀로 hikariCP 를 제공해줍니다. (그만큼 안정적이고 거의 시장을 먹었다..?)

 

 


2. DataSource 이해

  • JDBC 를 이용해 DB 구현체를 쉽게 바꿀수 있었던 것 처럼 Driver Manager를 사용하다가 hikariCP 커넥셕 풀로 변경한다거나
  • 다른 커넥션풀을 사용하다가 커넥션 풀을 변경할 떄, 똑같이 커넥션을 획득하는 애플리케이션 코드도 함께 변경해야 합니다.
  • 의존관계가 DriverManager 에서 HikariCP 로 병경되기 때문입니다.

 

 

DataSource 란 - 커넥션을 획득하는 방법을 추상화

  • 자바에서는 이런 문제를 해결하기 위해서 jvax.sql.DataSource라는 인터페이스를 제공합니다.
  • ✅ DataSource 는 커넥션을 획득하는 방법을 추상화하는 인터페이스입니다.
  • DataSource 인터페이스의 핵심 기능은 커넥션 조회 하나입니다. (다른건 중요하지 않다고 하네요!)

 

 

DataSoruce 정리

  1. 대부분의 커넥션 풀은 DataSource인터페이스 구현체가 존재합니다. 따라서 개발자는 DBCP2 커넥션 풀, HikariCP 커넥션 풀의 코드를 직접 의존하는 것이 아니라 DataSource 인터페이스에만 의존하도록 어플리케이션 로직을 작성하면 됩니다.
  2. 따라서 커넥셕 풀 구현 기술을 변경하고 싶다면, 구현체만 갈아 끼우면 됩니다.
    • 단, DriverManager는 DataSource 인터페이스를 사용하지 않습니다. 따라서 DriverManager는 직접 사용해야 합니다.
    • 이렇게 되면, 커넥션 풀 ↔️ DriverManager 로 변경한다면 관련 코드를 수정해야합니다.
  3. 이러한 문제점을 해결하기위해 DriverManagerDataSource 라는 DataSource를 구현한 클래스를 제공합니다.

 

 


3. 예제 코드

DataSource 예제1 - DriverManager

이해가 가지 않다면 이전 게시물을 보자..!

 

1) DriverManager 를 이용해서 커넥션을 획득하는 방법

@Test
void driverManager() throws SQLException {
    Connection connection1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
    Connection connection2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
    log.info("connection={}, class={}", connection1, connection1.getClass());
    log.info("connection={}, class={}", connection2, connection2.getClass());
}

 

2) DriverManagerDataSource 를 사용해서 커넥션을 얻는 방법

  • DriverManagerDataSource는 스프링이 제공하는 DataSource 가 적용된 DriverManager 입니다.
  • DriverManagerDataSource는 DataSource 를 통해서 커넥션을 획득할 수 있습니다.
  • 이렇게 함으로써 Repository는 DataSource 에만 의존하고, URL, UserName, Passeword 같은 설정값들은 전혀 몰라도 되는 장점이 있습니다.
  • 설정과 사용이 분리됨을 의미합니다.
@Test
void dataSourceDriverManager() throws SQLException {
    //DriverManagerSource - 항상 새로운 커넥션을 획득
    DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
    useDataSource(dataSource);
}


private void useDataSource(DataSource dataSource) throws SQLException {
    Connection connection1 = dataSource.getConnection();
    Connection connection2 = dataSource.getConnection();
    log.info("connection={}, class={}", connection1, connection1.getClass());
    log.info("connection={}, class={}", connection2, connection2.getClass());
}

 


 

DataSource 예제2 - 커넥션 풀

  • HikariCP 커넥션 풀을 사용합니다. HikariDataSource 는 DataSource 인터페이스를 구현하고 있습니다.
  • 커넥션 풀에서 커넥션을 생성하는 작업은 애플리케이션 실행 속도에 영향을 주지 않기 위해 별도의 쓰레드에서 작동합니다.
  • 별도의 쓰레드에서 동작하기 때문에 테스타가 먼저 종료되어서, Sleap을 이용해 시간을 주어 쓰레드 풀에 커넥션이 생성되는걸 로그로 확인할  수 있습니다.
@Test
void dataSourceConnectionPool() throws SQLException, InterruptedException {
    //hikari pool 을 사용해서 커넥션 pooling
    //커넥션 풀링: 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);

}

 

📌 커넥션 풀이 별도의 쓰레드를 사용하는이유

  • 커넥션 풀에 커넥션을 채우는 일은 상대적으로 오래걸리는 일입니다.
  • 따라서, 어플리케이션이 실행할 때 커넥션 풀을 채울때까지 마냥 대기하고 있다면 실행시간이 늦어지기에
  • 별도의 쓰레드를 사용하여 커넥션풀을 채우는 것입니다.

 

📌 커넥션 풀 로그 분석

 


 

DataSourcr 적용

어플리케이션 코드에 DataSource 를 적용해보자!

 

📌 Repository 에 DataSource 적용

  • 외부에서 DataSource를 주입받아서 사용하기 때문에, 이전 게시물에서 직접만들었던 DBConnectionUtil을 사용하지 않아도 되고
  • DataSource 는 표준 인터페이스이기 때문에, DriverManagerDataSource나 HikariDataSource 등으로 변경해도 비지니스 로직은 코드가 수정되지 않습니다.
  • JdbcUtils 를 이용해서 조금 더 편리하게 커넥션을 닫을 수 있습니다.
/**
 * JDBC - DataSource 사용, JdbcUtils 사용
 */

@Slf4j
public class MemberRepositoryV1 {

    private final DataSource dataSource;

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

    //CRUD 로직 생략
    
    
    private void close(Connection con, Statement stmt, ResultSet resultSet) {
        //JdbcUtils 에서 제공하는 close 메소드 사용
        JdbcUtils.closeResultSet(resultSet);
        JdbcUtils.closeStatement(stmt);
        JdbcUtils.closeConnection(con);
    }

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

 

📌 TEST

  • beforeEach 에서 사용할 DataSource 구현체를 주입
@Slf4j
class MemberRepositoryV1Test {

    MemberRepositoryV1 repositoryV1;

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

        //이러면 속도가 너무 느리니, 커넥션 풀을 이용

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

        repositoryV1 = new MemberRepositoryV1(dataSource);
    }

    @Test
    void crud() throws SQLException, InterruptedException {

        //save
        Member memberV0 = new Member("memberV0", 100000);
        repositoryV1.save(memberV0);

        //findById
        Member findMember = repositoryV1.findById(memberV0.getMemberId());
        log.info("findMember = {}", findMember);

        //2개는 다른 인스턴스지만, isEqualTo 가  equals() 를 호출해 값만 비교 하기 때문에 true
        //@Data 롬복은, 모든 상태 값을 비교할수 있는 equals 와 고유한 hashCode 를 자동으로 만들어준다.
        assertThat(findMember).isEqualTo(memberV0);


        //update
        int updateMoney = 200000;
        repositoryV1.update(memberV0.getMemberId(), updateMoney);
        Member updateMember = repositoryV1.findById(memberV0.getMemberId());
        assertThat(updateMember.getMoney()).isEqualTo(updateMoney);


        //delete
        repositoryV1.delete(memberV0.getMemberId());
        assertThatThrownBy(() -> repositoryV1.findById(memberV0.getMemberId()))
            .isInstanceOf(NoSuchElementException.class);

        //커넥션 풀 채우는 거 볼려고
        Thread.sleep(1000);
    }
}

 

 

커넥션 풀, DataSource 적용하기 끝!