3. 커넥션풀과 데이터소스

2025. 5. 19. 16:38Spring/DB접근

[목차] 

1. JDBC Driver를 통한 Connection 생성 과정 

 

2. Connection 획득 방식 비교
1) DriverManager 방식 
2) 커넥션 풀 방식 

3) 두 방식 비교 

3. 커넥션 풀 사용 과정: 풀 초기화 → 커넥션 획득 → 반납 
1) 커넥션 풀 초기화
2) 커넥션 풀에서 커넥션 획득
3) 커넥션 사용 후 커넥션 풀에 반환

 

4. 커넥션 풀 오픈소스: hikariCP 

 

5. DataSource

1) 커넥션을 획득하는 다양한 방법 

2) DataSource 인터페이스 

3) DataSource 예제

 

6. DataSource 활용하여 기존 애플리케이션 수정 


1. JDBC Driver를 통한 Connection 생성 과정 

  • 애플리케이션 서버와 데이터베이스가 통신하기 위해서는 JDBC의 Connection의 구현 객체 필요하다.
  • Connection 객체는 JDBC Driver가 connect()를 통해 직접 생성하여 반환해준다. 
  • JDBC Driver의 데이터베이스 Connection 생성 과정은 아래와 같다 

1. 드라이버가 DB와 TCP/IP로 연결 (3-way handshake 발생)
→ 2. ID, PW 등 인증 정보를 DB에 전달
→ 3. DB가 내부 인증을 완료하고, DB 세션을 생성
→ 4. 커넥션 생성이 완료되었다는 응답을 보냄 
→ 5. 드라이버는 Connection 객체를 생성해서 클라이언트(애플리케이션)에 반환
  • 애플리케이션 서버와 데이터베이스 간의 통신을 통해 커넥션을 새로 만드는 과정은
    네트워크 통신 + 인증 + 세션 생성까지 포함되므로 비용이 크고 시간이 많이 걸림 

 

2. Connection 획득 방식 비교

1) DriverManager 방식 

  • 기존 JDBC에서는 DriverManager를 통해 Connection 객체를 획득했다. 
  • DriverManager는 getConnection(..) 요청을 받을 때마다
    데이터베이스 커넥션을 만들어 새로운 Connection 객체를 반환한다. 
  • 이 방식은 위의 무거운 커넥션 생성 과정을 요청마다 반복하기 때문에, 리소스 낭비 + 성능 저하로 이어진다.

2) 커넥션 풀 방식 

  • 이러한 비효율을 해결하기 위해 커넥션 풀(Connection Pool)이 등장했다.
    • 애플리케이션 실행 시점에 미리 일정 수의 DB 커넥션을 생성해 풀(pool)에 저장해 둔다.
    • → 이후 DB 연결이 필요한 상황이 오면, 풀에 있는 커넥션을 꺼내서 재사용한다.
    • → 사용이 끝나면 반납하여 다시 풀에 넣는다.
  • 매번 커넥션을 새로 생성하는 것이 아니라 커넥션 풀에 이미 생성되어 있는 커넥션을 객체 참조로 가져다 쓰기만 하면 된다. 
    따라서 성능이 향상되고, DB 자원을 효율적으로 관리할 수 있게 된다.

3) 두 방식 비교

항목 DriverManager 방식 커넥션 풀 방식
커넥션 생성 시점 요청마다 새로 생성 앱 시작 시 미리 생성 후 재사용
내부 흐름 매번 JDBC 드라이버의 connect() 호출 최초 한 번만 connect() 호출
성능 느림 (매번 네트워크 + 세션 생성) 빠름 (객체 재사용)
리소스 사용 비효율적, 낭비 많음 효율적, 풀에서 관리
사용 방법 DriverManager.getConnection() DataSource.getConnection()

 

3. 커넥션 풀 사용 과정: 풀 초기화 → 커넥션 획득 → 반납 

1) 커넥션 풀 초기화

  • 애플리케이션을 시작하는 시점에 커넥션 풀은 필요한 만큼 커넥션을 미리 확보해서 풀에 보관한다.
  • 적절한 커넥션 풀 숫자는 서비스의 특징과 애플리케이션 서버 스펙, DB 서버 스펙에 따라 다르기 때문에 성능 테 스트를 통해서 정해야 한다. 기본값은 보통 10개이다.
  •  커넥션 풀은 서버당 최대 커넥션 수를 제한할 수 있다
    따라서 DB에 무한정 연결이 생성되는 것을 막아주어서 DB를 보호하는 효과도 있다.
  •  커넥션 풀에 들어 있는 커넥션은 TCP/IP로 DB와 커넥션이 연결되어 있는 상태이기 때문에
    언제든지 즉시 
    SQL을 DB에 전달할 수 있다.

2) 커넥션 풀에서 커넥션 획득

  • 애플리케이션 로직에서 DB 드라이버를 통해서 새로운 커넥션을 획득하는 것이 아니라,
    이제는 커넥션 풀을 통해 이미 생성되어 있는 커넥션을 객체 참조로 그냥 가져다 쓰기만 하면 된다.
  • 커넥션 풀에 커넥션을 요청하면 커넥션 풀은 자신이 가지고 있는 커넥션 중에 하나를 반환한다. 

3) 커넥션 사용 후 커넥션 풀에 반환 

  • 애플리케이션 로직은 커넥션 풀에서 받은 커넥션을 사용해서 SQL을 데이터베이스에 전달하고 그 결과를 받아서 처리한다.
  • 커넥션을 모두 사용하고 나면 이제는 커넥션을 종료하는 것이 아니라,
    다음에 다시 사용할 수 있도록 해당 커넥션 을 그대로 커넥션 풀에 반환하면 된다.
  • 여기서 주의할 점은 커넥션을 종료하는 것이 아니라 커넥션이 살아있는 상태로 커넥션 풀에 반환해야 한다는 것이다.

4. 커넥션 풀 오픈소스: hikariCP 

  • 커넥션 풀은 얻는 이점이 매우 크기 때문에 실무에서는 항상 기본으로 사용한다.
  • 커넥션 풀은 개념적으로 단순해서 직접 구현할 수도 있지만,
    사용도 편리하고 성능도 뛰어난 오픈소스 커넥션 풀이 많기 때문에 오픈소스를 사용하는 것이 좋다.
  • 대표적인 커넥션 풀 오픈소스는 commons-dbcp2, tomcat-jdbc pool, HikariCP등이 있다.
  • 성능과 사용의 편리함 측면에서 최근에는 hikariCP를 주로 사용한다.
    • 스프링 부트 2.0 부터는 기본 커넥션 풀 로 hikariCP를 제공한다.
    • 실무에서도 레거시 프로젝트가 아닌 이상 대부분 hikariCP를 사용한다.
    • 성능, 사용의 편리함, 안전성 측면에서 이미 검증이 되었기 때문에 커넥션 풀을 사용 할 때는 그냥 hikariCP를 사용하면 된다.

 

5. DataSource

1) 커넥션을 획득하는 다양한 방법  

  • 커넥션을 획득하는 방법은 DriverManager 사용, 커넥션 풀 사용 등 다양하게 존재한다.
  • 그런데 DriverManager를 통해서 커넥션을 획득하다가, 커넥션 풀을 사용하는 방법으로 변경하려면 어떻게 해야할까?
  • 만약 애플리케이션 로직에서 DriverManager를 사용해서 커넥션을 획득하다가 HikariCP같은 커넥션 풀을 사용하도록 변경하면
    커넥션을 획득하는 애플리케이션 코드도 함께 변경해야 한다
    . 의존관계가 DriverManager에서 HikariCP로 변경되기 때문이다.
  • 따라서 커넥션 획득 방법을 변경할 때 어플리케이션 코드를 변경해야하는 OCP 위반 문제를 해결하기 위해
    자바에서는 javax.sql.DataSource라는 인터페이스를 제공한다.

2) DataSource 인터페이스 

public interface DataSource {
	Connection getConnection() throws SQLException;
}
  • DataSource 는 커넥션을 획득하는 방법을 추상화하는 인터페이스이다.
    이 인터페이스의 핵심 기능은 커넥션 조회 하나이다. (다른 일부 기능도 있지만 크게 중요하지 않다.)
  • 대부분의 커넥션 풀은 DataSource 인터페이스를 이미 구현해두었다.
    따라서 개발자는 DBCP2 커넥션 풀, HikariCP 커넥션 풀의 코드를 직접 의존하는 것이 아니라
    DataSource 인터페이스에만 의존하도록 애플리케이션 로직을 작성하면 된다.
  • DriverManager 는 DataSource 인터페이스를 사용하지 않는다.
    단, 스프링은 DriverManager도 DataSource를 통해서 사용할 수 있도록
    DriverManagerDataSource 라는 DataSource를 구현한 클래스를 제공한다.
  • 정리하면 자바는 OCP 위반 문제를 해결하기 위해 DataSource 를 통해 커넥션을 획득하는 방법을 추상화했다.
    이제 애플리케이션 로직은 DataSource 인터페이스에만 의존하면 된다.
    덕분에 DriverManagerDataSource 를 통해서 DriverManager 를 사용 하다가 커넥션 풀을 사용하도록 코드를 변경해도
    애플리케이션 로직은 변경하지 않아도 된다.

3) DataSource 예제

@Slf4j
public class ConnectionTest {
    //1. 기존의 DriverManager로 커넥션 획득 
    //사용과 설정이 한 곳에 있음  
    @Test
    void useDriverManager() 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. DataSource 인터페이스로 감싸서 커넥션 획득 
    //설정과 사용의 분리가 명확하기 때문에 사용 메서드의 코드를 변경할 필요 없음 
    
    //2-1. 사용 메서드 
    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());
    }
    
    //2-2. 설정 메서드 
    @Test //DriverManagerDataSource - 항상 새로운 커넥션을 획득
    void dataSourceDriverManager() throws SQLException {
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        useDataSource(dataSource);
    }

    @Test //HikariDataSource - 커넥션 풀링 
    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);
    }
}
  • DriverManager의 한계
    • DriverManager는 구체 클래스이기 때문에, 이를 사용하는 코드는 구현체에 직접 의존하게 된다.
      → 즉, 커넥션 생성 방식을 변경하려면 코드 자체를 수정해야 한다.
    • 또한 커넥션을 얻을 때마다 URL, USERNAME, PASSWORD 등 접속 정보를 직접 전달해야 한다.
      → 사용자 코드가 설정 정보에 강하게 결합되어 있다.
  • DataSource 방식의 장점
    • DataSource는 인터페이스이기 때문에
      사용 코드가 구현체에 직접 의존하는 것이 아니라  DataSource 인터페이스에만 의존하면 된다.
    • 또한 설정(접속 정보 등록)과 사용(커넥션 획득)을 명확히 분리할 수 있다.
      접속 정보는 객체 생성 시 한 번만 설정하면 되고, 사용자는 단순히 dataSource.getConnection()만 호출하면 된다
  • DI + OCP 원칙 
  DriverManager 방식 DataSource 방식
DI 원칙
(의존성 주입)
DriverManager 구현체에 직접 의존
+ 설정 정보(URL, ID, PW)에 직접 의존
→ 설정과 사용이 섞여 있음
사용 코드는 DataSource 인터페이스에만 의존
DataSource와 설정 정보는 외부에서 주입받음
설정은 한 곳에서, 사용할 땐 주입받아서 사용만 함
(사용시에는 아무것도 몰라도 됨, 정확한 DI 구조)
OCP 원칙
(개방-폐쇄원칙)
URL이나 계정, 커넥션 획득 방식 변경 시
→ 기존 코드를 열어서 수정해야 함 (OCP 위반)
useDataSource()는 그대로 두고
설정에서 DataSource 구현체만 변경 가능
확장(설정 변경)에 열려 있고, 기존 코드 변경은 없음

 

6. 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 connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        Connection con = dataSource.getConnection();
        log.info("get connection={}, class={}", con, con.getClass());
        return con;
    } 
}
  • DataSource 의존관계 주입 (DI) 
    • 외부 설정에서 DataSource 를 주입 받아서 사용한다. 
    • 따라서 이제 직접 만든 DBConnectionUtil 을 사용하지 않아도 된다.
    • DataSource는 표준 인터페이스이기 때문에
      DriverManagerDataSource에서 HikariDataSource로 변경되어도 해당 코드를 변경하지 않아도 된다.
  • JdbcUtils 편의 메서드
    • 스프링은 JDBC를 편리하게 다룰 수 있는 JdbcUtils라는 편의 메서드를 제공한다. 
    • JdbcUtils 을 사용하면 커넥션을 좀 더 편리하게 닫을 수 있다.
@Slf4j
class MemberRepositoryV1Test {
    MemberRepositoryV1 repository;
    
    //외부에서 DataSource 의존관계 주입 
    @BeforeEach
    void beforeEach() throws Exception {
        //1. 기본 DriverManager: 항상 새로운 커넥션 획득 
        //DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        
        //2. 커넥션 풀링: HikariProxyConnection -> JdbcConnection 
        HikariDataSource dataSource = new HikariDataSource(); 
        dataSource.setJdbcUrl(URL); dataSource.setUsername(USERNAME); 
        dataSource.setPassword(PASSWORD);
        repository = new MemberRepositoryV1(dataSource);
    }
    @Test
    void crud() throws SQLException, InterruptedException {
        log.info("start");
        
        //save
        Member member = new Member("memberV0", 10000);
        repository.save(member);
        
        //findById
        Member memberById = repository.findById(member.getMemberId());
        assertThat(memberById).isNotNull();
        
        //update: money: 10000 -> 20000
        repository.update(member.getMemberId(), 20000);
        Member updatedMember = repository.findById(member.getMemberId());
        assertThat(updatedMember.getMoney()).isEqualTo(20000);
        
        //delete
        repository.delete(member.getMemberId());
        assertThatThrownBy(() -> repository.findById(member.getMemberId()))
        				.isInstanceOf(NoSuchElementException.class);
    }
}
  • MemberRepositoryV1은 외부 설정 단계에서 DataSource 의존관계 주입이 필요하다. 
  •  DriverManagerDataSource에서 → HikariDataSource로 커넥션 획득 방식을 변경해도
    • MemberRepositoryV1 의 코드는 전혀 변경하지 않아도 된다.
    • MemberRepositoryV1 는 DataSource 인터페이스에만 의존하기 때문이다.
    • 이것이 DataSource 를 사용하는 장점이다.