배포 후 피크 시간대에 서비스가 갑자기 느려졌습니다. 로그를 보니 "Cannot get a connection, pool error Timeout"이 쏟아집니다. 개발팀은 "DB 쿼리가 느려졌어요"라고 하고, DBA는 "DB 서버는 정상이에요"라고 합니다. 그사이 WAS 스레드가 하나씩 DB 대기 상태로 멈춰가고 있습니다.
Connection Pool이 왜 소진됐는지, timeout 설정을 어떻게 튜닝해야 하는지, DB 장애 시 서비스를 어떻게 격리하는지 — 인프라 엔지니어가 알아야 할 핵심을 정리합니다.
- 1JDBC URL 구조를 읽고 MySQL/PostgreSQL/Oracle 접속을 명령줄에서 테스트할 수 있다
- 2HikariCP 핵심 설정(maximumPoolSize, connectionTimeout, maxLifetime)을 설명하고 조정할 수 있다
- 3Connection Pool 소진 로그를 분석해 원인(느린 쿼리 vs 누수)을 구분할 수 있다
- 4socketTimeout 미설정 시 스레드 전체 블로킹 장애를 설명할 수 있다
- 5DB 계정 권한 최소화 원칙을 적용할 수 있다
JDBC 접속 구조
JDBC URL 구조와 DB별 접속 테스트
JDBC URL은 앱 서버가 DB에 접속하기 위한 연결 정보를 하나의 문자열로 표현합니다. URL 구조를 이해하면 접속 문제가 생겼을 때 무엇이 잘못됐는지 빠르게 파악할 수 있습니다.

신규 서버에서 앱을 기동했는데 "Unable to connect to database" 에러가 납니다. 개발팀은 "JDBC URL 그대로 복사했는데요"라고 하고, DBA는 "DB 서버는 정상입니다"라고 합니다. 포트가 열렸는지, 계정이 맞는지, URL 파라미터에 오타는 없는지 — 범위를 좁히려면 JDBC URL 구조를 읽을 줄 알아야 합니다. 실제 장애의 절반은 접속 URL의 호스트명·포트·DB명 중 하나가 틀린 데서 시작합니다. 명령줄에서 단계별로 연결을 확인하는 루틴이 있으면 원인을 5분 안에 좁힐 수 있습니다.
jdbc:mysql://db-server:3306/mydb?useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Seoul
↑ ↑ ↑ ↑ ↑
드라이버 호스트 포트 DB명 접속 파라미터
DB별 URL 패턴:
# MySQL / MariaDB
jdbc:mysql://db-server:3306/mydb?useSSL=false&characterEncoding=UTF-8
# PostgreSQL
jdbc:postgresql://db-server:5432/mydb?ssl=false
# Oracle (SID 방식)
jdbc:oracle:thin:@db-server:1521:ORCL
# Oracle (Service Name 방식 — 권장)
jdbc:oracle:thin:@//db-server:1521/ORCL
명령줄에서 DB 접속 테스트 — 앱 배포 전 필수 확인:
# 1. 포트 열려있는지 먼저 확인
nc -zv db-server 3306
# Connection to db-server 3306 port [tcp/mysql] succeeded!
# 2. MySQL 접속 테스트
mysql -h db-server -u appuser -p -e "SELECT 1"
# 또는 패스워드 인라인 (스크립트에서만, 보안 주의)
mysql -h db-server -u appuser -pmypassword -e "SELECT 1"
# 3. PostgreSQL 접속 테스트
psql -h db-server -U appuser -d mydb -c "SELECT 1"
# PGPASSWORD=mypassword psql ... (환경변수 방식)
# 4. Oracle 접속 테스트
sqlplus appuser/password@//db-server:1521/ORCL
# 5. 접속 성공하면 권한도 확인
mysql -h db-server -u appuser -p -e "SHOW GRANTS FOR CURRENT_USER()"
# PostgreSQL
psql -h db-server -U appuser -d mydb -c "\dp"
HikariCP 설정 — Connection Pool의 핵심
HikariCP는 Spring Boot의 기본 Connection Pool 라이브러리입니다. 잘못된 설정 하나가 피크 시간대 서비스 전체 장애로 이어질 수 있습니다.

# application.yml (Spring Boot)
spring:
datasource:
url: jdbc:mysql://db-server:3306/mydb?characterEncoding=UTF-8
username: appuser
password: ${DB_PASSWORD} # 환경변수로 관리
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
# Pool 크기 설정
maximum-pool-size: 20 # 최대 Connection 수 (기본: 10)
minimum-idle: 5 # 최소 유지 Connection 수
# Timeout 설정
connection-timeout: 30000 # Pool에서 Connection 대기 최대 시간 (ms, 기본 30초)
idle-timeout: 600000 # 유휴 Connection 유지 시간 (ms, 기본 10분)
max-lifetime: 1800000 # Connection 최대 수명 (ms, 기본 30분)
# 연결 검증
connection-test-query: SELECT 1 # MySQL/PostgreSQL
# connection-test-query: SELECT 1 FROM DUAL # Oracle
# Pool 이름 (로그 식별용)
pool-name: MyApp-HikariPool
설정값 결정 기준:
| 설정 | 낮으면 | 높으면 | 권장 시작값 |
|---|---|---|---|
| maximum-pool-size | Pool 소진 | DB 연결 수 부하 | 10~20 (WAS 인스턴스당) |
| connection-timeout | 빠른 실패 | 스레드 오래 대기 | 3000~5000ms (운영) |
| max-lifetime | 자주 재연결 | 죽은 Connection 오래 유지 | DB의 wait_timeout보다 짧게 |
MySQL wait_timeout과 max-lifetime 연관 관계:
-- DB 서버에서 확인
SHOW VARIABLES LIKE 'wait_timeout';
-- 일반적으로 28800초(8시간)
-- max-lifetime은 이보다 반드시 짧게 설정 (예: 1800000ms = 30분)
DB 접속 테스트와 Pool 모니터링
DB 접속 문제는 네트워크 → 포트 → 인증 → 권한 순서로 범위를 좁혀 확인합니다.
# 1단계: 네트워크 연결 확인
nc -zv db-server 3306
# 실패 → 방화벽/보안그룹에서 3306 포트 차단
# 2단계: DB 서버 응답 확인
telnet db-server 3306
# 연결 시 "J. 8.0.32..." 같은 MySQL 헤더가 나오면 서버 동작 중
# 3단계: 인증 테스트
mysql -h db-server -u appuser -p
# ERROR 1045 → 패스워드 오류 또는 계정 없음
# ERROR 1130 → 해당 IP에서 접속 권한 없음
# 4단계: DB별 권한 확인
-- MySQL: 이 계정이 어떤 권한을 가지는지
SHOW GRANTS FOR 'appuser'@'%';
-- 최소 권한 설정 예시 (DBA에게 요청)
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'appuser'@'%';
-- DDL 권한(CREATE, DROP, ALTER)은 앱 계정에 부여하지 않는 것이 원칙
nc -zv db-server 3306 && mysql -h db-server -u appuser -p -e 'SELECT 1'nc -zv db-server 3306에서succeeded메시지가 나왔는가 (포트 열림 확인)mysql -h db-server -u appuser -p -e 'SELECT 1'실행 결과가1로 반환됐는가SHOW GRANTS FOR 'appuser'@'%'출력에CREATE/DROP/ALTER권한이 없는가
HikariCP는 상세한 Pool 상태를 로그로 남깁니다. Pool 소진 여부와 원인을 로그에서 읽는 방법을 익힙니다.
# HikariCP 관련 로그 필터링
grep "HikariPool" /opt/app/logs/app.log | tail -20
# Pool 정상 상태 로그 예시:
# HikariPool-1 - Pool stats (total=20, active=3, idle=17, waiting=0)
# total: maximumPoolSize / active: 현재 사용 중 / idle: 유휴 / waiting: 대기 스레드
# Pool 소진 로그 예시:
# HikariPool-1 - Pool stats (total=20, active=20, idle=0, waiting=5)
# active=20(max), idle=0, waiting=5 → Pool 소진, 5개 스레드 대기 중
# Timeout 예외 로그:
# com.zaxxer.hikari.pool.HikariPool$PoolInitializationException:
# Cannot get a connection, pool error Timeout waiting for connection from pool
# 느린 쿼리로 인한 소진인지 확인
grep "Slow query" /opt/app/logs/app.log | tail -10
# 또는 DB 서버에서 slow query log 확인
# MySQL: /var/log/mysql/slow-query.log (설정 필요)
grep 'HikariPool' /opt/app/logs/app.log | tail -20- 로그에서
Pool stats라인이 나왔는가 (grep "HikariPool" /opt/app/logs/app.log | tail -5) total,active,idle,waiting네 필드를 모두 확인했는가waiting=0인 경우 Pool이 여유 있는 정상 상태임을 확인했는가- Pool 소진 징후(
active=max, idle=0, waiting>0)가 있는지 확인했는가
socketTimeout이 없으면 DB 서버가 응답을 안 해도 스레드가 영원히 기다립니다. Pool의 모든 스레드가 여기에 묶이면 서비스 전체가 멈춥니다.
# JDBC URL에 timeout 파라미터 추가
# MySQL
jdbc:mysql://db-server:3306/mydb
?characterEncoding=UTF-8
&connectTimeout=3000
&socketTimeout=10000
# PostgreSQL
jdbc:postgresql://db-server:5432/mydb
?connectTimeout=3
&socketTimeout=10
# Oracle (JDBC 드라이버 파라미터)
jdbc:oracle:thin:@//db-server:1521/ORCL?oracle.net.READ_TIMEOUT=10000
timeout 파라미터 의미:
| 파라미터 | 의미 | 권장값 |
|---|---|---|
| connectTimeout | TCP 연결 수립 timeout | 3~5초 |
| socketTimeout | SQL 응답 대기 timeout | 10~30초 (쿼리 복잡도에 따라) |
| connectionTimeout (HikariCP) | Pool에서 Connection 획득 대기 | 3~5초 (운영에서 짧게) |
grep -r 'socketTimeout\|connectTimeout' /opt/app/config/- nc -zv db-server 3306 → 'succeeded' 메시지가 나왔는가 (포트 열림 확인)
- mysql -h db-server -u appuser -p -e 'SELECT 1' → '1' 결과가 나왔는가
- HikariPool 로그에서 waiting=0인가 (Pool 여유 확인)
- JDBC URL에 socketTimeout 파라미터가 포함됐는가
- appuser 계정에 DDL 권한(CREATE/DROP/ALTER)이 없는가 (SHOW GRANTS 확인)
DB 장애 시 서비스 격리
DB 장애가 서비스 전체를 멈추는 구조와 대응
DB 서버가 일시적으로 응답하지 않으면, socketTimeout이 없는 서비스에서 어떤 일이 벌어지는지 이해하는 것이 핵심입니다.
[요청 1] → WAS 스레드 1 → DB 쿼리 대기 (socketTimeout 없음 → 무한 대기)
[요청 2] → WAS 스레드 2 → DB 쿼리 대기 (무한 대기)
...
[요청 N] → WAS 스레드 N → 스레드 풀 고갈 → 새 요청 처리 불가
→ 서비스 전체 중단
DB 장애 격리 체크리스트:
# 1. socketTimeout 설정 (반드시)
# DB 응답 없으면 N초 후 예외 발생 → 스레드 해제
# 2. connectionTimeout 단축 (운영 환경)
# 기본 30초 → 3~5초로 줄여 빠른 실패(fast-fail)
# 3. 읽기/쓰기 분리 고려
# 읽기 전용 기능은 Read Replica로 → 주 DB 장애 시에도 일부 서비스 유지
# 4. DB 접속 실패 시 Circuit Breaker 적용 (Spring Cloud Circuit Breaker 등)
# 일정 횟수 실패 시 DB 호출 차단 → 즉시 fallback 반환
# 5. 헬스체크 엔드포인트에서 DB 연결 상태 포함
# /actuator/health → db: DOWN 시 로드밸런서가 해당 인스턴스 제외
DB 계정 권한 최소화 (보안 기본 원칙):
-- 앱 계정: DML만
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'appuser'@'%';
-- 마이그레이션 전용 계정: DDL 포함 (배포 시에만 사용)
GRANT ALL PRIVILEGES ON mydb.* TO 'migrator'@'localhost';
-- 모니터링 계정: SELECT만
GRANT SELECT ON mydb.* TO 'monitor'@'%';
-- 계정 생성 후 즉시 확인
SHOW GRANTS FOR 'appuser'@'%';
트러블슈팅
원인: Connection Pool이 소진됐습니다. 두 가지 경우가 대부분입니다: ① 느린 쿼리로 Connection이 오래 점유됨, ② Connection Leak(반환 안 됨).
# 1. Pool 상태 즉시 확인
grep "Pool stats" /opt/app/logs/app.log | tail -5
# total=20, active=20, idle=0, waiting=N → Pool 소진 확인
# 2. 느린 쿼리가 원인인지 확인
# DB 서버에서 현재 실행 중인 쿼리 확인 (MySQL)
SHOW PROCESSLIST;
# Time 컬럼이 큰 값(수십 초)인 쿼리 → 느린 쿼리 원인
# PostgreSQL
SELECT pid, now() - pg_stat_activity.query_start AS duration,
query, state
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration DESC;
# 3. Connection Leak 의심 시 leakDetectionThreshold 설정
# hikari:
# leak-detection-threshold: 5000 # 5초 이상 반환 안 되면 경고 로그
# 4. 임시 조치: Pool 크기 증가 (근본 원인 해결 전 임시)
# maximum-pool-size: 20 → 30 (단, DB 서버 max_connections 초과 금지)
# 5. 근본 원인 해결
# - 느린 쿼리: 인덱스 추가, 쿼리 최적화 (DBA 협업)
# - Connection Leak: try-with-resources 미사용 코드 점검
원인: socketTimeout 미설정으로 모든 WAS 스레드가 DB 응답 대기 상태에 묶였습니다. DB가 복구돼도 WAS 스레드가 여전히 대기 중이어서 새 요청을 처리하지 못합니다.
# 1. 현재 WAS 스레드 상태 확인 (Java)
# JVM Thread Dump 생성
kill -3 <WAS_PID>
# 또는 jstack 사용
jstack <WAS_PID> | grep -A 10 "WAITING\|BLOCKED" | head -60
# Thread Dump에서 확인할 것:
# "WAITING (on object monitor)" with socketRead0 → DB 응답 대기 중
# 이런 스레드가 다수면 → socketTimeout 미설정 확인
# 2. 설정 확인
grep -r "socketTimeout" /opt/app/config/
# 없으면 → 즉시 추가 필요
# 3. 임시 복구
sudo systemctl restart tomcat # WAS 재시작으로 스레드 초기화
# 4. 영구 해결 — JDBC URL에 socketTimeout 추가
# jdbc:mysql://db-server:3306/mydb?socketTimeout=10000
# 재배포 또는 WAS 재기동 필요
# 5. 추가 방어: connectionTimeout도 단축
# hikari.connection-timeout: 5000 (5초)
# Pool 소진 시 5초 후 빠른 실패 → 스레드 해제
실제 업무에서 이 지식이 쓰이는 상황:
DB Connection Pool 문제는 인프라 엔지니어가 야간에 가장 많이 받는 장애 유형 중 하나입니다.
1. 신규 서비스 DB 설정 검토 (배포 전):
# 체크리스트
# □ JDBC URL에 socketTimeout 설정 있는가
# □ HikariCP connectionTimeout이 30초(기본값)로 남아있지 않은가
# □ maximumPoolSize × WAS 인스턴스 수가 DB max_connections 이하인가
# □ DB 계정에 DDL 권한이 없는가
# □ validationQuery(SELECT 1) 설정됐는가
# □ max-lifetime이 DB wait_timeout보다 짧은가
2. 피크 시간 장애 대응:
# 빠른 진단 루틴 (5분 이내)
grep "Pool stats\|Timeout waiting\|Cannot get" /opt/app/logs/app.log | tail -20
# active=max, waiting>0 → Pool 소진 확인
# DB 서버 연결 수 확인
mysql -h db-server -u monitor -p -e "SHOW STATUS LIKE 'Threads_connected'"
# Threads_connected가 max_connections에 근접 → DB 측 포화
3. 운영 중 DB 서버 IP 변경 (인프라 작업): JDBC URL의 호스트명을 IP 대신 DNS 이름으로 관리하면 재기동 없이 전환이 가능합니다. max-lifetime(기본 30분)으로 기존 Connection이 만료되면서 새 IP로 재연결됩니다.
다음 모듈에서는 Redis와 Elasticsearch 접속 확인과 운영 기초를 다룹니다.