커넥션 풀(Connection Pool)

ddee·2026년 7월 13일

문제: DB 연결은 비싸다

애플리케이션이 DB에 쿼리를 날리려면 먼저 연결(Connection)을 맺어야 한다. 그런데 이 과정이 생각보다 무겁다.

1. TCP 소켓 연결 (3-way handshake)
2. DB 인증 (아이디/비밀번호 검증)
3. DB 서버 쪽에서 세션용 메모리/프로세스 할당
4. (TLS를 쓰면) 암호화 핸드셰이크까지

이게 요청 한 번에 수 밀리초~수십 밀리초가 걸린다. 쿼리 자체는 1ms에 끝나는데 연결 맺는 데 20ms가 걸린다.

커넥션 풀 없이 매 요청마다 이렇게 하면:

요청 → 연결 생성(느림) → 쿼리 실행 → 연결 종료 → 응답
요청 → 연결 생성(느림) → 쿼리 실행 → 연결 종료 → 응답
... (매번 반복)

초당 수백~수천 요청이 들어오는 서버라면 연결 생성/해제만으로 서버와 DB가 모두 지쳐버린다.

해결: 연결을 미리 만들어놓고 재사용하자

커넥션 풀은 애플리케이션 시작 시 DB 연결을 여러 개 미리 만들어서 "풀(수영장/저장소)"에 담아두고, 필요할 때 빌려 쓰고 반납하는 방식

[애플리케이션]                    [커넥션 풀]                [DB]
                            ┌──────────────┐
 요청 스레드 1 ──"빌려줘"──▶     │ 🔌 연결 #1    │ ◀━━ 미리 맺어둔
 요청 스레드 2 ──"빌려줘"──▶     │ 🔌 연결 #2    │      실제 연결들
 요청 스레드 3 ──대기...──▶     │ 🔌 연결 #3    │ ━━━▶ PostgreSQL
                            │ (전부 사용중)   │
                            └──────────────┘

동작 흐름:

1. 앱 시작 시: 풀이 연결을 N개(예: 10개) 미리 생성해서 보관
2. 요청이 들어오면: 스레드가 풀에서 유휴(idle) 연결을 하나 빌림 — 새로 만드는 게 아니라 이미 있는 걸 꺼내니 거의 0ms
3. 쿼리 실행 후: connection.close()를 호출하는데, 진짜로 끊는 게 아니라 풀에 반납됨 (프록시가 가로챔)
4. 다음 요청이 그 연결을 또 빌려 씀

풀이 가득 차면?

연결이 10개인데 동시에 15개 요청이 들어오면, 나머지 5개는 대기. 누군가가 반납할 때까지 기다리다가, 제한 시간(HikariCP 기본 30초) 안에 못 빌리면 Connection is not available 예외가 터진다. 이게 실무에서 자주 보는 "커넥션 풀 고갈" 장애

원인은 보통:

  • 트랜잭션이 너무 길다(연결을 오래 붙잡음)
  • 연결을 반납 안 하는 코드(커넥션 누수)
  • 풀 크기 대비 트래픽이 너무 많음

HikariCP

커넥션 풀을 구현한 라이브러리. 다른 구현체(Tomcat JDBC, DBCP2 등)도 있지만 HikariCP가 가장 빠르고 가벼워서 Spring Boot 2.0부터 기본 커넥션 풀로 채택됨. spring-boot-starter-data-jpa만 넣어도 자동으로 딸려옴.


풀 크기 제약, 데이터 베이스 한계

제약1: DB 서버의 최대 연결 수

가장 확실한 하드 리밋. DB 서버 자체가 받아줄 수 있는 연결 수에 상한이 있다.

  • PostgreSQL: max_connections 기본 100
  • MySQL: max_connections 기본 151

이걸 넘는 요청은 DB가 거부한다.

여기서 중요한 건 마이크로서비스가 여러 개면 합산.

user_service    : 풀 10개
match_service   : 풀 10개
chat_service    : 풀 10개
feed_service    : 풀 10개
assistant_service: 풀 10개
─────────────────────────
합계 50개가 DB에 연결됨

게다가 서비스를 스케일 아웃해서 인스턴스가 3대씩 뜨면? 50*3=150개

PostgresSQL 기본값 100을 이미 초과한다.
이런 경우 실무에서 배포 시 DB연결이 거부될 수 있다.

제약2: 연결 하나하나가 DB의 자원을 먹음

PostgreSQL은 연결 하나당 별도의 프로세스를 띄운다. 연결마다 수 MB의 메모리를 쓰고, 연결이 수백 개가 되면 DB 서버의 메모리와 CPU(컨텍스트 스위칭)가 낭비된다. 그래서 max_connections를 무작정 올리는 것도 답이 아니다.

제약3: 많다고 빨라지지 않음(오히려 느려짐)

이게 제일 직관에 반하는 부분. HikariCP 공식 문서가 강조하는 내용.
HikariCP 공식 문서가 강조하는 내용.

DB 서버의 CPU 코어가 8개라면, 실제로 동시에 처리할 수 있는 쿼리도 그 근처.
연결이 500개 있어봤자 8개만 일하고 나머지 492개는 DB 안에서 줄 서 있는 거. 오히려

  • 락 경합 증가
  • 컨텍스트 스위칭 오버헤드
  • 디스크 I/0 경합
    으로 전체 처리량이 떨어진다. "줄이 애플리케이션 앞에 서느냐, DB안에 서느냐"의 차이인데, DB 안에 서는 게 더 비싸다.

HikariCP가 제시하는 유명한 공식:

pool_size = (CPU 코어 수 × 2) + 유효 디스크 수

예를 들어 DB 서버가 4코어 + SSD 1개면 9~10개 정도. 그래서 HikariCP 기본값이 10인 거.

실제 벤치마크에서 연결 2,000개 보다 96개짜리 풀이 더 높은 TPS를 낸 사례도 유명하다.

제약4: 애플리케이션 쪽 지원

풀 크기만큼 앱 쪽에서도 소켓, 스레드 대기, 메모리를 쓴다. 그리고 애초에 톰캣 워커 스레드가 200개인데 풀이 500개면 의미가 없다(동시에 연결을 쓸 수 있는 주체가 최대 200개니까)

그럼 어떻게 정할까?

실무적인 순서는 아래와 같다.

1. 기본값(10)으로 시작 — 대부분의 서비스는 이걸로 충분
2. 모니터링 지표를 봄 — HikariCP는 active, idle, pending(대기 중인 스레드) 메트릭을 제공
3. pending이 자주 발생하면 그때 원인을 파악 — 풀이 작아서인지, 느린 쿼리/긴 트랜잭션이 연결을 오래 붙잡아서인지
4. 후자라면 풀을 늘리는 게 아니라 쿼리를 고쳐야 함 (풀 늘리기는 진통제일 뿐)
5. 늘릴 때도 서비스 수 × 인스턴스 수 × 풀 크기 < DB max_connections를 항상 계산

참고로 서비스/인스턴스가 정말 많아져서 연결 수가 감당이 안되면, PostgreSQL 앞단에 PgBouncer 같은 외부 커넥션 풀러를 두는 게 다음 단계의 해법. 여러 앱의 연결을 한 번 더 모아서 DB에는 적은 수의 연결만 유지해주는 중간 계층

정리하면: 설정은 자유지만, 상한은 DB의 max_connections이고, 최적값은 그보다 훨씬 작다.

  • 크게 잡는 것보다 작게 유지하면서 쿼리를 빠르게 만드는 게 정석.

0개의 댓글