infra
Platform

모듈 맵

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

0 / 52 완료

펼치기
0 / 52 완료0%

인프라 운영 & SRE · 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 접속 구조

💡개념

앱이 DB에 질의하기까지 — 커넥션 문자열부터 연결 반납까지 5단계

앱 코드에서는 userRepository.findById(42) 한 줄이지만, 그 뒤에서 드라이버가 커넥션 문자열을 해석하고 → 풀에서 연결을 빌리고 → 쿼리를 보내 DB가 실행하고 → 결과를 받아 객체로 매핑하고 → 연결을 풀에 되돌리는 5단계가 순서대로 일어납니다. 이 단계를 알면 "왜 안 되지", "왜 느리지", "왜 풀이 마르지"를 각각 어느 단계 문제인지로 좁힐 수 있습니다. DB 연동 장애는 대부분 이 5단계 중 한 곳에서 끊긴 결과입니다.

TEXT
[앱 코드]  userRepository.findById(42)
   │
   ① 커넥션 문자열·드라이버 로드   (jdbc:mysql://host:3306/db → 드라이버 선택)
   │
   ② 풀에서 커넥션 획득           (idle 커넥션 대여 / 없으면 connectionTimeout까지 대기)
   │
   ③ 쿼리 전송·실행              (SQL을 소켓으로 전송 → DB가 파싱·실행)
   │    → socketTimeout까지 응답 대기
   │
   ④ 결과 수신·매핑              (ResultSet → 객체/DTO로 변환)
   │
   ⑤ 커넥션 반납                (close() = 풀에 반환, 실제 연결은 끊지 않음)
   ▼
[앱 코드]  다음 요청이 같은 커넥션을 재사용

각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:

단계하는 일여기서 막히면
① 문자열·드라이버host·port·db·파라미터를 파싱하고 드라이버 클래스를 로드드라이버 미포함 → No suitable driver / URL 오타 → Unknown database·ClassNotFoundException
② 풀에서 획득HikariCP가 idle 커넥션을 빌려주거나 새로 만들거나 대기시킴풀 소진 → connectionTimeout 초과 → Cannot get a connection ... Timeout / 최초 연결 실패 → Connection refused(포트·방화벽)·Access denied(인증)
③ 쿼리 전송·실행SQL을 소켓으로 보내고 DB가 실행해 응답할 때까지 대기socketTimeout 없으면 느린 쿼리에 스레드가 영구 블로킹 → 풀 고갈 / 있으면 지정 시간 후 Read timed out
④ 결과 수신·매핑ResultSet을 받아 객체로 변환대량 결과를 한 번에 로딩 → OOM·GC 폭주 (페이징·스트리밍으로 나눠 받아야 함)
⑤ 커넥션 반납close()로 풀에 되돌려 다음 요청이 재사용try-with-resources 누락으로 반납 안 됨(누수) → 서서히 풀 소진 (leakDetectionThreshold로 탐지)

즉 DB 연동이 안 되거나 느릴 때 이 5단계 중 어디서 끊겼는지를 좁히는 것이 진단의 핵심입니다 — ①②는 접속 계층(nc·mysql 클라이언트로 재현), ③은 쿼리 계층(SHOW PROCESSLIST·slow log), ⑤는 코드 계층(누수)입니다. Connection refused는 ①②(네트워크·인증), pool error Timeout은 ②(풀 소진), 스레드가 socketRead0에 묶여 있으면 ③(응답 대기)로 갈라집니다. 아래에서는 먼저 ①의 커넥션 문자열(JDBC URL)부터 읽는 법을 봅니다.

💡개념

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

JDBC URL은 앱 서버가 DB에 접속하기 위한 연결 정보를 하나의 문자열로 표현합니다. URL 구조를 이해하면 접속 문제가 생겼을 때 무엇이 잘못됐는지 빠르게 파악할 수 있습니다.

JDBC URL 구조와 타임아웃 파라미터 — jdbc:mysql://host:3306/db?connectTimeout=3000&socketTimeout=10000 형식. connectTimeout은 TCP 연결 대기(권장 35초), socketTimeout은 SQL 응답 대기(권장 1030초)로, socketTimeout 미설정 시 응답 없는 쿼리에 스레드가 영원히 묶여 WAS 스레드 풀이 고갈됨확대

신규 서버에서 앱을 기동했는데 "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 정상 vs 소진 — 정상은 total=20·active 일부·waiting=0으로 새 요청에 즉시 할당. 소진은 active=20·waiting 누적으로 connectionTimeout 초과 시 "Cannot get a connection" 오류. 원인은 느린 쿼리의 Connection 장기 점유·try-with-resources 미사용 누수(leak-detection-threshold로 탐지)확대

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'
🔍실행 후 확인할 것
  • nc -zv db-server 3306 으로 TCP 포트 도달 먼저 확인, succeeded이면 mysql 클라이언트로 실제 인증 테스트 — nc succeeded인데 mysql 연결 실패하면 DB 계정/권한 문제
  • 접속 기준: nc succeeded + mysql SELECT 1 결과 1=DB 연결 정상 / nc타임아웃=방화벽 차단 / nc succeeded + mysql "Access denied"=계정 없거나 호스트 제한 / mysql "Can't connect"=DB 프로세스 미기동
  • SELECT 1이 성공해도 SHOW GRANTS에서 CREATE/DROP/ALTER 권한이 있으면 → 서비스 계정이 DDL 권한을 가진 위험 상태 — 운영 계정은 SELECT/INSERT/UPDATE/DELETE 만 허용, REVOKE로 DDL 권한 즉시 제거
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
🔍실행 후 확인할 것
  • grep "HikariPool" 로 Pool stats 라인 먼저 확인, 그 다음 active/idle/waiting 세 숫자로 Pool 상태 판단 — Pool stats 라인 자체가 없으면 HikariCP 로그 레벨이 DEBUG가 아닌 것(logback 설정 확인)
  • Pool 수치 기준: idle > 0 && waiting == 0=정상 여유 있음, idle == 0 && waiting == 0=Pool 풀 가동 중(주의), idle == 0 && waiting > 0=Pool 소진 상태(서비스 응답 지연 시작) — waiting이 5 이상이면 즉시 조치
  • Pool stats에서 active가 total(maximumPoolSize)과 같고 waiting > 0이면 → Pool 소진 상태 — 원인은 슬로우 쿼리(DB slow log 확인) 또는 socketTimeout 누락(스레드 영구 블로킹) 두 가지 중 하나
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 → mysql SELECT 1 → HikariPool waiting 확인 → JDBC URL socketTimeout 확인 순서로 점검 — 앞 단계 실패 시 이후 단계는 의미 없으므로 순서 준수
  • 종합 기준: nc succeeded + mysql 성공 + waiting=0 + socketTimeout 설정됨=DB 통합 정상 / socketTimeout 없으면 DB 응답 지연 시 스레드 영구 블로킹 위험으로 시한폭탄 상태
  • JDBC URL에 socketTimeout이 있는데도 Pool 소진이 발생하면 → socketTimeout 값이 실제 쿼리 소요 시간보다 짧은 것 — DB slow log에서 실제 쿼리 시간 확인 후 socketTimeout을 최대 허용 쿼리 시간보다 1.5배 이상으로 설정

DB 장애 시 서비스 격리

💡개념

DB 장애가 서비스 전체를 멈추는 구조와 대응

DB 서버가 일시적으로 응답하지 않으면, socketTimeout이 없는 서비스에서 어떤 일이 벌어지는지 이해하는 것이 핵심입니다.

DB 장애가 서비스 전체를 멈추는 cascade와 대응 — socketTimeout이 없으면 요청마다 WAS 스레드가 DB 응답을 무한 대기하며 하나씩 묶이고, 스레드 풀이 고갈되면 새 요청을 못 받아 부분 장애(DB 지연)가 서비스 전체 중단으로 증폭된다. 격리 5가지: ① socketTimeout으로 N초 후 스레드 해제(필수), ② connectionTimeout을 30초에서 3~5초로 단축해 빠른 실패, ③ 읽기를 Read Replica로 분리해 주 DB 장애 시 일부 기능 유지, ④ Circuit Breaker로 연속 실패 시 DB 호출을 차단하고 즉시 fallback, ⑤ 헬스체크에 db:DOWN을 포함해 로드밸런서가 해당 인스턴스를 제외. 함께 DB 계정 권한을 앱=DML만·마이그레이션=DDL·모니터링=SELECT만으로 최소화한다확대

[요청 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초 후 빠른 실패 → 스레드 해제

심화 — 숫자 뒤의 원리: 풀 크기와 커넥션 수명

💡개념

심화: 커넥션 풀은 클수록 빠르지 않다 — 적정 크기의 역설

maximumPoolSize를 키우면 처리량이 오를 것 같지만, 어느 지점을 넘으면 오히려 느려집니다. 풀이 실제로 무엇을 기다리게 하는지 보면 이유가 드러납니다.

  • DB의 동시 처리량은 코어·디스크로 정해집니다: DB가 한 번에 진짜 병렬로 처리하는 쿼리 수는 CPU 코어와 디스크 채널이 상한입니다. 풀을 그보다 크게 잡아도 초과분은 DB 안에서 줄을 서고, 컨텍스트 스위칭·락 경합·캐시 미스만 늘어 전체가 느려집니다.
  • 줄은 앱 쪽에서 세우는 게 낫습니다: 요청이 몰릴 때 커넥션을 무한정 늘리기보다, 작은 풀 앞(메모리 큐)에서 잠깐 기다리게 하는 편이 낫습니다. 커넥션 획득 대기는 connectionTimeout이 지키고, DB는 자기가 감당할 만큼만 동시에 처리합니다.
  • 경험식: HikariCP 가이드의 출발점은 (코어 수 × 2) + 디스크 수 수준입니다. 수백 개가 아니라 수십 개가 최적인 경우가 많습니다. 큰 풀은 대개 슬로우 쿼리·커넥션 누수를 덮으려는 반창고입니다.
  • 한계: 이 최적값은 워크로드가 CPU 바운드일 때 이야기입니다. 쿼리가 외부 대기(원격 호출·락)로 노는 시간이 많으면 조금 더 키울 수 있지만, 그때도 "무한정 크게"가 아니라 부하 테스트로 무릎점(knee)을 찾아 정합니다.

그래서 풀 튜닝의 첫 질문은 "얼마나 키울까"가 아니라 "DB가 동시에 몇 개를 진짜로 처리하나"입니다.

상황: SELECT 1 검증도 켜 뒀고 DB는 멀쩡한데, 유휴 구간 뒤 첫 쿼리에서만 Connection reset 또는 broken pipe가 뜹니다. 재시도는 성공합니다. 로그를 뒤져도 슬로우 쿼리나 풀 소진은 없고, 재현은 항상 트래픽이 낮았던 구간 직후입니다.

원인: 앱과 DB 사이의 방화벽/NAT/로드밸런서가 유휴 TCP 연결을 조용히 끊습니다(idle timeout, 예: 5~60분). HikariCP의 maxLifetime(기본 30분)과 유휴 커넥션이 이 네트워크 유휴 한도보다 오래 살아 있으면, 중간 장비가 이미 세션을 버린 뒤라 첫 패킷에 RST가 돌아옵니다. DB가 끊은 게 아니라 중간 장비가 끊은 것이라, DB 재기동과 무관하게 유휴 뒤에만 재현됩니다. 검증 쿼리는 커넥션을 꺼내는 그 순간에만 살아있는지 보므로, 빌려준 뒤 죽는 경우까지 항상 막지는 못합니다.

진단: 재현이 항상 저트래픽 구간 직후인지 확인합니다. 방화벽/LB의 idle timeout 값과 maxLifetime·idleTimeout을 나란히 비교하고, ss -o state established 등으로 유휴 커넥션이 네트워크 한도보다 오래 유지되는지, 방화벽 세션 테이블에서 그 연결이 만료됐는지 봅니다.

해결: maxLifetime을 네트워크 idle timeout보다 짧게 낮추고, keepaliveTime(주기적 keepalive)을 idle timeout보다 짧게 설정해 중간 장비가 세션을 살아 있는 것으로 유지하게 합니다. OS의 TCP keepalive를 함께 켜면 방어가 두꺼워집니다. 근본적으로는 앱-DB 사이에 세션을 끊는 장비를 없애거나, 네트워크 팀과 idle timeout을 조율합니다.

💼
실무 맥락
현업 패턴

실제 업무에서 이 지식이 쓰이는 상황:

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로 재연결됩니다.


명령어·단축키 빠른 참조

이 모듈에서 다룬 DB 접속 점검·커넥션 풀 진단 명령을 실전 옵션과 함께 모았습니다.

명령어/단축키용도자주 쓰는 예
nc -zv <host> <port>DB 포트 도달·방화벽 확인(첫 단계)nc -zv db-server 3306
mysql -h -u -p -eMySQL 접속·쿼리 테스트mysql -h db -u appuser -p -e "SELECT 1"
psql -h -U -d -cPostgreSQL 접속 테스트psql -h db -U appuser -d mydb -c "SELECT 1"
sqlplusOracle 접속 테스트sqlplus appuser/pw@//db:1521/ORCL
SHOW GRANTS계정 권한 최소화 확인SHOW GRANTS FOR 'appuser'@'%' (DDL 있으면 위험)
SHOW PROCESSLIST실행 중 느린 쿼리 식별Time 큰 쿼리가 커넥션 점유 원인
grep HikariPoolPool 소진 여부 로그 판독active=max, waiting>0이면 소진
grep -r socketTimeout타임아웃 파라미터 누락 점검grep -r 'socketTimeout' /opt/app/config/
jstack <PID>WAS 스레드 블로킹 덤프jstack <PID> | grep -A10 BLOCKED
kill -3 <PID>JVM 스레드 덤프(로그로 출력)스레드가 socketRead0에 묶였는지 확인
SHOW STATUS LIKE 'Threads_connected'DB 측 연결 수 포화 확인max_connections 근접 시 DB 포화
SHOW VARIABLES LIKE 'wait_timeout'max-lifetime 기준값 확인max-lifetime을 이 값보다 짧게

관련 모듈로 더 깊이:

다음 모듈에서는 Redis와 Elasticsearch 접속 확인과 운영 기초를 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

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

Q2

DB 서버를 점검 후 재기동했더니 애플리케이션에서 'Broken pipe' 또는 'Connection closed' 오류가 나오다 잠시 후 정상 복구됩니다. 재기동 직후 Connection 오류를 최소화하려면 HikariCP에 어떤 설정을 추가해야 하는가?

Q3

배치 작업 중 특정 슬로우 쿼리가 30분 이상 실행되면서 Connection을 점유해 WAS 스레드 전체가 멈춘 장애가 발생했습니다. 이런 상황을 방지하려면 어떤 설정을 추가해야 하는가?

Q4

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

Q5

애플리케이션이 DB에 접속하지 못한다. JDBC URL 구조를 떠올리며 가장 먼저 좁혀야 할 단계는?

Q6

Connection Pool 크기(maximumPoolSize)를 무조건 크게 잡는 것이 좋지 않은 이유는?

Q7

[심화] 커넥션 풀의 maximumPoolSize를 크게 늘렸는데 오히려 처리량이 떨어졌다. DB 서버의 max_connections 한도에는 아직 여유가 있다. 가장 근본적인 이유는?

Q8

[심화] 배포도 DB 재기동도 없었는데 트래픽이 뜸한 구간 직후 첫 요청에서만 간헐적으로 Connection reset이 나고, 재시도하면 바로 된다. SELECT 1 검증도 켜져 있다. 가장 유력한 원인은?

0 / 8 답변

🧪 실습으로 확인하기

Nginx 설치 및 기동

초급

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

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

이것도 배워보세요