서비스 사용자가 급증하거나 트래픽이 몰릴 때 갑자기 서비스 전체가 먹통이 되면서 FATAL: sorry, too many clients already라는 PostgreSQL 에러 로그를 마주해본 적이 있을 것이다. 백엔드 서버는 정상인데 데이터베이스 연결이 거부되어 모든 API 요청이 500 에러를 뿜어내는 아찔한 상황이다.
다만 대부분의 개발자들은 이 에러를 마주했을 때 원인을 제대로 파악하지 않고 단순히 postgresql.conf 파일의 max_connections 수치만 계속 높이는 임시방편을 선택한다. 이번에는 이 에러가 발생하는 근본적인 원인과 단순 수치 변경이 가져오는 위험성, 그리고 PgBouncer 커넥션 풀러(Connection Pooler)를 활용해 문제를 완벽하게 해결하는 방법을 정리해서 소개하겠다.
PostgreSQL은 클라이언트(웹 서버, WAS 등)가 연결을 요청할 때마다 새로운 프로세스를 생성(Fork)하여 처리하는 구조를 가지고 있다. 프로세스 기반 아키텍처이기 때문에 하나의 커넥션이 생성될 때마다 일정량의 메모리와 CPU 자원이 소모된다.
데이터베이스 설정 파일에는 서버의 자원 고갈을 막기 위해 동시에 연결할 수 있는 최대 클라이언트 수를 제한하는 max_connections 파라미터가 존재한다. 백엔드 애플리케이션의 프로세스 수가 늘어나거나, 트래픽 폭주로 커넥션 생성이 폭증하여 이 설정값에 도달하면 PostgreSQL은 더 이상의 연결을 거부하고 FATAL: sorry, too many clients already 에러를 반환한다.
에러가 났다고 해서 max_connections를 100에서 1000으로 무작정 늘리는 것은 매우 위험하다. PostgreSQL의 프로세스당 메모리 사용량(work_mem, maintenance_work_mem 등)이 곱해져 전체 데이터베이스 메모리가 고갈(OOM Kicker 작동)되거나, 컨텍스트 스위칭(Context Switching) 비용이 급증하여 오히려 전체 쿼리 처리 성능이 곤두박질치기 때문이다.
따라서 데이터베이스 차원의 커넥션 수 제한을 무작정 늘리기보다는, 애플리케이션과 DB 사이에 커넥션 버퍼 역할을 해주는 **PgBouncer** 같은 전문 커넥션 풀러를 도입하는 것이 실무에서의 정석적인 해결법이다.
| 해결 방식 | 장점 | 단점 / 위험성 |
|---|---|---|
| max_connections 수치 증대 | 설정 파일 한 줄 수정으로 즉시 적용 가능 | DB 메모리 고갈(OOM) 위험, 컨텍스트 스위칭 증가로 전체 성능 저하 |
| 애플리케이션 풀 크기 줄이기 | DB 과부하 방지 가능 | 트래픽 폭주 시 백엔드 애플리케이션에서 대기 타임아웃 발생 |
| PgBouncer 도입 (권장) | 수천 개의 클라이언트 연결을 적은 수의 DB 커넥션으로 효율적 재사용 | 별도의 미들웨어 프로세스 관리 및 네트워크 레이어 추가 |
문제 해결을 위한 두 가지 접근 방식을 코드로 확인해보자.
데이터베이스에 오랫동안 방치된 연결(Idle Connections)이나 트랜잭션이 맺어진 채 멈춰있는 연결을 자동으로 정리하도록 설정한다.
✗ 잘못된 해결책 (단순히 max_connections만 거대하게 설정)
# postgresql.conf
# 메모리 계산 없이 수치만 무작정 증대
max_connections = 2000✓ 올바른 설정 (적절한 커넥션 수 조정 및 고사한 커넥션 자동 정리 타임아웃 추가)
# postgresql.conf
max_connections = 200
# 트랜잭션 수립 후 방치된 세션 자동 종료 (30초)
idle_in_transaction_session_timeout = 30000
# 쿼리 수행 없이 방치된 세션 자동 종료 (60초)
idle_session_timeout = 60000
PgBouncer를 설치하고 트랜잭션 풀링(Transaction Pooling) 모드로 설정하면, 수천 명의 클라이언트 접속을 수십 개의 DB 실제 커넥션으로 압축 처리할 수 있다.
✓ PgBouncer 구성 파일 (pgbouncer.ini)
[databases]
# 클라이언트가 접근할 DB 이름 = 실제 DB 접속 정보
app_db = host=127.0.0.1 port=5432 dbname=app_db
[pgbouncer]
listen_addr = *
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
# 핵심 설정: 트랜잭션 단위로 커넥션을 재사용
pool_mode = transaction
# 클라이언트에 허용할 최대 동시 연결 수
max_client_conn = 5000
# 실제 PostgreSQL DB에 연결할 커넥션 수 제한
default_pool_size = 50
min_pool_size = 10
reserve_pool_size = 5출력 결과 및 확인 방법:
-- PgBouncer 관리 콘솔 접속 (psql -h 127.0.0.1 -p 6432 -U postgres pgbouncer)
SHOW POOLS;
-- 출력 예시:
-- database | user | cl_active | cl_waiting | sv_active | sv_idle | sv_used
-- ----------+----------+-----------+------------+-----------+---------+---------
-- app_db | app_user | 120 | 0 | 18 | 32 | 0위와 같이 120개의 클라이언트 접속(cl_active)이 실제 DB 커넥션 18개(sv_active)만으로 안정적으로 처리되는 것을 볼 수 있다.
PgBouncer를 도입할 때 가장 흔히 범하는 실수는 풀링 모드(Pool Mode) 선택을 잘못하는 것이다.
✗ 잘못된 설정: Prepared Statement나 Named Cursor를 많이 사용하는 서비스에서 pool_mode = transaction을 사용하면서 세션 상태를 의존하는 경우
✓ 올바른 조치: 트랜잭션 풀링 모드에서는 세션 레벨의 설정(예: SET TIMEZONE)이나 Prepared Statement가 커넥션 재사용 시 다른 요청에 영향을 주거나 에러를 발생시킬 수 있다. Application단에서 Prepared Statement 사용을 끄거나, PgBouncer 대신 Prepared Statement를 지원하는 버전(PgBouncer 1.21 이상)의 준비된 문장 캐싱 기능을 활성화해야 한다.
PostgreSQL의 FATAL: sorry, too many clients already 에러는 DB 서버 자원의 한계를 알리는 경고 신호다. max_connections 파라미터를 무턱대고 늘리는 대증요법은 더 큰 서버 다운 재앙을 불러올 수 있다는 점을 잊지 말자. 이 글의 PgBouncer 트랜잭션 풀링 설정 가이드를 참고해 미들웨어 구조를 개편하면, 동일한 하드웨어 스펙에서도 수배 이상의 트래픽을 거뜬히 버텨내는 견고한 백엔드 환경을 얻을 수 있을 것이다.