> ## Content Index
> Fetch the complete content index at: https://mlog.me/llms.txt
> Use this file to discover other available public pages before exploring further.

# PostgreSQL 연결 풀 20개는 어디서 정할까: HikariCP와 max_connections 구분
- URL: https://mlog.me/postgresql-hikaricp-pool-size-max-connections-budget/
- Published: 2026-10-16T07:08:06.000Z
- Updated: 2026-10-16T07:08:06.000Z
- Description: PostgreSQL max_connections와 HikariCP maximum-pool-size의 차이를 정리합니다. 앱 수·읽기/쓰기 풀·롤링 배포를 합산하고, 풀 대기와 DB 연결 거절을 구분하는 방법을 확인하세요.
- Author: mLog
- Tags: 데이터베이스, PostgreSQL, Spring Boot, HikariCP, 성능 최적화

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](https://www.postgresql.org/docs/18/sql-alterrole.html?ref=mlog.me), [ALTER DATABASE](https://www.postgresql.org/docs/18/sql-alterdatabase.html?ref=mlog.me)

Spring Boot가 자동 구성하는 단일 Hikari DataSource라면 설정 예시는 다음과 같다. **20은 설명용 값이며 운영 권장값이 아니다.**

```yaml
spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USER}
    password: ${DB_PASSWORD}
    hikari:
      pool-name: main-db-pool
      maximum-pool-size: 20

```

[Spring Boot SQL 문서](https://docs.spring.io/spring-boot/3.5/reference/data/sql.html?ref=mlog.me)는 Hikari 관련 옵션을 `spring.datasource.hikari.*`로 설정하는 구성을 설명한다. 직접 여러 DataSource 빈을 만들었다면 각 빈에 바인딩한 설정 경로를 확인해야 한다. 위 YAML 한 군데를 바꿨다고 모든 풀이 바뀌는 것은 아니다.

HikariCP에서 `minimumIdle`을 별도로 지정하지 않으면 기본적으로 `maximumPoolSize`와 같아 고정 크기 풀처럼 동작하도록 구성된다. 최소 유휴 수를 따로 정할지는 연결 생성 비용과 실제 부하를 보고 결정한다. 숫자 20만 보고 현재 항상 20개가 사용 중이라고 해석해서도 안 된다. [HikariCP 공식 설정](https://github.com/brettwooldridge/HikariCP?ref=mlog.me)

## 같은 DB로 가는 모든 풀을 합산한다

계산 단위는 앱 이름이나 JDBC URL 문자열이 아니라 **최종적으로 연결되는 PostgreSQL 서버**다. URL 두 개가 같은 서버의 다른 데이터베이스를 가리켜도 서버 전체 상한은 공유한다.

먼저 다음을 적는다.

1. 같은 DB 서버에 접근하는 앱·배치·관리 도구
2. 프로세스별 실제 Hikari 풀 개수와 각 최대 크기
3. 평시 인스턴스 수와 배포 중 동시에 살아 있는 최대 수
4. 읽기 서버 장애 시 쓰기 서버로 돌아오는 연결
5. 모니터링·마이그레이션·운영 접속에 남길 여유

읽기 풀과 쓰기 풀이 서로 다른 PostgreSQL 서버로 가면 각 서버에서 따로 계산한다. 장애 시 두 풀이 하나의 서버로 향할 수 있다면 그 상황도 별도 표로 만든다. 라우팅 구조는 [Spring Boot PostgreSQL 읽기·쓰기 분리](https://mlog.me/spring-boot-postgresql-read-write-splitting/)에서 확인할 수 있다.

일반 권한 앱이 DB에 직접 접속하는 구성의 보수적인 예산은 다음처럼 잡을 수 있다.

```text
일반 역할 연결 영역 = 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 연결 설정](https://www.postgresql.org/docs/18/runtime-config-connection.html?ref=mlog.me)

현재 적용값은 다음처럼 확인할 수 있다. PostgreSQL 18 공식 뷰 정의에 맞춘 읽기 전용 SQL이다.

```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 공식 문서](https://www.postgresql.org/docs/18/view-pg-settings.html?ref=mlog.me)

## 풀 대기와 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에 세 번째 연결을 요청하면 다음 오류가 발생했다.

```text
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` 수치만 보고 앱에 반환 가능한 연결이 충분하다고 판단하지 않는다.

현재 세션은 다음처럼 분류한다. 다른 사용자 세션의 상세 정보는 조회 권한에 따라 제한될 수 있으므로 승인된 모니터링 계정으로 실행한다.

```sql
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`과 구분해서 조사한다.

```sql
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`여도 락 등을 기다릴 수 있다. 공식 [통계 뷰 문서](https://www.postgresql.org/docs/18/monitoring-stats.html?ref=mlog.me)와 [mLog 모니터링 쿼리](https://mlog.me/postgresql-monitoring-queries/)를 함께 참고한다. 세션 수를 줄이려고 모든 idle 세션을 일괄 종료하기보다 소유 앱과 트랜잭션 경계를 확인한다.

## 풀을 늘리기 전에 오래 잡는 이유를 찾는다

연결이 부족하다고 해서 풀이 작기만 한 것은 아니다. 느린 SQL, 락 대기, 긴 트랜잭션, 연결 반납 누락을 먼저 확인한다. 최근 실행시간 증가를 볼 때는 [pg\_stat\_statements 스냅샷 차이](https://mlog.me/postgresql-pg-stat-statements-snapshot-delta-slow-queries/)가 후속 진단에 도움이 된다.

HikariCP 공식 [풀 크기 안내](https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing?ref=mlog.me)도 단순히 풀을 크게 하면 빨라진다고 설명하지 않는다. 연결 예산에 들어오는 후보값을 정한 뒤, 동일한 부하에서 요청 지연·풀 대기·DB CPU·I/O·락을 비교해야 한다. 사용자 수나 CPU 개수 하나만으로 이 서비스의 최적값을 확정할 수 없다.

PgBouncer가 중간에 있다면 계산 경계가 하나 더 생긴다. 앱→PgBouncer의 클라이언트 연결과 PgBouncer→PostgreSQL의 서버 연결을 나눠야 한다. `max_client_conn`을 DB 연결 수로 읽거나 `default_pool_size`를 모든 사용자·DB를 통틀어 단 하나의 상한이라고 해석하지 않는다. 풀 구성과 모드에 따라 달라지므로 [PgBouncer 설정 문서](https://www.pgbouncer.org/config.html?ref=mlog.me)를 별도로 대조한다.

## 이번에 검증한 범위

첨부 자료에는 HikariCP 로컬 재현 코드, 연결 예산 계산기, PostgreSQL 읽기 전용 SQL을 담았다.

| 검증                | 결과                                                               |
| ----------------- | ---------------------------------------------------------------- |
| HikariCP + H2     | 독립 풀 2개, 연결 합산, 대기 timeout, 반납 후 재사용, 대여 종료, 유휴 연결 유지 관찰의 6항목 통과 |
| 연결 예산 계산          | 평시·배포 중 증설·복수 풀·경계값·소규모 예약·잘못된 입력의 6항목 통과                        |
| PostgreSQL 점검 SQL | PostgreSQL 18 공식 문서 대조. 이번 실행 환경에서는 서버를 구동하지 못해 실행 검증은 하지 않음     |

풀 20개를 늘릴지는 이 숫자가 어디에 적용되는지부터 확인한 뒤 결정한다. 같은 DB를 향하는 풀과 배포 중 인스턴스를 합산하고, 실제 대기가 앱 풀에서 생기는지 DB 안에서 생기는지 나눠 보면 다음 변경의 근거가 분명해진다.

[HikariCP 연결 풀 재현 코드와 연결 예산 계산기HikariCP+H2 로컬 실험 6항목·연결 예산 계산 6항목. PostgreSQL 읽기 전용 SQL은 문서 대조만 했으며 실행 검증하지 않았습니다.postgresql-hikaricp-pool-repro-20261016.zip5 KBdownload-circle](https://mlog.me/content/files/2026/10/postgresql-hikaricp-pool-repro-20261016.zip "Download")