컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
db · 15

커넥션 풀

성능HikariCP커넥션풀

커넥션 풀은 데이터베이스 연결을 미리 만들어 두고 요청이 빌려 쓰고 반납하게 하는 기법입니다. DB 연결을 새로 맺는 데는 네트워크 연결, 인증, 세션 준비 비용이 들기 때문에, 요청마다 새로 맺지 않고 열린 연결을 재사용해 이 비용을 줄입니다.

풀은 연결 수에 상한을 두는 역할도 합니다. 동시에 DB로 가는 요청이 풀 크기를 넘으면 나머지는 기다리게 해서 DB 서버를 보호합니다. 이 토픽에서는 풀이 연결을 빌려 주고 돌려받는 과정, 풀 크기가 부족하거나 너무 클 때의 문제, 연결을 반납하지 않는 누수를 시각화로 확인합니다.

핵심 개념

연결 재사용

풀은 열린 연결을 보관하다 요청이 오면 빌려 주고, 끝나면 돌려받아 다음 요청에 다시 줍니다. 연결을 새로 만드는 비용을 줄입니다.

핵심 개념

풀 크기

풀이 동시에 빌려 줄 수 있는 연결의 최대 수입니다. 크기를 넘는 요청은 연결이 반납될 때까지 기다립니다.

핵심 개념

대기와 시간 초과

연결이 모두 사용 중이면 요청은 줄을 서고, 정해진 시간 안에 얻지 못하면 오류로 실패합니다. HikariCP에서는 connectionTimeout이 이 한도입니다.

핵심 개념

반납

풀에서 받은 연결의 close()는 연결을 끊지 않고 풀에 돌려줍니다. 빌린 연결은 반드시 반납해야 합니다.

핵심 개념

연결 누수

빌린 연결을 반납하지 않는 문제입니다. 쌓이면 풀이 비어 새 요청이 연결을 얻지 못하는 풀 고갈로 이어집니다.

핵심 개념

HikariCP

가볍고 빠른 자바 커넥션 풀 구현체입니다. 스프링 부트는 HikariCP가 클래스패스에 있으면 이를 기본 풀로 사용합니다.

연결 비용을 한 번만 냅니다 — 연결을 만들고 인증하는 비용은 풀을 채울 때나 오래된 연결을 교체할 때만 내고, 요청마다 내지는 않습니다.
DB를 동시 연결 수로부터 보호합니다 — 풀 크기가 DB로 가는 동시 연결의 상한이 되어 연결이 폭주해 성능이 떨어지거나 max_connections를 넘어 연결이 거부되는 것을 막습니다.
풀이 크다고 빠르지 않습니다 — DB 서버가 동시에 효율적으로 처리할 수 있는 양에는 한계가 있어 연결을 지나치게 늘리면 경합이 커져 처리량이 줄 수 있습니다.
서버 대수도 계산에 넣어야 합니다 — 풀은 애플리케이션 인스턴스마다 있으므로 인스턴스 수와 풀 크기의 곱이 DB의 최대 연결 수를 넘지 않아야 하고, 예약된 연결과 관리용 접속도 빼고 계산해야 합니다.
이 시각화에서 확인할 것 — "풀의 동작" 탭에서 풀 없이 쓰면 요청마다 새 연결이 생기고 풀을 쓰면 연결이 반납되어 재사용되는 모습을, "풀 크기" 탭에서 풀 크기를 넘는 요청이 기다리다 시간 초과로 실패하는 모습과 풀이 너무 클 때 DB가 힘들어지는 모습을, "연결 누수" 탭에서 반납하지 않은 연결 때문에 풀이 비는 모습을 STEP으로 확인하세요.

커넥션 풀의 배경

웹 서비스가 요청마다 DB 연결을 새로 맺으면 요청이 몰릴 때 연결 생성이 병목이 되고, DB 서버의 연결 수 한도를 쉽게 넘깁니다. 자바에서는 JDBC의 DataSource 뒤에 풀을 두는 구조로 이 문제를 풀어 왔습니다.

자바 진영에는 Apache DBCP, C3P0, Tomcat JDBC Pool, HikariCP 같은 구현체가 있습니다. 스프링 부트 2.0부터 HikariCP가 기본 풀이 되었고, 설정은 spring.datasource.hikari 아래 속성으로 합니다.

숫자로 확인하기

풀 없이 3개 요청 — 요청이 3개면 연결 생성과 종료가 3번 일어나고, 풀을 쓰면 미리 만든 연결 3개를 돌려 씁니다.
풀 크기 3, 요청 5 — 연결 3개가 모두 쓰이면 R4와 R5는 기다리고, R1이 반납하면 R4가 받습니다. R5가 한도 안에 연결을 얻지 못하면 오류로 실패합니다.
HikariCP 기본값 — 풀 크기(maximumPoolSize) 10, 대기 한도(connectionTimeout) 30초, 누수 감지(leakDetectionThreshold) 0(꺼짐)입니다.
코드 확인 — 파이썬 풀 모델은 연결 3개를 빌린 뒤 네 번째 요청이 시간 초과가 나고, conn1을 반납하면 다음 요청이 conn1을 받습니다.

자주 하는 오해

풀 크기는 클수록 좋다 — 연결이 많으면 DB의 메모리 사용과 컨텍스트 전환, 잠금 경합이 늘어 처리량이 오히려 줄 수 있습니다. 필요한 만큼만 둡니다.
풀의 close()는 연결을 끊는다 — 풀이 준 연결의 close()는 실제 연결을 끊지 않고 풀에 반납합니다.
풀을 쓰면 쿼리 자체가 빨라진다 — 풀이 줄이는 것은 연결을 맺는 비용이고 쿼리 실행 시간은 그대로입니다.
풀이 가득 차면 DB가 바쁜 것이다 — 연결 누수나 오래 걸리는 쿼리, 트랜잭션을 열어 둔 코드 때문에 연결이 반납되지 않아도 풀은 가득 찹니다. 원인을 따로 확인해야 합니다.
풀 크기만 늘리면 대기가 해결된다 — 쿼리가 느려서 연결을 오래 쥐고 있는 것이 원인이면 풀을 키워도 DB 부담만 커집니다. 쿼리와 트랜잭션 길이를 먼저 봅니다.

면접에서 이렇게 물어요

면접 질문

커넥션 풀을 쓰는 이유는?

DB 연결을 새로 맺는 비용을 줄이려고 미리 만든 연결을 재사용하고, 동시 연결 수를 제한해 DB를 보호하기 위해서입니다.

면접 질문

풀 크기를 무작정 크게 잡으면 어떻게 되나요?

DB 서버의 자원 부담과 경합이 늘어 성능이 오히려 떨어질 수 있습니다. 코어 수와 쿼리 특성을 고려해 적정 크기를 잡고 측정으로 조정합니다.

면접 질문

스프링 부트의 기본 커넥션 풀은?

HikariCP입니다. 스프링 부트 2.0부터 기본이고, maximum-pool-size 같은 속성으로 크기를 조정합니다.

면접 질문

연결 누수는 왜 위험하고 어떻게 막나요?

반납되지 않은 연결이 쌓이면 풀이 비어 새 요청이 실패합니다. try-with-resources 같은 구조로 항상 반납하고, 누수 감지 설정으로 오래 반납되지 않는 연결을 찾습니다.

활용 분야

웹 애플리케이션 서버 — 요청이 몰려도 연결 생성 비용 없이 DB에 접근하고 연결 수가 DB 한도 안에 머물게 합니다.
연결 수가 많은 환경의 프록시 풀 — 애플리케이션 인스턴스가 많을 때 DB 앞에 연결 풀링 프록시를 두어 DB가 받는 연결 수를 줄이기도 합니다.
운영 점검 — 풀의 사용 중 연결 수와 대기 요청 수, 연결 획득 시간을 감시해 풀 크기를 조정하고 누수를 찾습니다.

함께 보면 좋은 토픽

트랜잭션 — 연결을 쥔 채 트랜잭션이 길어질 때의 영향을 봅니다.
락 — 연결이 늘 때 커지는 잠금 경합을 봅니다.
쿼리 실행 계획 — 연결을 오래 쥐는 느린 쿼리를 찾는 방법을 봅니다.
복제 — 읽기 부하를 나누는 구성과 풀을 함께 설계하는 방법을 봅니다.

참고 자료

SQL — PostgreSQL에서 연결 수와 상태 확인

SHOW max_connections;                                   -- 서버가 받는 최대 동시 연결 수 (보통 기본 100)

SELECT state, count(*)
FROM pg_stat_activity
WHERE datname = current_database()
GROUP BY state;                                        -- active / idle / idle in transaction 별 연결 수

SELECT pid, now() - xact_start AS open_time, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_start;                                   -- 트랜잭션을 연 채 놀고 있는 연결 (반납이 늦는 후보)

PostgreSQL은 연결마다 서버 프로세스를 하나씩 띄우므로 max_connections로 동시에 받을 수 있는 연결 수를 제한하고, 기본값은 보통 100입니다. 애플리케이션 서버가 여러 대일 때는 서버마다 가진 풀 크기를 모두 더한 값이 이 한도를 넘지 않아야 합니다. pg_stat_activity의 state로 지금 연결이 쿼리를 실행 중인지(active), 놀고 있는지(idle), 트랜잭션을 연 채 놀고 있는지(idle in transaction) 볼 수 있습니다. 세 번째 쿼리에서 idle in transaction 연결이 오래 남아 있다면 연결을 쥔 채 트랜잭션을 끝내지 않는 코드가 있다는 신호입니다. state 값은 이 셋만 있는 것은 아니며, 권한이 없으면 다른 사용자 세션의 열은 NULL로 보일 수 있습니다. 시각화의 연결 누수 탭이 보여 주는 문제와 이어집니다.

Python — 큐로 만든 연결 풀

import queue

class Pool:
    def __init__(self, size):
        self.q = queue.Queue()
        for i in range(size):
            self.q.put(f'conn{i + 1}')            # 미리 만들어 둔 연결

    def get(self, timeout):
        return self.q.get(timeout=timeout)        # 빈 연결이 없으면 대기, 시간 초과 시 queue.Empty

    def put(self, conn):
        self.q.put(conn)                          # 반납: 연결은 끊지 않고 되돌려 놓음

pool = Pool(3)
held = [pool.get(0.1) for _ in range(3)]
print(held)
try:
    pool.get(0.1)
except queue.Empty:
    print('4번째 요청: 시간 초과')
pool.put(held.pop(0))
print('반납 뒤 재사용:', pool.get(0.1))
# ['conn1', 'conn2', 'conn3']
# 4번째 요청: 시간 초과
# 반납 뒤 재사용: conn1

풀의 핵심을 큐로 옮긴 장난감 모델입니다. 시작할 때 연결 3개를 큐에 넣어 두고, 요청은 get으로 하나씩 꺼내 쓰고 put으로 돌려놓습니다. 연결 3개를 모두 빌린 뒤 네 번째 요청은 빈 연결이 없어 기다리다 시간 초과가 나고, conn1을 반납하자 바로 다음 요청이 같은 conn1을 받아 갑니다. 시각화의 풀의 동작, 풀 크기 탭과 같은 결과입니다. 실제 풀은 연결이 끊어졌는지 확인하고, 필요하면 새 연결을 만들며, 스레드 안전을 위한 처리를 더 합니다. 여기서는 큐가 스레드 안전한 것에 기대어 가장 단순한 형태만 보였습니다.

Java — HikariCP 설정과 안전한 반납

HikariConfig cfg = new HikariConfig();
cfg.setJdbcUrl("jdbc:postgresql://localhost:5432/app");
cfg.setMaximumPoolSize(10);              // 풀 크기 (기본값 10)
cfg.setConnectionTimeout(30_000);         // 연결을 얻으려 기다리는 한도 ms (기본값 30초)
cfg.setLeakDetectionThreshold(2_000);     // 이보다 오래 반납 안 되면 로그 (기본값 0 = 꺼짐)

try (HikariDataSource ds = new HikariDataSource(cfg)) {
    try (Connection conn = ds.getConnection();
         PreparedStatement ps = conn.prepareStatement("SELECT 1")) {
        ps.executeQuery();
    }                                          // 블록을 나가면 close()가 연결을 풀에 반납
}

// Spring Boot: spring.datasource.hikari.maximum-pool-size=10

HikariCP는 자바 풀 구현체이고 스프링 부트는 HikariCP가 있으면 기본으로 사용합니다. 풀 크기를 정하는 maximumPoolSize, 연결을 기다리는 한도인 connectionTimeout, 반납되지 않는 연결을 로그로 알려 주는 leakDetectionThreshold를 설정할 수 있습니다. 괄호 안의 기본값은 HikariCP 문서에 적힌 값입니다. 설정 값은 서비스 환경에 맞춰 정해야 합니다. try-with-resources로 Connection을 열면 예외가 나도 블록을 나갈 때 close()가 호출됩니다. 풀이 준 Connection의 close()는 연결을 끊지 않고 풀에 반납하므로, 이를 빠뜨리지 않는 것이 연결 누수를 막는 가장 확실한 방법입니다.

Q1

커넥션 풀을 사용하는 주된 이유는?

연결을 맺을 때는 TCP 핸드셰이크와 인증 등이 필요합니다.
DB 연결을 새로 맺는 데는 네트워크 핸드셰이크와 인증 등 비용이 듭니다. 커넥션 풀은 미리 만들어 둔 연결을 빌려 쓰고 반납하게 해 이 비용을 줄이고 동시 연결 수도 제한합니다.

Q2

커넥션 풀 크기를 무작정 크게 잡으면 생기는 문제는?

DB 서버가 동시에 처리할 수 있는 양에는 한계가 있습니다.
연결이 너무 많으면 DB 서버의 메모리 사용과 컨텍스트 스위칭이 늘고 잠금 경합이 심해져 처리량이 오히려 줄 수 있습니다. DB 서버 코어 수와 쿼리 특성을 고려해 적정 크기를 잡아야 합니다.

Q3

스프링 부트가 기본으로 사용하는 커넥션 풀 구현체는?

"히카리"는 일본어로 "빛"이라는 뜻입니다.
Spring Boot 2.0부터 기본 커넥션 풀은 HikariCP입니다. 가볍고 빠르며, maximum-pool-size 같은 설정으로 풀 크기를 조정합니다.