커넥션 풀과 데이터소스

2025. 9. 29. 16:07·Spring

커넥션 풀 이해

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

1. 애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회

2. DB 드라이버는 DB와 TCP/IP 커넥션 연결한다. (3way handshake같은 네트워크 동작 발생)

3. TCP/IP 커넥션이 연결되면 id, pw등 기타 부가정보를 DB에 전달

4. DB는 id, pw를 통해 내부 인증 완료하고 내부에 DB 세션 생성

5. DB는 커넥션 생성이 완료되었다는 응답 보냄

6. DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환

 

이렇게 과정이 복잡하고 시간도 많이 걸린다. DB는 물론 애플리케이션 서버에서도 커넥션 생성에 필요한 리소스를 매번 사용해야한다. 진짜 문제는 고객이 애플리케이션을 사용할 때 SQL을 실행하는 시간 뿐 아니라 커넥션을 새로 만드는 시간까지 추가되기 때문에 응답속도에 영향을 준다.

 

이런 문제를 해결하기위해 커넥션을 미리 생성해두고 사용하는 커넥션 풀을 사용한다.

 

애플리케이션 로직에서 커넥션 조회 -> 커넥션 획득 -> 커넥션 사용 -> 커넥션 풀에 반환

*커넥션을 사용한 후 종료하는것이 아닌 살아있는 상태에서 반환한다.

커넥션 풀은 필수적으로 사용한다. HTTP강의에서 봤듯 서버 규모에 맞는 커넥션 풀 숫자를 잘 정하는것이 중요하다.

최대 커넥션 수를 제한할 수 있기때문에 DB에 무한정 생성되는것을 막아줄 수 있어 DB보호 효과도 있다.

 

DataSource 이해

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

그런데, 문제는 DriverManager를 사용하다가 커넥션 풀로 변경 시 애플리케이션 코드도 함께 변경해야한다.

의존관계가 DriverManager -> HikariCP(예시)로 변경되기 때문이다.

 

자바에서 이런 문제를 해결하기 위해 javax.sql.DataSource라는 인터페이스를 제공한다.

DataSource는 커넥션을 획득하는 방법을 추상화 하는 인터페이스이다.

이 인터페이스의 핵심 기능은 커넥션 조회 하나이다. (다른것도 있지만 크게 중요하지 않음)

 

대부분 커넥션 풀은 DataSource 인터페이스를 이미 구현해두었기 때문에, DataSource 인터페이스에만 의존하도록 애플리케이션 로직을 작성하면 된다. 커넥션 풀 구현 기술을 변경하고 싶으면 해당 구현체로 갈아끼우기만 하면 되는것이다.

 

DataSource 예제1 - DriverManager

package hello.jdbc.connection;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import javax.sql.DataSource;
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());
    }

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

 

driverManager()과 dataSourceDriverManager()의 차이를 알아보자

 

drvierManager()는 커넥션을 획득할 때마다, URL, USERNAME, PASSWORD같은 파라미터를 계속해서 전달해야한다.

반면 dataSourceDriverManager()는 객체를 생성할때만 파라미터를 넘겨두고, 단순히 getConnection()만 호출하면 된다.

 

설정과 사용 분리

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

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

 

쉽게 이야기해서 repository는 DataSource만 의존하고, 이런 속성을 몰라도 된다.

애플리케이션을 개발해보면 설정은 한곳에서, 사용은 수 많은 곳에서 하게 된다. 이렇게 하는게 편하고 보기좋다.

 

DataSource 예제2 - 커넥션 풀

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

 

HikariCP 커넥션 풀을 사용했다. HikariDataSource는 DataSource 인터페이스를 구현하고 있다.

풀 maximum 10로 설정하고, MyPool이라고 이름도 지정할 수 있다.

커넥션 풀에서 커넥션을 생성하는 작업은 애플리케이션 실행속도에 영향을 주지 않기 위해 별도의 쓰레드에서 작동한다. 별도의 쓰레드에서 작동하기 때문에 테스트가 먼저 종료되어 버리기 때문에 Thread.sleep을 통해 대기시간을 준 것이다.

 

강의에선 After adding stats (total, active, idle, waiting)값이 나왔는데, 따로 설정하면 된다.

10개로 지정해두었다고 해서 10개를 미리 만들어두는 개념이 아니다.

minimumIdle 값을 지정해두면 미리 생성해둘 수 있다. 자세히 보려면 로그레벨을 DEBUG로 설정해라

 

DataSource 적용

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

 

package hello.jdbc.repository;

import com.zaxxer.hikari.HikariDataSource;
import hello.jdbc.connection.ConnectionConst;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import java.net.URL;
import java.sql.SQLException;
import java.util.NoSuchElementException;

import static hello.jdbc.connection.ConnectionConst.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

@Slf4j
class MemberRepositoryV1Test {

    MemberRepositoryV1 repository;

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

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

        repository = new MemberRepositoryV1(dataSource);
    }

    @Test
    void crud() throws SQLException {
        Member member = new Member("memberV8", 10000);
        repository.save(member);

        //findById
        Member findMember = repository.findById(member.getMemberId());
        log.info("findMember={}", findMember);
        log.info("member != findMember {}", member == findMember);
        log.info("member equals findMember {}", member.equals(findMember));
        assertThat(findMember).isEqualTo(member);

        //update: money 10000 -> 20000
        repository.update(member.getMemberId(), 20000);
        Member updateMember = repository.findById(member.getMemberId());
        assertThat(updateMember.getMoney()).isEqualTo(20000);

        repository.delete(member.getMemberId());
        assertThatThrownBy(() -> repository.findById(member.getMemberId()))
                .isInstanceOf(NoSuchElementException.class);
    }
}

 

DriverManager를 사용했을 때, 항상 새로운 커넥션을 획득했다. (코드 확인)

외부에서 DataSource를 주입받아 사용했기 때문에 DBConnectionUtil을 사용하지 않아도 된다.

JdbcUtils를 사용하면 커넥션을 좀 더 편리하게 닫을 수 있다.

 

그리고 그 이후 DriverManager -> HikariDataSource로 변경을 했는데, 코드 변경 없이 구현체를 바꿀 수 있었다.

실행결과 conn0만 호출되는것처럼 보이는데, 이건 커넥션을 종료할 경우 커넥션 풀에 반환해주는 과정 때문에 0번이 사용되고, 반환되는 과정이다.

 

DI

DriverManagerDataSource -> HikariDataSource로 변경해도 MemberREpositoryV1 코드는 전혀 변경하지 않았다.

MemberRepositoryV1은 DataSource인터페이스에만 의존하기 때문이다. 이것이 DataSource를 사용하는 장점이다.

(DI + OCP) 

 

 

'Spring' 카테고리의 다른 글

트랜잭션 2  (0) 2025.10.13
트랜잭션  (0) 2025.10.02
JDBC  (0) 2025.09.28
파일 업로드  (0) 2025.09.23
스프링 타입 컨버터  (0) 2025.09.21
'Spring' 카테고리의 다른 글
  • 트랜잭션 2
  • 트랜잭션
  • JDBC
  • 파일 업로드
공부처음하는사람
공부처음하는사람
  • 공부처음하는사람
    lazzzykim
    공부처음하는사람
  • 전체
    오늘
    어제
    • 분류 전체보기 (159)
      • Kotlin (31)
      • Java (56)
      • Spring (44)
      • JPA (6)
      • Algorithm (3)
      • TroubleShooting (1)
      • 내일배움캠프 프로젝트 (14)
      • Setting (2)
      • ... (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
  • 링크

  • 인기 글

  • 태그

    김영한의 실전자바
    김영한의 실전 자바
    중첩클래스
    예외처리
    java
    빈 생명주기
    Di
    싱글톤
    OCP
    다형성
    제네릭
    spring
    jpa
    내일배움캠프
    캡슐화
    트랜잭션
    배열
    kotlin
    언체크예외
    김영한
  • hELLO· Designed By정상우.v4.10.3
공부처음하는사람
커넥션 풀과 데이터소스
상단으로

티스토리툴바