Tick Tick Boom

시간이 다 가기 전에

개발/자바,스프링

커넥션 풀이란?

bbingle 2025. 6. 19. 18:46

1. 개념과 역사

1.1 커넥션 풀(Connection Pool)이란?

커넥션 풀은 DB와의 연결(Connection)을 미리 만들어두고 재사용하는 기술입니다.

java에서는 관계형 DB를 사용하기 위해 jdbc 즉, 다양한 종류의 관계형 데이터베이스에 접속하고 SQL 문을 처리하고자 할때 사용되는 표준 SQL 인터페이스 API 를 사용합니다.

이 jdbc 는 아래와 같은 과정을 거쳐서 데이터베이스에 연결 및 조작 혹은 조회를 하는 과정을 거치게 됩니다.

여기서 우리가 주목해야할 부분은 java.sql.Connection 부분입니다.
데이터베이스에 연결하기 위해서 커넥션을 즉, java.sql.Connection 객체를 만드는 과정을 거쳐야 하는데, 이 부분에서 굉장히 비용이 크게 발생합니다.
그래서, 일정 개수의 연결을 풀(pool) 형태로 보관하고, 필요할 때 빌려쓰고 다시 돌려주는 구조를 만듭니다.

즉, DB 연결을 미리 만들어놓고 재활용하는 구조라고 할 수 있다.

1.2 커넥션 풀이 등장하게 된 이유

(DB 연결 비용, 성능 문제)

위에서 언급했지만, 커넥션 풀이 등장하게 된 계기가 비용 때문이라고 했는데, 비용차이가 얼마나 날까?
간단하게 실험해보자.

테스트 구성

두가지 상황을 비교 해볼 것이다.

  1. 데이터베이스에 대한 작업을 할때마다 커넥션을 만드는 상황
  2. 히카리 커넥션풀을 사용하여 작업을 하는 상황

실제 여러 사용자가 순차적으로 요청을 하는 상황처럼 멀티쓰레드 환경에서 비교해보도록 하자

import java.sql.*;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.TimeUnit;  

public class NoConnectionPoolTest {  
  public static void main(String[] args) throws Exception {  
    String url = "jdbc:mysql://localhost:3306/community";  
    String user = "root";  
    String password = "20010310";  

    ExecutorService executor = Executors.newFixedThreadPool(10);  
    long start = System.currentTimeMillis();  

    for (int i = 0; i < 100; i++) {  
      executor.submit(() -> {  
        try (Connection conn = DriverManager.getConnection(url, user, password);  
            PreparedStatement stmt = conn.prepareStatement("SELECT 1");  
            ResultSet rs = stmt.executeQuery()) {  
          while (rs.next()) {  
            rs.getInt(1);  
          }  
        } catch (SQLException e) {  
          e.printStackTrace();  
        }  
      });  
    }  

    executor.shutdown();  
    executor.awaitTermination(2, TimeUnit.MINUTES);  

    long end = System.currentTimeMillis();  
    System.out.println("No Pool (멀티스레드) - Total Time: " + (end - start) + "ms");  
  }  
}
import com.zaxxer.hikari.HikariConfig;  
import com.zaxxer.hikari.HikariDataSource;  

import java.sql.*;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.TimeUnit;  

public class HikariCPTest {  
  public static void main(String[] args) throws Exception {  
    HikariConfig config = new HikariConfig();  
    config.setJdbcUrl("jdbc:mysql://localhost:3306/community");  
    config.setUsername("root");  
    config.setPassword("20010310");  
    config.setMaximumPoolSize(10);  

    HikariDataSource ds = new HikariDataSource(config);  

    ExecutorService executor = Executors.newFixedThreadPool(10);  
    long start = System.currentTimeMillis();  

    for (int i = 0; i < 100; i++) {  
      executor.submit(() -> {  
        try (Connection conn = ds.getConnection();  
            PreparedStatement stmt = conn.prepareStatement("SELECT 1");  
            ResultSet rs = stmt.executeQuery()) {  
          while (rs.next()) {  
            rs.getInt(1);  
          }  
        } catch (SQLException e) {  
          e.printStackTrace();  
        }  
      });  
    }  

    executor.shutdown();  
    executor.awaitTermination(1, TimeUnit.MINUTES);  

    long end = System.currentTimeMillis();  
    System.out.println("HikariCP (멀티스레드) - Total Time: " + (end - start) + "ms");  

    ds.close();  
  }  
}

두 클래스의 메인 메서드를 각각 수행시켜볼 경우 아래와 같은 결과를 확인할 수 있다.

 


단순 비교만으로 약 10배 정도에 달하는 성능 차이가 나는 것을 확인할 수 있다.

1.3 커넥션 풀의 발전사

위에서 봤듯이, 초기에 RDBMS에 연결하고 사용하기 위해서는 작업에 대해 매번 DriverManager.getConnection() 을 통해 직접 연결을 호출하는 과정이 필요했다.

이후에는 커넥션 풀이라는 개념이 만들어지게 되어, Commons DBCP, C3P0 등 초창기 커넥션풀 라이브러리가 사용되었다.
그러다, 기존 커넥션 풀의 무거움과 낮은 성능을 해결하기 위해 Brett Wooldridge 라는 미국개발자가 2012년경에 출발한 오픈소스 프로젝트이다.

2. 기술 원리

2.1 커넥션 풀의 기본 구조

커넥션 풀 관리자 (Pool Manager)

  • 커넥션 풀의 중앙 제어 유닛 역할을 합니다.
  • 풀을 생성하고, 풀 내 커넥션 객체의 상태를 관리하며, 요청에 따라 커넥션을 할당하거나 회수합니다.
  • 일반적으로 다음 기능을 포함합니다
    • 커넥션 생성, 회수, 폐기
    • 유휴 커넥션 관리
    • 커넥션 누수 감지 (timeout, leak detection)
    • 최대 커넥션 수, 최소 유휴 커넥션 수 등 설정값 유지

커넥션 객체(Connection Object) 관리 방식

HTTP 요청에 의해 점유/생성된 쓰레드가 데이터베이스 커넥션을 요청하면 유휴 커넥션을 찾아서 반환한다.
Hikari CP의 경우 이전에 사용했던 Connection이 존재하는 지 확인하고, 이를 우선적으로 반환하는 특성이 있다. (캐시 관련 성능 개선이 가능하다)


가능한 Connection이 존재하지 않으면, HandOffQueue를 Polling하면서 다른 Thread가 Connection을 반납하기를 기다린다. (지정한 TimeOut 시간까지 대기하다가 시간이 만료되면 예외를 던진다.)

HandOffQueue?

  • 새로운 Connection이 없고 풀도 가득 찬 경우, 다른 스레드가 커넥션을 반납하길 기다려야 함.
  • 이때 그냥 무작정 polling 하거나 block 하는 게 아니라,
    다른 스레드가 반납할 때까지 기다릴 수 있는 구조가 필요함.
  • 그래서 HikariCP는 내부적으로 ConcurrentBag 안에 있는 handoffQueue를 이용해서
    다른 스레드가 커넥션을 반납하는 순간, 기다리던 스레드에게 바로 전달되게 구현함.

풀 내부 상태

커넥션 풀은 보통 다음과 같은 상태별로 커넥션을 구분하여 관리합니다:

상태 설명
Idle 대기 중인, 아직 사용되지 않은 커넥션
In-use 현재 애플리케이션이 사용 중인 커넥션
Reserved 특정 요청을 위해 잠깐 보류된 커넥션 (예: 초기화 중)
Evicted 유휴 시간 초과, 테스트 실패 등으로 제거된 커넥션

설정 가능한 주요 파라미터

  • maximumPoolSize: 최대 커넥션 수
  • minimumIdle: 최소 유휴 커넥션 수
  • idleTimeout: 유휴 커넥션이 제거되기까지 대기 시간
  • connectionTimeout: 커넥션을 가져오기 위한 최대 대기 시간
    • 커넥션 생성/할당/회수 과정
    • 유휴 커넥션 감시, 재사용 메커니즘
  • 2.2 주요 동작 방식

2.3 커넥션 누수(Leak)란 무엇인가?

커넥션 누수(Connection Leak) 란,
애플리케이션이 DB 커넥션을 빌려왔지만, 반환하지 않고 계속 점유하고 있는 상태를 말한다.

즉, Connection conn = dataSource.getConnection() 으로 커넥션을 얻어온 후
conn.close() 를 호출하지 않으면 해당 커넥션은 커넥션 풀에 반납되지 않고 영원히 점유된다.

이 누적된 점유는 커넥션 풀의 전체 개수를 소진시켜, 새로운 커넥션 요청이 실패하게 만든다.
결과적으로 SQLTransientConnectionException: Connection is not available 같은 예외를 발생시킨다.

커넥션 누수는 성능 이슈보다도 장애로 이어지는 핵심 이슈 이므로(서비스 초반에는 잘 안 드러나지만, 트래픽 증가 시 대기 큐가 터져 갑자기 장애 발생), 정기적으로 DB 커넥션 수와 풀 상태 모니터링 (Prometheus + Grafana 등 활용) 해야한다.

3. 스프링부트와 커넥션 풀

3.1 Spring Boot에서의 기본 커넥션 풀 (HikariCP)

Spring Boot 2.x 이후 기본 커넥션 풀은 HikariCP 이다.
이유는 아래와 같다.

  • 뛰어난 성능(경량화)
  • 간단한 설정

그렇다면, 스프링부트 프로젝트에서 어떻게 사용할 지 알아보자

3.2 application.yml 설정 항목

HikariCP는 다양한 설정을 application.yml 또는 application.properties에서 할 수 있다.

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: user
    password: pass
    hikari:
      maximum-pool-size: 10 # 최대 커넥션 수
      minimum-idle: 5       # 최소 유휴 커넥션 수
      idle-timeout: 600000  # 유휴 커넥션 유지 시간(ms)
      connection-timeout: 30000 # 커넥션 획득 대기 시간(ms)
      max-lifetime: 1800000 # 커넥션 최대 생존 시간(ms)

3.3 커넥션 풀 모니터링 방법 (Actuator, JMX, 외부 툴)

커넥션풀은 위에서 언급했던 것처럼, 잘 관리하지 못할 경우 장애로 이어질 수 있는 요소이기 때문에, 모니터링이 중요하다.

어떤 방법들이 있을까?

  1. Spring Boot Actuator 사용
    management:
    endpoints:
     web:
       exposure:
         include: "health", "metrics"

가장 첫번째로 spring actuator를 이용해서 metrics 를 확인하는 것이다.
metric 이란 시스템상에서 측정가능한 지표의 단위로 metrics 엔드포인트가 노출 되어있다면 actuator/metrics/hikaricp.connections 과 같은 경로로 조회가 가능하다.

  1. JMX 사용
    JDK 1.5부터 포함된 런타임 어플리케이션 상태 모니터링 API이다.
    JMX를 통해 리소스를 관리하려면 MBean (Managed Bean)를 생성해야한다. 자원을 MBean으로 감싸 외부에서 API로 설정, 데이터수집, 원격제어등을 할 수 있게 만들어준다.
  2. 그라파나 등 외부 모니터링 도구 사용
  • Prometheus + Grafana
  • Micrometer와 연계하여 수집 가능

4. 실전 튜닝 가이드

4.1 적절한 max-pool-size 계산법

maximumPoolSize는 커넥션 풀의 최대 커넥션 수를 의미한다.
너무 작으면 병목이 생기고, 너무 크면 DB 리소스를 낭비하기 때문에, 적절한 커넥션 사이즈를 잡는게 좋다.

계산기준은 다음과 같다.

최대 동시 사용자 수 × 1명당 필요한 커넥션 수

적절한 최대 풀사이즈는 공식문서에서 아래와 같이 제시하고 있다.

maxPoolSize = (CPU 코어 수 × 2) + 디스크 대기 수
  • 예: 4코어 서버, 디스크 대기 없음 → 8개 정도 적정

4.2 DB 커넥션과 커넥션 풀 간 병목 분석

커넥션 풀이 꽉차고 병목현상이 발생하는 원인은 무엇일까?
커넥션 풀이 꽉 차는 현상은 보통 DB 성능상의 한계 혹은 쿼리 문제 때문에 발생한다.

보통

  • 커넥션이 너무 오래 사용되고 있거나 (long-running query)
  • 풀에 반환되지 않고 유실된 커넥션이 있거나 (leak)
    등으로 인해 발생한다.

이런 부분들은 slow query log, HikariCP metrics(액츄에이터) 등으로 확인하자!

4.3 스레드와 커넥션 풀 상호작용

각 HTTP 요청은 스레드 하나로 처리되고, 그 스레드는 DB 작업 시 풀에서 커넥션 하나를 할당받는다.
따라서 너무 많은 스레드(요청)가 동시에 오면 커넥션 풀이 부족해질 수 있다.

이부분은 Tomcat max-threads 설정과 함께 튜닝 고려해야 한다.
Tomcat의 maxThreads=200인데, 커넥션 풀은 10개 인 상황에 대해 계산해보면 아래와 같다.

요청 수: 200
DB 커넥션 수: 10
→ 동시에 처리 가능한 DB 요청: 10

나머지 190개의 요청은 대기 상태

예를 들어, DB 쿼리 평균 응답 시간이 100ms라고 하면:
- 1차 처리: 10개 → 0 ~ 100ms
- 2차 처리: 10개 → 100 ~ 200ms
- ...
- 20차 처리: 마지막 10개 → 1900 ~ 2000ms

👉 최악의 경우 응답 대기 시간: **2초 이상**

4.4 고부하 상황에서의 튜닝 포인트

고부하 시에는 다음 항목들 중심으로 튜닝해보자

  • maximumPoolSize 늘리기
  • connectionTimeout 늘려서 예외 피하기
  • 쿼리 최적화 (JOIN 줄이기, 인덱스 활용 등)
  • HikariCP의 leakDetectionThreshold 사용해 누수 감지
  • Actuator + Prometheus + Grafana 등으로 실시간 모니터링

    5. 문제 해결

5.1 커넥션 부족/과다 현상

커넥션 부족: 요청이 많은데 커넥션 풀이 너무 작으면, 커넥션을 얻기 위해 기다리는 요청들이 생기고, 결국 Timeout 에러 발생한다.

원인 예시는 다음과 같다.

  • maximumPoolSize 값이 너무 낮음
  • 커넥션을 회수하지 않고 누수(leak)된 경우

해결법으로는

  • maximumPoolSize 조정
  • 커넥션을 꼭 try-with-resourcesfinally 블록에서 닫도록 처리
  • HikariCP의 leakDetectionThreshold 설정해서 누수 추적

커넥션 과다: 풀 크기를 너무 키우면 DB 서버 자원이 부족해지고, 오히려 성능 저하 발생

  • DB는 동시에 처리 가능한 커넥션 수가 정해져 있음
    해결법으로는 아래와 같은 방법이 있다.
  • DB의 max_connections와 서버의 처리 능력을 고려한 적절한 설정

5.2 Timeout, Deadlock 관련 이슈

5.3 커넥션 풀 로그 분석법