왜 Connection Timeout이 발생할까?
웹 애플리케이션에서 데이터베이스 커넥션은 한정된 자원입니다. 사용자가 몰리는 순간 또는 백그라운드 배치가 돌 때, 커넥션 요청이 폭증하면 커넥션 풀에서 즉시 빌려줄 수 없고 대기열에서 차례를 기다리게 됩니다. 대기 시간이 커넥션 acquire 타임아웃을 넘기면 바로 “Connection Timeout” 예외가 발생합니다. 여기에 네트워크 지연, 장기 실행 쿼리, 락 경합까지 겹치면 문제는 더 빈번하고 재현도 어렵습니다.
이 글에서는 Connection Timeout의 본질을 짚고, 커넥션 풀 설정을 중심으로 애플리케이션과 데이터베이스 전반의 성능을 최적화하는 구체적인 방법을 제공합니다. 프레임워크별 설정 예시, 진단 SQL, 모니터링 지표, 용량 산정 공식, 운영 체크리스트까지 실전에서 바로 적용할 수 있는 내용을 담았습니다.
Connection Timeout의 증상과 유형
- 애플리케이션 로그에 “Timeout waiting for a connection from pool”, “Connection acquisition timed out” 등의 오류가 주기적으로 발생
- 평균 응답 시간은 평소와 비슷하지만 p95/p99 응답 시간과 에러율이 급증
- API 특정 엔드포인트나 특정 시간대에만 집중적으로 에러 발생
- DB 측에서는 커넥션 수가 상한 근처에서 유지되거나 idle-in-transaction 세션이 축적됨
타임아웃은 보통 아래처럼 구분됩니다. 서로 다른 원인이므로 각각 설정과 진단이 필요합니다.
- 커넥션 획득 타임아웃: 풀에서 커넥션을 빌릴 때 대기 초과
- 연결(핸드셰이크) 타임아웃: DB 소켓 연결 자체가 실패/지연
- 소켓/읽기 타임아웃: 쿼리 결과를 기다리다 응답 없음
- 쿼리/문장 타임아웃: SQL 실행 시간이 특정 임계치를 넘음
- 트랜잭션 유휴 타임아웃: 트랜잭션 중에 아무 작업도 안 한 채 방치
근본 원인 파악: 어디서 병목이 생기나
Connection Timeout의 근본 원인은 대체로 다음 범주로 묶입니다.
- 풀 자체의 병목
- maximumPoolSize가 낮아 순간 트래픽을 수용 못함
- 커넥션 leak(빌리고 반납 안 함)으로 풀 고갈
- 검증 쿼리/validation이 너무 느리거나 과도함
- DB 서버 측 병목
- max_connections 소진, CPU/IO 포화, 디스크 대기
- 장기 실행 쿼리, 인덱스 미비, 풀스캔
- 락 경합(테이블/행 락), 데드락 재시도 부재
- 오토바큠 지연(PostgreSQL), InnoDB flush(MySQL) 등 내부 유지보수 부하
- 네트워크/인프라 문제
- DNS 지연, TLS 핸드셰이크 비용, NAT/로드밸런서 idle timeout
- 클라이언트/서버 간 MTU 문제, 패킷 손실
- 애플리케이션 설계 이슈
- N+1 쿼리, 비효율적 배치 처리, 불필요한 트랜잭션 범위 확대
- 동기 I/O 남발로 스레드가 놀면서 커넥션을 오래 점유
- 캐시 부재로 반복 조회가 DB를 과소비
진단 가이드: 무엇을 어떻게 볼 것인가
실전에서 유효한 진단 루틴을 제시합니다.
- 애플리케이션 레벨
- 풀 지표: active, idle, pending, timeout count
- p95/p99 connection wait time, acquire 실패 횟수
- 요청당 DB 시간 비중(trace, APM: New Relic, Datadog, Pinpoint 등)
- 에러 로그 샘플링: 특정 엔드포인트, 특정 DB 쿼리 패턴과 상관관계 확인
- 데이터베이스 레벨
-
PostgreSQL
- 현재 세션과 실행 중 쿼리
SELECT pid, usename, state, wait_event_type, wait_event, query, now() - query_start AS runtime FROM pg_stat_activity WHERE state <> 'idle' ORDER BY runtime DESC; - 락 대기
SELECT bl.pid AS blocked_pid, a.usename, a.query AS blocked_query, now() - a.query_start AS blocked_for, kl.pid AS locker_pid, ka.query AS locker_query FROM pg_locks bl JOIN pg_stat_activity a ON a.pid = bl.pid JOIN pg_locks kl ON bl.locktype = kl.locktype AND bl.database IS NOT DISTINCT FROM kl.database AND bl.relation IS NOT DISTINCT FROM kl.relation AND bl.page IS NOT DISTINCT FROM kl.page AND bl.tuple IS NOT DISTINCT FROM kl.tuple AND bl.classid IS NOT DISTINCT FROM kl.classid AND bl.objid IS NOT DISTINCT FROM kl.objid AND bl.objsubid IS NOT DISTINCT FROM kl.objsubid AND bl.pid <> kl.pid AND NOT bl.granted AND kl.granted JOIN pg_stat_activity ka ON ka.pid = kl.pid ORDER BY blocked_for DESC; - 슬로우 쿼리 확인: pg_stat_statements 확장으로 aggregate costs 확인
- 현재 세션과 실행 중 쿼리
-
MySQL/MariaDB
- 현재 스레드
SHOW PROCESSLIST; - InnoDB 락 대기
SELECT * FROM information_schema.innodb_trx; SELECT * FROM information_schema.INNODB_LOCK_WAITS; - performance_schema의 events_statements_summary_by_digest로 상위 슬로우 쿼리 확인
- 현재 스레드
- 인프라 레벨
- NAT 게이트웨이/로드밸런서 idle timeout 설정
- 보안 그룹/방화벽 드롭 기록, DNS 응답 시간
- OS ulimit (nofile), TCP keepalive, TIME_WAIT/ephemeral port 고갈 여부
커넥션 풀의 핵심 개념 정리
좋은 풀 설정은 “적은 수의 커넥션으로 높은 처리량을 내는 것”을 목표로 합니다.
- maximumPoolSize: 동시에 빌릴 수 있는 최대 커넥션 수
- minimumIdle: 풀에 유지하는 최소 유휴 커넥션 수
- connectionTimeout: 커넥션을 빌리기 위해 기다리는 최대 시간
- idleTimeout: 유휴 커넥션을 풀에서 제거하기까지 시간
- maxLifetime: 커넥션을 강제로 재생성하기까지 최대 수명(연결 부패/네트워크 문제 예방)
- validation/keepalive: 커넥션 유효성 검증, 주기적 ping
- leakDetection: 커넥션을 오래 보유하는 코드 경고
핵심 원칙
- 풀은 “크다고 좋은 것”이 아닙니다. 너무 크면 DB가 컨텍스트 스위칭으로 오히려 느려집니다.
- maxLifetime은 DB나 네트워크 idle timeout보다 약간 짧게 설정해 커넥션 ‘썩음’을 예방합니다.
- connectionTimeout은 사용자 경험과 재시도 전략을 고려해 합리적으로 설정합니다. 너무 길면 쓰레드가 대기만 하다 타임아웃 납니다.
권장 설정 가이드라인과 이유
아래는 일반적인 웹 애플리케이션의 출발점 설정입니다. 실제로는 워크로드와 DB 성능 프로파일에 따라 튜닝하세요.
-
maximumPoolSize
- 단일 애플리케이션 인스턴스: 10~30 사이에서 시작
- CPU 코어 수와 DB 코어 수를 고려하되, “전체 인스턴스 합계”가 DB가 처리 가능한 동시 커넥션 수(유효 병렬성)를 초과하지 않게 합니다.
- 예: DB 8 vCPU, 트랜잭션 평균 10~20ms라면 앱 인스턴스 4대 × 15 = 60 정도로 시작해 계측 기반 조정
-
minimumIdle
- 트래픽 급증 시 커넥션 생성 지연을 줄이기 위해 maximumPoolSize의 30~70% 수준
- 너무 높게 잡으면 한가할 때도 커넥션이 낭비됨
-
maxLifetime
- 20
30분 권장 또는 DB/NAT idle timeout보다 15분 짧게 - 예: RDS MySQL wait_timeout 8시간이라도 NAT나 LB idle timeout이 60분이면 maxLifetime=50~55분
- 20
-
idleTimeout
- 5~10분 정도로 설정해 유휴 커넥션을 적절히 정리
-
connectionTimeout
- 1~5초 사이에서 시작. 사용자-facing 요청이라면 2초 전후가 실용적
- 백그라운드 작업은 더 길게 가능하지만, 큐잉을 길게 허용하면 지연 폭발이 발생할 수 있음
-
validation
- testOnBorrow(빌릴 때 검증)는 비용이 커질 수 있습니다. 가능하면 keepalive/pre-ping 사용
- DB statement_timeout(또는 jdbc queryTimeout)도 병행 설정
-
leakDetection
- 예: HikariCP leakDetectionThreshold=2~5초로 설정해서 장시간 보유 코드 찾기
- 개발/스테이징에서 특히 유용
프레임워크별 실전 설정 예시
Spring Boot + HikariCP (JDBC)
application.yml 예시:
spring:
datasource:
url: jdbc:postgresql://db:5432/app
username: app
password: secret
hikari:
maximum-pool-size: 20
minimum-idle: 10
connection-timeout: 2000 # 2s
idle-timeout: 600000 # 10m
max-lifetime: 1800000 # 30m
validation-timeout: 1000
keepalive-time: 300000 # 5m ping
leak-detection-threshold: 2000 # 2s (스테이징에서만 권장)
jpa:
properties:
hibernate:
jdbc:
time_out: 2 # statement timeout(초)
HikariCP 팁
- maxLifetime은 DB나 네트워크 idle timeout보다 짧게
- keepaliveTime으로 NAT/LB idle로 인한 커넥션 drop 완화
- connectionTimeout은 사용자 요청 SLA와 일치시키기
Node.js + pg (PostgreSQL)
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
min: 5,
idleTimeoutMillis: 10 * 60 * 1000, // 10m
connectionTimeoutMillis: 2000, // 2s
allowExitOnIdle: false,
statement_timeout: 2000, // 서버에 전달 가능
query_timeout: 3000
});
module.exports = pool;
- 요청마다 pool.connect()로 빌린 뒤 반드시 release() 호출
- 긴 쿼리는 server-side statement_timeout으로 안전장치
Python + SQLAlchemy
from sqlalchemy import create_engine
engine = create_engine(
"postgresql+psycopg2://app:secret@db:5432/app",
pool_size=20,
max_overflow=5,
pool_timeout=2, # acquire timeout (sec)
pool_recycle=1800, # 30m
pool_pre_ping=True # 유효성 검사
)
- pool_recycle은 maxLifetime 유사
- pool_pre_ping으로 커넥션 부패 대비
Go + database/sql
db, _ := sql.Open("pgx", connString)
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(10 * time.Minute)
// 쿼리에는 context.WithTimeout으로 별도 타임아웃
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
- 쿼리 단위 context 타임아웃을 적극 활용
커넥션 풀 사이징: 정량적 접근
필요 커넥션 수 대략치:
- 필요한 동시 DB 세션 수 ≈ RPS × 평균 DB 처리시간(초) × 안전계수
예시
- RPS=200, 평균 DB 시간=15ms(0.015초), 안전계수=2
- 200 × 0.015 × 2 = 6
- 인스턴스 3대라면 인스턴스당 최대 풀 크기≈6/3=2 → 버스트 고려해 8~12에서 시작, 관측 기반 조정
주의
- 평균 대신 p95 DB 시간으로 계산하면 더 보수적
- “DB 코어 수 × 2~4”를 전체 시스템 동시 쿼리 상한으로 삼는 경험칙도 유용
- 전체 애플리케이션들의 합계 풀 크기가 DB 처리 능력을 넘지 않도록 중앙에서 관리
쿼리와 트랜잭션 최적화: 커넥션 점유 시간을 줄여라
Connection Timeout을 줄이는 가장 강력한 방법은 “커넥션 점유 시간을 최소화”하는 것입니다.
-
트랜잭션 최소화
- 필요한 구간에서만 트랜잭션 시작, 즉시 커밋/롤백
- 비즈니스 로직(외부 API 호출, 파일 I/O)을 트랜잭션 밖으로 분리
-
인덱스 최적화
- WHERE/GROUP BY/JOIN에 맞는 복합 인덱스
- 커버링 인덱스 사용으로 테이블 접근 감소
- EXPLAIN/EXPLAIN ANALYZE로 실행 계획 확인
-
N+1 제거
- JOIN으로 데이터 한 번에 가져오거나, IN 절로 배치 조회
- ORM의 eager fetch 전략 조정, SELECT 컬럼 최소화
-
쿼리 타임아웃
- 서버 측 statement_timeout(PostgreSQL) 또는 max_execution_time(MySQL Percona/ProxySQL)등 설정
- 클라이언트 측 query timeout도 병행
-
락 경합 완화
- 적절한 인덱싱으로 갭락/테이블락을 피하고 행락 범위를 최소화
- 일관된 락 순서로 데드락 가능성 축소
- 대량 업데이트는 배치로 나누고 오프피크 실행
-
읽기 부하 분산
- 읽기 전용 트래픽을 리드 레플리카로 분산 (읽기 일관성 요구사항 검토)
- 캐시(예: Redis)로 자주 조회되는 핫 키 offload
데이터베이스 설정과 외부 풀러(pooler) 활용
-
PostgreSQL
- max_connections은 과도하게 늘리지 말고 pgbouncer 같은 세션/트랜잭션 풀러 사용
- idle_in_transaction_session_timeout 설정으로 방치 세션 종료
- work_mem, shared_buffers 튜닝은 워크로드 기반으로
-
MySQL/InnoDB
- max_connections는 여유를 두되, 애플리케이션 풀 총합이 한계를 넘지 않게
- innodb_lock_wait_timeout으로 과도한 대기 방지
- thread_pool 플러그인(Enterprise/Percona) 고려
-
Connection Pooler
- pgbouncer(Transaction pooling)로 초경량 커넥션 관리
- ProxySQL로 라우팅/리트라이/쿼리 규칙 기반 제어
네트워크와 OS 레벨 최적화
-
LB/NAT idle timeout 확인
- 예: ALB 60초, NLB 350초 등 환경별 상이
- keepalive/keepaliveTime 또는 maxLifetime을 해당 값보다 짧게
-
TCP keepalive, ulimit
- net.ipv4.tcp_keepalive_time, tcp_fin_timeout 검토
- 파일 디스크립터 제한(nofile) 상향
-
DNS/TLS 비용
- 커넥션 재생성을 최소화하고, DNS 캐시와 TLS 세션 재사용 고려
재시도, 백오프, 서킷 브레이커
타임아웃은 일시적일 때가 많습니다. 재시도는 신중하게 설계하세요.
-
재시도 정책
- 짧은 지수 백오프 + jitter
- 멱등 작업(GET/조회)에만 기본 적용, 비멱등 작업은 신중
-
서킷 브레이커
- 연속 실패 시 빠른 실패로 시스템 보호
- 반개방(half-open)으로 회복 감지
-
타임아웃 계층
- 애플리케이션 전체 SLA < 커넥션 acquire timeout < 쿼리 timeout < 네트워크 read timeout
- 각 계층의 타임아웃이 논리적으로 정렬되도록 설계
커넥션 리크 탐지와 방지
-
코드 패턴
- try-with-resources(Java), defer close(Go), context manager(Python)로 자동 반환
- 예외 발생 시에도 커넥션/Statement/ResultSet 닫히는지 확인
-
리크 탐지
- HikariCP leakDetectionThreshold
- APM으로 “DB span 시작~종료” 시간이 비정상적으로 긴 호출 식별
-
테스트
- 로컬/스테이징에서 혼합 워크로드로 부하 테스트하고 풀 고갈이 재현되는지 관찰
모니터링 지표: 무엇을 꾸준히 볼 것인가
애플리케이션
- pool.active, pool.idle, pool.pending(waiting)
- pool.acquire.time p95/p99
- connection timeout rate, error budget 소진율
- request latency 분해(DB 비중)
데이터베이스
- 활성 세션 수, 트랜잭션/초, 쿼리 대기 시간
- 슬로우 쿼리(상위 10개), buffer hit ratio, temp spill
- 락 대기 수/시간, 데드락 카운트
- CPU/IO 사용률, 체크포인트/오토바큠/flush 이벤트
인프라
- 네트워크 RTT, 패킷 손실률, LB/NAT 연결 수
- DNS 응답 시간, TLS handshake 실패
대시보드 팁
- “풀 대기열(pending)”과 “슬로우 쿼리”를 같은 화면에 배치
- 타임아웃 이벤트 발생 시점과 DB CPU/IO 스파이크 상관관계 확인
- 버전 릴리스/배포 타임라인과 겹쳐 보기
실전 시나리오: 단계별 튜닝 예
상황
- 낮 시간대 p95 응답 800ms로 상승, “Timeout waiting for connection” 발생
- HikariCP 설정: maxPool=10, minIdle=2, connectionTimeout=30s
- Postgres CPU 40%, 슬로우 쿼리 다수, pg_stat_activity에서 1~2초짜리 조회가 간헐적 등장
조치
-
긴급 조치
- connectionTimeout 2초로 단축해 대기 폭발 방지
- maxPool 10→20으로 소폭 증가, minIdle 10으로 증가
- statement_timeout 2초 설정으로 악성 쿼리 차단
-
원인 제거
- 슬로우 쿼리 TOP3 인덱스 보강, 불필요 JOIN 제거
- N+1 해결: 배치 조회로 대체
- 트랜잭션 범위 축소(외부 API 호출 분리)
-
안정화
- maxLifetime 30분, keepalive 5분 추가로 커넥션 부패 예방
- pgbouncer 도입 검토로 연결 관리 비용 감소
결과
- pool.pending 거의 0으로 유지, p95 220ms 수준 회복
- connection timeout 에러율 0.01% 미만으로 감소
체크리스트: 배포 전·후 확인 사항
사전
- 풀 크기와 전체 인스턴스 합계가 DB 처리 여력 내인지
- connectionTimeout, query timeout, read timeout의 계층적 정합성
- maxLifetime과 네트워크 idle timeout 정렬
- 인덱스/쿼리 플랜 검증, N+1 제거
- 리크 탐지 활성화(스테이징)
운영
- pool.acquire p95 < 50ms 유지
- 슬로우 쿼리 상위 목록 주기적 점검
- 락 대기/데드락 알람 설정
- LB/NAT 재설정이나 보안 정책 변경 시 회귀 테스트
장애 대응
- 풀 임시 확장 + 엄격한 timeout으로 급한 불 끄기
- 슬로우 쿼리/락 원인 제거 후 풀 원복
- 원인 분석 RCA 문서화 및 방지책 배포
자주 하는 실수와 반례
-
풀을 너무 크게 설정
- 단기적으로 타임아웃은 줄지만 DB가 포화되며 전반적 지연 증가
-
testOnBorrow 남발
- 매 대출마다 validation 쿼리로 대기 시간 증가. pre-ping/keepalive로 대체
-
트랜잭션 안에서 외부 API 호출
- 커넥션을 불필요하게 오래 점유
-
무제한 재시도
- 장애를 증폭시키는 지름길. 백오프와 회로 차단 필요
-
캐시 무시
- 읽기 부하 대부분은 캐시로 흡수 가능. 데이터 일관성 정책과 함께 도입
실용 팁: 단 몇 줄로 체감 효과 얻기
- HikariCP: connectionTimeout=2000, maxLifetime=1800000, keepalive=300000, leakDetectionThreshold=2000
- PostgreSQL: statement_timeout=2s, idle_in_transaction_session_timeout=60s
- MySQL: innodb_lock_wait_timeout=5~10s, slow_query_log=ON으로 병목 파악
- 앱 코드: 요청 핸들러에서 DB 호출을 묶어 배치화, 트랜잭션 밖으로 I/O 이동
- 캐시: 상위 TOP N 핫키만이라도 캐싱해 즉시 효과
마무리: Connection Timeout은 “증상”, 풀 설정과 성능 최적화는 “치료”
Connection Timeout은 시스템 어딘가에 있는 병목의 결과입니다. 커넥션 풀 설정을 제대로 하면 증상을 완화할 수 있고, 쿼리/트랜잭션 최적화와 인프라 조정으로 원인도 제거할 수 있습니다. 핵심은 계측과 점진적 튜닝입니다. 숫자로 보고, 작은 변경을 하고, 효과를 검증하세요.
오늘 다음을 시도해 보세요.
- 풀 크기와 타임아웃을 SLA에 맞춰 재정렬
- 슬로우 쿼리 TOP 목록에 인덱스를 추가
- 트랜잭션 범위를 절반으로 줄이기
- p95 connection wait time을 모니터링 대시보드로 승격
이 4가지만으로도 Connection Timeout 문제의 80%는 해결됩니다. 나머지는 여러분의 워크로드와 환경에 맞춘 정밀 튜닝으로 완성할 수 있습니다.