데이터베이스 커넥션 풀링 (Database Connection Pooling): 추측을 멈추고 측정을 시작하라
요약
데이터베이스 커넥션 풀링을 단순 설정이 아닌 리소스 할당 관점에서 접근해야 함을 강조합니다. 적절한 풀 크기 설정과 커넥션 누수 방지를 위한 올바른 생명주기 관리 및 측정의 중요성을 다룹니다.
핵심 포인트
- 커넥션 풀은 무한한 수요 흡수가 아닌 트래픽 완화 도구임
- 기본값에 의존하지 말고 실제 워크로드에 맞춰 설정할 것
- 트랜잭션 중 외부 API 호출 등 긴 작업은 커넥션 점유를 유발함
- finally 블록을 사용하여 커넥션 누수를 반드시 방지할 것
- 지연 시간, 동시 요청 수, DB 제한치를 기반으로 측정할 것
대부분의 개발자들은 커넥션 풀링 (Connection Pooling)을 한 번 설정하면 끝나는 설정값으로 취급합니다. 드라이버를 추가하고, maxPoolSize를 10으로 설정한 뒤, 트래픽 급증 상황에서도 데이터베이스가 잘 버텨주기만을 바랍니다. 하지만 이렇게 해서 잘 되는 경우는 드뭅니다. 커넥션 풀링 (Connection Pooling)은 마법이 아닙니다. 그것은 리소스 할당 (Resource Allocation)입니다. 측정하지 않는다면, 여러분은 운영 환경이 무너질 때까지 그저 추측만 하고 있는 것입니다.
문제는 기본값 (Defaults)에서 시작됩니다. 모든 ORM, 쿼리 빌더 (Query Builder), 그리고 클라우드 프록시 (Cloud Proxy)는 실제 워크로드 (Workload)와는 전혀 맞지 않는 보수적인 제한치를 가지고 출시됩니다. 10개의 풀 (Pool)은 50명의 동시 사용자를 충분히 처리할 수 있지만, 백그라운드 워커 (Background Workers)가 배치 업데이트 (Batch Updates)를 위해 데이터베이스를 몰아치기 시작하면 숨이 막힙니다. 반대로, 데이터베이스의 max_connections 제한을 이해하지 못한 채 100으로 설정하는 것은 피크 로드 (Peak Load) 동안 커넥션 고갈 (Connection Exhaustion)을 보장하는 것과 다름없습니다.
여러분은 커넥션 풀 (Connection Pool)을 유한 큐 (Bounded Queue)처럼 다뤄야 합니다. 커넥션 풀은 무한한 수요를 흡수하기 위해서가 아니라, 급증하는 트래픽을 완화하기 위해 존재합니다. 큐 (Queue)가 가득 차면 요청은 차단되거나 실패합니다. 그것은 설계된 대로 작동하는 것입니다. 이러한 실패 모드 (Failure Mode)는 여러분의 아키텍처 (Architecture)가 어디에서 누수되고 있는지를 정확히 알려줍니다.
먼저 커넥션 생명주기 (Connection Lifecycle)를 매핑하는 것부터 시작하세요. 모든 요청은 커넥션을 빌려오고, 실행한 뒤, 즉시 반환해야 합니다. 오래 지속되는 트랜잭션 (Transactions)은 소리 없는 살인자입니다. 외부 API를 기다리거나 무거운 인메모리 변환 (In-memory Transformation)을 처리하는 동안 커넥션을 열어둔 상태로 유지한다면, 동시 요청이 발생할 때마다 유효한 풀 크기 (Pool Size)를 하나씩 줄이는 셈이 됩니다. 이것은 데이터베이스 문제가 아닙니다. 애플리케이션 설계 (Application Design) 문제입니다.
다음은 실제 환경에서 적절한 범위로 설정된 풀 (Pool)의 모습입니다. 래퍼 (Wrappers)도 없고, 숨겨진 상태 (Hidden State)도 없으며, 오직 명시적인 획득 (Acquisition)과 해제 (Release)만 존재합니다:
async function fetchUserWithOrders(userId) {
const client = await pool.connect();
try {
...
finally 블록에 주목하세요. 이것은 타협할 수 없는 필수 사항입니다. 만약 반환(release) 전에 에러가 발생하면, 풀(pool)에 누수(leak)가 발생합니다. 시간이 흐름에 따라 누수된 커넥션(connection)들이 쌓이게 되고, 결국 풀이 용량에 도달하여 애플리케이션에 타임아웃(timeout)이 발생하기 시작합니다. 모니터링 대시보드에는 높은 지연 시간(latency)이 표시되겠지만, 근본 원인은 항상 동일합니다. 바로 커넥션이 풀로 돌아오지 않고 있다는 것입니다.
풀의 크기를 올바르게 설정하려면 블로그 포스트의 권장 사항이 아닌 실제 메트릭(metrics)이 필요합니다. 세 가지 숫자가 필요합니다: 평균 쿼리 지연 시간(average query latency), 피크 동시 요청 수(peak concurrent requests), 그리고 데이터베이스의 하드 커넥션 제한(hard connection limit)입니다. 공식은 간단합니다. 풀 크기 = (ms 단위의 평균 지연 시간 / 1000) * 목표 QPS + 20% 버퍼. 만약 쿼리가 50ms가 걸리고 200 QPS가 필요하다면, 약 10개에서 12개의 커넥션이 필요합니다. 50개도 아니고, 100개도 아닙니다. 12개입니다.
PgBouncer나 RDS Proxy와 같은 클라우드 프록시(Cloud proxies)는 또 다른 복잡성을 더합니다. 이들은 애플리케이션과 데이터베이스 사이에 위치하여, 수천 개의 애플리케이션 커넥션을 소수의 실제 데이터베이스 커넥션으로 멀티플렉싱(multiplexing)합니다. 이는 유용하지만 지연 시간을 숨깁니다. 프록시 큐(proxy queue)가 쌓이면, 애플리케이션은 데이터베이스 장애처럼 보이는 타임아웃을 목격하게 됩니다. 하지만 그것은 데이터베이스 장애가 아닙니다. 프록시 포화(proxy saturation) 상태인 것입니다. 애플리케이션 풀을 설정할 때 프록시의 프런트엔드(frontend) 제한이 아닌, 백엔드(backend) 제한에 맞추십시오. 그렇지 않으면 병목 현상(bottleneck)을 상류(upstream)로 밀어낼 뿐입니다.
직감에 의존하여 풀 크기를 조정하는 것을 멈추십시오. 풀에 계측 도구(instrument)를 도입하십시오. 활성 커넥션(active connections), 유휴 커넥션(idle connections), 큐 깊이(queue depth), 그리고 대기 시간(wait time)을 추적하십시오. 큐 깊이에 대해 알림(alert)을 설정하십시오. 만약 요청이 커넥션을 얻기 위해 500ms 이상 대기하고 있다면, 풀 크기가 너무 작거나 쿼리가 너무 느린 것입니다. 먼저 쿼리를 수정하십시오. 느린 쿼리에 더 많은 커넥션을 추가하는 것은 단지 더 많은 동시 느린 쿼리를 발생시킬 뿐입니다.
커넥션 풀링(Connection pooling)은 기능(feature)이 아니라 인프라(infrastructure)입니다. 이는 지루해야 합니다. 만약 당신이 끊임없이 이를 조정하고 있다면, 당신의 애플리케이션이 커넥션당 너무 많은 작업을 수행하고 있다는 뜻입니다. 쿼리를 리팩터링(refactor)하십시오. 인덱스(index)를 추가하십시오. 빈번한 읽기(hot reads)는 캐싱(cache)하십시오. 그런 다음 현실에 맞게 풀 크기를 설정하십시오. 측정하십시오. 조정하십시오. 반복하십시오. 그 외의 모든 것은 그저 소음일 뿐입니다.
[

AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기