PostgreSQL 연결 풀 20개는 어디서 정할까: HikariCP와 max_connections 구분
Spring Boot에서 연결 풀을 20개로 설정했다고 해서 PostgreSQL 전체 연결이 20개로 제한되는 것은 아니다. HikariCP의 설정은 그 풀 하나에 적용된다. 앱을 세 개 띄우면 같은 DB에 세 개의 풀이 붙을 수 있고, 읽기·쓰기 풀이나 배치 프로세스가 추가되면 더 합산해야 한다.
반대로 DB에 빈 연결 슬롯이 있어도 특정 앱의 풀은 먼저 고갈될 수 있다. 두 상황을 구분하지 않고 숫자부터 올리면 연결 거절이 사라지는 대신 DB 대기와 지연이 늘어날 수 있다.
이 글은 2026-10-16 공식 문서를 확인했다. HikariCP 동작은 Java 17.0.20·HikariCP 6.3.0·H2 2.3.232로 로컬 재현했다. PostgreSQL 설정과 SQL은 18 문서 기준이며 실제 PostgreSQL 통합 실행이나 성능 측정은 포함하지 않았다. 사용 버전은 실험을 고정하기 위한 것으로 최신 버전 권고가 아니다.
풀 크기와 DB 연결 상한은 설정 위치가 다르다
| 설정 | 적용 범위 | 의미 |
|---|---|---|
HikariCP maximumPoolSize |
Hikari 풀 하나 | 사용 중인 연결과 유휴 연결을 합친 최대 크기 |
PostgreSQL max_connections |
PostgreSQL 서버 인스턴스 | 서버가 받아들일 동시 연결 상한 |
역할 CONNECTION LIMIT |
지정한 DB 역할 | 그 역할의 연결 제한 |
데이터베이스 CONNECTION LIMIT |
지정한 데이터베이스 | 그 DB에 대한 연결 제한 |
계정을 새로 만드는 행위 자체가 애플리케이션 연결 풀을 만들어 주지는 않는다. 앱은 DataSource/풀을 구성하고, PostgreSQL은 접속을 받아 제한을 적용한다. 역할·DB 제한도 있으므로 max_connections만 보고 모든 접속 거절을 설명할 수는 없다. PostgreSQL ALTER ROLE, ALTER DATABASE
Spring Boot가 자동 구성하는 단일 Hikari DataSource라면 설정 예시는 다음과 같다. 20은 설명용 값이며 운영 권장값이 아니다.
spring:
datasource:
url: ${DB_URL}
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
pool-name: main-db-pool
maximum-pool-size: 20
Spring Boot SQL 문서는 Hikari 관련 옵션을 spring.datasource.hikari.*로 설정하는 구성을 설명한다. 직접 여러 DataSource 빈을 만들었다면 각 빈에 바인딩한 설정 경로를 확인해야 한다. 위 YAML 한 군데를 바꿨다고 모든 풀이 바뀌는 것은 아니다.
HikariCP에서 minimumIdle을 별도로 지정하지 않으면 기본적으로 maximumPoolSize와 같아 고정 크기 풀처럼 동작하도록 구성된다. 최소 유휴 수를 따로 정할지는 연결 생성 비용과 실제 부하를 보고 결정한다. 숫자 20만 보고 현재 항상 20개가 사용 중이라고 해석해서도 안 된다. HikariCP 공식 설정
같은 DB로 가는 모든 풀을 합산한다
계산 단위는 앱 이름이나 JDBC URL 문자열이 아니라 최종적으로 연결되는 PostgreSQL 서버다. URL 두 개가 같은 서버의 다른 데이터베이스를 가리켜도 서버 전체 상한은 공유한다.
먼저 다음을 적는다.
- 같은 DB 서버에 접근하는 앱·배치·관리 도구
- 프로세스별 실제 Hikari 풀 개수와 각 최대 크기
- 평시 인스턴스 수와 배포 중 동시에 살아 있는 최대 수
- 읽기 서버 장애 시 쓰기 서버로 돌아오는 연결
- 모니터링·마이그레이션·운영 접속에 남길 여유
읽기 풀과 쓰기 풀이 서로 다른 PostgreSQL 서버로 가면 각 서버에서 따로 계산한다. 장애 시 두 풀이 하나의 서버로 향할 수 있다면 그 상황도 별도 표로 만든다. 라우팅 구조는 Spring Boot PostgreSQL 읽기·쓰기 분리에서 확인할 수 있다.
일반 권한 앱이 DB에 직접 접속하는 구성의 보수적인 예산은 다음처럼 잡을 수 있다.
일반 역할 연결 영역 = max_connections
- superuser_reserved_connections
- reserved_connections
앱 풀에 배정할 예산 = 일반 역할 연결 영역
- 기타 연결 예산
- 추가 운영 여유
앱 풀 최대 합계 = Σ(동시 인스턴스 수 × 해당 풀의 최대 크기)
이 계산은 슬롯 부족을 예방하기 위한 상한 검토다. 처리량이 가장 높은 풀 크기를 찾는 공식은 아니다. 예약 영역을 제외한 뒤 차감하는 기타 연결·추가 여유는 일반 영역에서 따로 확보하는 몫이며, 예약 슬롯과 중복 집계하지 않도록 표의 의미를 정한다.
설명용 숫자로 계산해 보기
아래 수치는 실제 서버 관측값이 아니다.
| 가정 | 연결 수 |
|---|---|
| max_connections | 100 |
| superuser 예약 | 3 |
| 별도 reserved_connections | 5 |
| 일반 영역의 기타 접속 예산 | 12 |
| 일반 영역의 추가 운영 여유 | 10 |
| 앱 풀에 배정할 예산 | 70 |
평시 앱 3개가 각각 풀 20개면 최대 합계는 60이다. 배포 중 앱 한 개가 추가되어 총 4개가 동시에 존재하면 80으로 올라가 예산 70을 초과한다. 평시 잔여 예산은 10이지만 배포 시에는 10이 부족하다.
항상 풀을 전부 채우는 것은 아니어도 최대로 늘어날 수 있는 조건을 먼저 계산해야 한다. 붙어 있는 세션의 순간 개수만 보고 배포 여유가 있다고 판단하면 피크를 놓칠 수 있다.
예약 연결은 앱이 더 쓰라고 남겨 둔 공간이 아니다
PostgreSQL 18에는 superuser용 예약과 pg_use_reserved_connections 권한을 가진 역할용 예약이 구분돼 있다. 일반 앱은 두 예약 영역을 쓸 수 있다고 계산하지 않는다. 접속 실패를 피하려고 앱 계정을 superuser로 바꾸는 방식도 사용하지 않는다.
max_connections와 이 예약 설정들은 서버 시작 시 적용된다. 연결 상한을 높이면 PostgreSQL이 그 값을 바탕으로 잡는 공유 메모리 등 일부 자원도 늘어난다. 따라서 설정 파일 수정과 reload만으로 적용됐다고 판단하거나, 메모리 여유 확인 없이 크게 올리지 않는다. PostgreSQL 연결 설정
현재 적용값은 다음처럼 확인할 수 있다. PostgreSQL 18 공식 뷰 정의에 맞춘 읽기 전용 SQL이다.
SELECT name, setting, context, source, pending_restart
FROM pg_settings
WHERE name IN (
'max_connections',
'reserved_connections',
'superuser_reserved_connections'
)
ORDER BY name;
setting은 현재 값이다. 파일을 바꿨지만 재시작이 필요한 변경이라면 pending_restart가 나타날 수 있다. context와 source를 함께 보면 설정이 적용되는 시점과 출처를 구분할 수 있다. pg_settings 공식 문서
풀 대기와 DB 접속 거절을 나눈다
| 증상 | 먼저 조사할 곳 |
|---|---|
Hikari Connection is not available, request timed out |
해당 풀의 대여 중 연결·대기 요청·연결 생성 실패 로그 |
PostgreSQL too many clients already 또는 예약 슬롯 관련 거절 |
서버의 연결 수·예약 영역·역할 및 DB 제한 |
| 연결을 얻은 뒤 쿼리가 오래 걸림 | 쿼리 실행·락·I/O·트랜잭션 범위 |
Hikari의 connectionTimeout은 풀에서 연결을 얻기 위해 기다리는 시간이다. SQL 실행시간 제한이나 JDBC 소켓 timeout과 동일한 설정이 아니다. 시간 초과가 났다는 사실만으로 누수를 확정할 수도 없다. 풀 내 연결을 오래 잡고 있거나, DB 연결 생성이 실패하거나, 순간 요청이 몰리는 경우를 나누어 봐야 한다.
실험에서는 풀 A와 B의 최대 크기를 각각 2로 두었다. 네 연결을 대여해 유지하면 DB에는 풀 연결 4개와 관측용 연결 1개가 보였다. 이 상태에서 A에 세 번째 연결을 요청하면 다음 오류가 발생했다.
pool-A - Connection is not available, request timed out after 500ms
(total=2, active=2, idle=0, waiting=0)
500ms는 재현을 짧게 하기 위한 테스트 설정이다. 운영 추천값이 아니며, 실제 시간은 실행 환경에서 달라질 수 있다. 끝의 waiting=0도 오류 시점의 스냅샷이므로 기다린 요청이 처음부터 없었다는 의미로 읽지 않는다. A에 빌린 연결 하나를 반환하자 다시 대여할 수 있었고 같은 H2 세션 ID가 재사용됐다.
이 실험은 HikariCP와 H2 메모리 DB에서 수행했다. PostgreSQL이 예약 슬롯에서 실제로 어떤 메시지로 거절하는지까지 재현했다는 의미는 아니다.
PostgreSQL의 idle과 풀의 idle은 같지 않다
애플리케이션이 연결을 빌린 뒤 외부 API 응답을 기다리면, 풀은 그 연결을 대여 중으로 셀 수 있다. 반면 그 순간 DB가 SQL 명령을 기다리는 상태일 수 있다. 따라서 DB의 idle 수치만 보고 앱에 반환 가능한 연결이 충분하다고 판단하지 않는다.
현재 세션은 다음처럼 분류한다. 다른 사용자 세션의 상세 정보는 조회 권한에 따라 제한될 수 있으므로 승인된 모니터링 계정으로 실행한다.
SELECT datname, usename, application_name, state,
count(*) AS connections
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY datname, usename, application_name, state
ORDER BY connections DESC;
관측 세션 자신도 집계될 수 있다. 이 조회는 클라이언트 연결 분포를 보는 것이며, 모든 종류의 백엔드와 내부 예약 조건을 계산하는 정밀한 잔여 슬롯 계량기는 아니다.
idle in transaction은 트랜잭션을 연 상태로 다음 명령을 기다리는 경우다. 단순 idle과 구분해서 조사한다.
SELECT pid, application_name, state, wait_event_type, wait_event,
clock_timestamp() - xact_start AS transaction_age
FROM pg_stat_activity
WHERE backend_type = 'client backend'
AND state LIKE 'idle in transaction%'
ORDER BY xact_start;
상태와 대기 이벤트는 서로 다른 축이다. active여도 락 등을 기다릴 수 있다. 공식 통계 뷰 문서와 mLog 모니터링 쿼리를 함께 참고한다. 세션 수를 줄이려고 모든 idle 세션을 일괄 종료하기보다 소유 앱과 트랜잭션 경계를 확인한다.
풀을 늘리기 전에 오래 잡는 이유를 찾는다
연결이 부족하다고 해서 풀이 작기만 한 것은 아니다. 느린 SQL, 락 대기, 긴 트랜잭션, 연결 반납 누락을 먼저 확인한다. 최근 실행시간 증가를 볼 때는 pg_stat_statements 스냅샷 차이가 후속 진단에 도움이 된다.
HikariCP 공식 풀 크기 안내도 단순히 풀을 크게 하면 빨라진다고 설명하지 않는다. 연결 예산에 들어오는 후보값을 정한 뒤, 동일한 부하에서 요청 지연·풀 대기·DB CPU·I/O·락을 비교해야 한다. 사용자 수나 CPU 개수 하나만으로 이 서비스의 최적값을 확정할 수 없다.
PgBouncer가 중간에 있다면 계산 경계가 하나 더 생긴다. 앱→PgBouncer의 클라이언트 연결과 PgBouncer→PostgreSQL의 서버 연결을 나눠야 한다. max_client_conn을 DB 연결 수로 읽거나 default_pool_size를 모든 사용자·DB를 통틀어 단 하나의 상한이라고 해석하지 않는다. 풀 구성과 모드에 따라 달라지므로 PgBouncer 설정 문서를 별도로 대조한다.
이번에 검증한 범위
첨부 자료에는 HikariCP 로컬 재현 코드, 연결 예산 계산기, PostgreSQL 읽기 전용 SQL을 담았다.
| 검증 | 결과 |
|---|---|
| HikariCP + H2 | 독립 풀 2개, 연결 합산, 대기 timeout, 반납 후 재사용, 대여 종료, 유휴 연결 유지 관찰의 6항목 통과 |
| 연결 예산 계산 | 평시·배포 중 증설·복수 풀·경계값·소규모 예약·잘못된 입력의 6항목 통과 |
| PostgreSQL 점검 SQL | PostgreSQL 18 공식 문서 대조. 이번 실행 환경에서는 서버를 구동하지 못해 실행 검증은 하지 않음 |
풀 20개를 늘릴지는 이 숫자가 어디에 적용되는지부터 확인한 뒤 결정한다. 같은 DB를 향하는 풀과 배포 중 인스턴스를 합산하고, 실제 대기가 앱 풀에서 생기는지 DB 안에서 생기는지 나눠 보면 다음 변경의 근거가 분명해진다.