PostgreSQL이 'too many connections'를 던지는 진짜 이유: 커넥션 풀링과 PgBouncer

🤖 AI Summary
운영 중인 PostgreSQL이 sorry, too many clients already 에러를 던지면, 가장 먼저 떠오르는 해결책은 max_connections를 올리는 것입니다. 그런데 이건 대개 함정입니다. PostgreSQL은 연결 하나마다 별도 프로세스를 띄우는 구조라, 한도를 키우면 그만큼 프로세스와 공유 메모리 부담이 함께 늘고, 이 값은 서버를 재시작해야만 바뀝니다. 진짜 해법은 커넥션 풀링입니다. 수천 개의 클라이언트 연결을 소수의 서버 연결로 수렴시켜 재사용하는 것이죠. 대표적인 도구가 PostgreSQL 전용 경량 풀러 PgBouncer이고, 핵심은 session·transaction·statement 세 가지 풀링 모드의 트레이드오프입니다. 효율이 높은 transaction 모드는 세션 상태에 의존하는 기능을 포기해야 하므로, 우리 애플리케이션이 무엇을 쓰는지부터 알아야 합니다. 이 글은 그 구조와 선택 기준을 1차 출처로 정리합니다.
블로그 목차
연결을 늘리는 게 왜 해결책이 아닌가
데이터베이스가 sorry, too many clients already를 던지는 순간, 손이 가장 먼저 향하는 곳은 max_connections입니다. 한도가 모자라니 올리면 되지 않느냐는 거죠. 하지만 그 전에 PostgreSQL이 연결을 어떻게 다루는지를 알아야 합니다.
PostgreSQL 공식 문서는 서버가 연결마다 새 프로세스를 fork한다고 설명합니다. 클라이언트가 접속할 때마다 전담 서버 프로세스가 하나씩 생기는 구조죠. 그래서 연결 수가 곧 프로세스 수이고, max_connections를 키우면 그 값에 비례해 공유 메모리 같은 자원 할당도 늘어납니다. 게다가 이 설정은 서버 시작 시에만 바꿀 수 있어, 트래픽이 몰린 그 순간 즉시 늘릴 수도 없습니다. max_connections 기본값은 보통 100이며, 시스템 자원에 따라 더 낮을 수도 있습니다.
정리하면 "연결을 더 받게 한도를 올린다"는 대응은, 부담을 줄이는 게 아니라 같은 부담을 더 키우는 쪽에 가깝습니다. 순간적으로 연결이 몰리는 문제라면 접근을 바꿔야 합니다.
커넥션 풀링: 소수의 연결을 재사용한다
해법의 핵심은 간단합니다. 클라이언트가 매번 새 연결을 맺고 끊는 대신, 미리 열어 둔 소수의 서버 연결을 여러 클라이언트가 돌려쓰게 하는 것입니다. 이 역할을 하는 중간 계층이 커넥션 풀러입니다.
PostgreSQL 진영의 대표 도구가 PgBouncer입니다. 공식 소개는 "PostgreSQL을 위한 경량 커넥션 풀러"이고, 목적은 새 연결을 여는 데 드는 성능 부담을 낮추는 것입니다. 애플리케이션은 PgBouncer를 PostgreSQL 서버처럼 바라보고 접속하며, PgBouncer가 실제 서버 연결을 새로 만들거나 기존 연결을 재사용합니다.
구조의 묘미는 두 숫자가 분리되어 있다는 점입니다. 받아들이는 클라이언트 연결 수(max_client_conn)와 실제로 PostgreSQL에 여는 서버 연결 수(default_pool_size)가 따로 관리됩니다. 그래서 클라이언트는 수천 개라도, 뒤편 서버 연결은 수십 개로 묶어 둘 수 있습니다.

PgBouncer의 세 가지 풀링 모드
PgBouncer의 성격을 결정하는 핵심 설정이 pool_mode입니다. 서버 연결을 언제 풀로 돌려주느냐에 따라 세 가지로 나뉩니다.
session pooling: 가장 보수적인 방식입니다. 클라이언트가 연결되어 있는 동안 서버 연결 하나가 그 클라이언트에게 통째로 배정됩니다. 호환성은 가장 좋지만, 연결을 오래 점유해 효율은 낮습니다.transaction pooling: 서버 연결이트랜잭션 동안에만클라이언트에 배정되고, 트랜잭션이 끝나면 곧바로 풀로 돌아갑니다. 적은 서버 연결로 많은 클라이언트를 받을 수 있어 효율이 높고, 가장 널리 쓰입니다.statement pooling: 가장 공격적인 방식으로, transaction pooling에 한 가지 제약을 더합니다.여러 statement로 이루어진 트랜잭션을 금지합니다.

transaction pooling의 함정: 깨지는 기능들
효율 때문에 transaction pooling을 고를 때 반드시 알아야 할 점이 있습니다. 공식 문서는 이 모드가 클라이언트의 세션 기대를 "의도적으로(by design)" 깨뜨린다고 분명히 적습니다. 서버 연결이 트랜잭션마다 다른 클라이언트에게 넘어가므로, 한 세션에 걸쳐 상태를 유지해야 하는 기능이 어긋나는 것이죠.
공식 문서가 transaction pooling에서 동작하지 않는다고 표시한 기능에는 다음이 있습니다.
SET/RESET같은 세션 변수 설정LISTEN(NOTIFY는 정상 동작)WITH HOLD커서세션 레벨
PREPARE/DEALLOCATE세션 레벨 advisory lock
한 가지 자주 생기는 오해를 짚겠습니다. "transaction pooling에서는 prepared statement를 못 쓴다"는 말은 정확하지 않습니다. 세션 레벨 PREPARE는 안 되지만, 프로토콜 레벨 prepared statement는 PgBouncer의 max_prepared_statements를 0이 아닌 값으로 설정하면 transaction pooling에서도 지원됩니다. 즉 "무조건 불가"가 아니라 "설정에 따라 조건부로 가능"이 맞습니다.
그래서 모드 선택의 출발점은 효율이 아니라 우리 애플리케이션이 이런 세션 기능에 의존하는지입니다. 의존한다면 session pooling을 쓰거나, 애플리케이션이 해당 기능을 쓰지 않도록 맞춰야 합니다.
그래서 어떻게 적용하나
too many connections를 마주했을 때, 한도를 올리기 전에 다음을 순서대로 점검하는 편이 좋습니다.
한도 인상보다 풀링 먼저: max_connections를 올리는 건 자원을 더 쓰는 일이고 재시작도 필요합니다. 연결이 몰리는 문제라면 커넥션 풀러로 서버 연결을 재사용하는 쪽을 먼저 검토합니다.애플리케이션 기능 점검 후 모드 선택: 세션 변수, LISTEN, advisory lock, 세션 prepared statement 등을 쓰는지 확인합니다. 쓴다면 session pooling, 아니라면 효율 높은 transaction pooling이 후보입니다.풀 크기는 대기 지표로 조정: 풀을 크게 잡을수록 좋은 게 아닙니다. PgBouncer가 제공하는 큐 대기 시간 지표가 계속 늘면 서버 과부하이거나 풀이 작다는 신호이니, 그 값을 보며 적정선을 찾습니다.이중 풀링 주의: 애플리케이션 자체 커넥션 풀과 PgBouncer를 함께 쓴다면, 앱 인스턴스 수와 앱 풀 크기가 곱해져 실제 서버 연결 수가 예상보다 불어날 수 있으니, 그 총량을 함께 계산합니다.
참고로 설정 기본값(예: default_pool_size, max_client_conn)은 PgBouncer 버전에 따라 다를 수 있으므로, 운영 전에 사용 중인 버전의 설정 문서를 한 번 확인하는 것이 안전합니다.
이것만 기억하세요
PostgreSQL은 연결마다 프로세스를 띄우는 구조라, too many connections의 해법은 max_connections 인상이 아니라 커넥션 풀링입니다. PgBouncer로 많은 클라이언트를 소수의 서버 연결로 모으되, transaction 모드는 효율이 높은 대신 일부 세션 기능을 포기해야 합니다.
자주 묻는 질문 (FAQ)
Q. max_connections를 늘리면 too many connections가 해결되나요?
근본 해결책은 아니에요. PostgreSQL은 연결마다 별도 프로세스를 띄우는 구조라, max_connections를 키우면 프로세스와 공유 메모리 등 자원 할당도 함께 늘어납니다. 게다가 이 값은 서버 시작 시에만 바꿀 수 있어요. 연결이 순간적으로 몰리는 문제라면, 한도를 올리기보다 커넥션 풀러로 소수의 서버 연결을 여러 클라이언트가 재사용하게 하는 편이 안전합니다.
Q. PgBouncer의 세 가지 풀링 모드는 어떻게 다른가요?
서버 연결을 언제 풀로 돌려주느냐가 달라요. session은 클라이언트 연결이 끊길 때까지 점유하고, transaction은 트랜잭션이 끝나면 바로 반환하며, statement는 각 statement 후 반환합니다(여러 statement 트랜잭션 금지). 적은 서버 연결로 많은 클라이언트를 받는 효율은 transaction과 statement가 높지만, 그만큼 세션 상태에 의존하는 기능을 쓸 수 없게 됩니다.
Q. transaction pooling을 쓰면 무엇이 안 되나요?
공식 문서는 transaction pooling이 세션 기대를 의도적으로 깨뜨린다고 명시해요. SET/RESET, LISTEN, WITH HOLD 커서, 세션 레벨 PREPARE/DEALLOCATE, 세션 레벨 advisory lock이 동작하지 않습니다. 다만 프로토콜 레벨 prepared statement는 max_prepared_statements를 0이 아닌 값으로 설정하면 지원되니, '무조건 못 쓴다'는 정확하지 않아요.
Q. PgBouncer는 어떤 데이터베이스에서 쓰나요?
이름 그대로 PostgreSQL 전용 경량 커넥션 풀러예요. 애플리케이션은 PgBouncer를 PostgreSQL 서버처럼 바라보고 접속하고, PgBouncer가 실제 서버 연결을 새로 만들거나 기존 연결을 재사용합니다. MySQL 등 다른 데이터베이스는 각자의 풀링 방식을 따로 검토해야 해요.
Q. 풀 크기는 클수록 좋은가요?
아니에요. 서버 연결 풀을 무작정 키우면 결국 PostgreSQL의 프로세스 수가 늘어 같은 부담으로 돌아옵니다. PgBouncer는 큐에서 가장 오래 기다린 클라이언트의 대기 시간을 지표로 주는데, 이 값이 계속 늘면 서버 과부하이거나 풀이 너무 작다는 신호예요. 풀 크기는 그 지표를 보며 적정선을 찾는 값이지, 클수록 좋은 값이 아닙니다.



