infra
Platform

모듈 맵

[Infra Ops] JDBC Connection Pool과 DB 장애 분리 실무

0 / 52 완료

펼치기
0 / 52 완료0%

Infra-ops · 30 / 52

[Infra Ops] JDBC Connection Pool과 DB 장애 분리 실무

JDBC URL 구조, HikariCP connection pool 설정, DB 계정/schema 관리, timeout 튜닝, DB 장애 시 서비스 격리까지

🚨INCIDENT ALERT
HIGH

배포 후 피크 시간대에 서비스가 갑자기 느려졌습니다. 로그를 보니 "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 구조를 이해하면 접속 문제가 생겼을 때 무엇이 잘못됐는지 빠르게 파악할 수 있습니다.

JDBC URL 구조와 DB별 접속 테스트

신규 서버에서 앱을 기동했는데 "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 라이브러리입니다. 잘못된 설정 하나가 피크 시간대 서비스 전체 장애로 이어질 수 있습니다.

HikariCP 설정 — Connection Pool의 핵심

YAML
# 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-sizePool 소진DB 연결 수 부하10~20 (WAS 인스턴스당)
connection-timeout빠른 실패스레드 오래 대기3000~5000ms (운영)
max-lifetime자주 재연결죽은 Connection 오래 유지DB의 wait_timeout보다 짧게

MySQL wait_timeout과 max-lifetime 연관 관계:

SQL
-- DB 서버에서 확인
SHOW VARIABLES LIKE 'wait_timeout';
-- 일반적으로 28800초(8시간)
-- max-lifetime은 이보다 반드시 짧게 설정 (예: 1800000ms = 30분)

DB 접속 테스트와 Pool 모니터링

1DB 접속 테스트 — 네트워크부터 쿼리까지 단계별 확인

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'
🔍DB 접속 테스트 결과
  • nc -zv db-server 3306 에서 succeeded 메시지가 나왔는가 (포트 열림 확인)
  • mysql -h db-server -u appuser -p -e 'SELECT 1' 실행 결과가 1로 반환됐는가
  • SHOW GRANTS FOR 'appuser'@'%' 출력에 CREATE/DROP/ALTER 권한이 없는가
2Connection Pool 상태 로그 분석

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
🔍Connection Pool 상태 로그 분석 결과
  • 로그에서 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)가 있는지 확인했는가
3socketTimeout 설정으로 스레드 블로킹 방지

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 파라미터 의미:

파라미터의미권장값
connectTimeoutTCP 연결 수립 timeout3~5초
socketTimeoutSQL 응답 대기 timeout10~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 장애 격리 체크리스트:

YAML
# 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 계정 권한 최소화 (보안 기본 원칙):

SQL
-- 앱 계정: 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 접속 확인과 운영 기초를 다룹니다.

지식 확인

퀴즈 — 4문제

Q1

Connection Pool이 소진됐을 때 애플리케이션에서 나타나는 현상은?

Q2

HikariCP의 validationQuery(connectionTestQuery) 설정의 목적은?

Q3

connectionTimeout과 socketTimeout(queryTimeout)의 차이는?

Q4

DB 서버 IP가 변경됐을 때 WAS 재기동 없이 반영하는 방법은?

0 / 4 답변

🧪 실습으로 확인하기

Nginx 설치 및 기동

초급

Linux 서버에 Nginx를 설치하고 systemd 서비스로 등록하여 80포트에서 응답하는 상태까지 만든다.

30📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

infra-ops중급 · 60
[Infra Ops] Cron/Quartz 장애 분석과 배치 재처리 실무
인프라 서비스 운영 트랙 계속
linux입문 · 30
[Linux] 개발자가 왜 리눅스 서버와 커맨드라인을 반드시 배워야 하는가
Linux 트랙 시작점