icon

안동민 개발노트

15장 : 실무 데이터베이스 운영

커넥션 관리


개발 환경에서는 잘 동작하던 DB가 운영 환경에서 터지는 이유 중 하나가 커넥션 관리입니다. 데이터베이스 커넥션은 생성 비용이 비싼 리소스이므로, 효율적으로 관리해야 합니다.


커넥션의 생성 과정

데이터베이스 커넥션을 생성하려면 여러 단계의 네트워크 통신과 인증 과정을 거쳐야 합니다.

커넥션 생성 과정
1. TCP 3-way Handshake (SYN → SYN-ACK → ACK)
2. DB 프로토콜 버전 협상
3. 사용자 인증 (ID/PW 검증)
4. 세션 초기화 (문자셋, 시간대, NLS 설정 등)
5. 서버 측 메모리 할당 (PGA, 세션 메모리)

→ 전체 과정: 20~100ms (네트워크 상태에 따라)
→ 일반적인 쿼리 실행 시간: 1~10ms
→ 커넥션 생성이 쿼리보다 비쌈!

Oracle 전용 서버 모드
  커넥션 1개 = 서버 프로세스 1개
  → 1000개 커넥션 = 1000개 OS 프로세스
  → 메모리: 커넥션당 약 5~10MB PGA

MySQL
  커넥션 1개 = 스레드 1개
  → 스레드당 약 256KB~1MB 메모리

커넥션 풀의 개념

커넥션 풀은 미리 커넥션을 생성해두고 재사용하는 기법입니다. 매 요청마다 커넥션을 생성/종료하는 대신, 풀에서 빌리고 반환합니다.

커넥션 풀 없이 vs 있을 때
커넥션 풀 없이
  요청1 → 커넥션 생성(50ms) → 쿼리(5ms) → 커넥션 종료
  요청2 → 커넥션 생성(50ms) → 쿼리(5ms) → 커넥션 종료
  요청3 → 커넥션 생성(50ms) → 쿼리(5ms) → 커넥션 종료
  → 전체 시간의 90%가 커넥션 생성에 소모!

커넥션 풀 사용
  시작 시 10개 커넥션 미리 생성

  요청1 → 풀에서 빌림(0.1ms) → 쿼리(5ms) → 풀에 반환
  요청2 → 풀에서 빌림(0.1ms) → 쿼리(5ms) → 풀에 반환
  → 커넥션 재사용으로 대기 시간 99% 감소

커넥션 풀 구조

커넥션 풀 내부 구조
┌────────────────────────────────────────┐
│           애플리케이션 서버            │
│                                        │
│  요청 스레드 ──→ ┌──────────────┐      │
│  요청 스레드 ──→ │  커넥션 풀   │      │
│  요청 스레드 ──→ │  ┌─┬─┬─┬─┐   │      │
│  요청 스레드 ──→ │  │C│C│C│C│   │─→ DB │
│  요청 스레드 ──  │  └─┴─┴─┴─┘   │      │
│  (대기...)       │  최소~최대   │      │
│                  └──────────────┘      │
└────────────────────────────────────────┘

C = Connection (미리 생성된 커넥션)

풀 동작 흐름:
  1. 요청 도착 → 풀에서 유휴 커넥션 획득
  2. 커넥션으로 쿼리 실행
  3. 작업 완료 → 커넥션을 풀에 반환 (close가 아님!)
  4. 유휴 커넥션 없으면 → maximumPoolSize까지 새로 생성
  5. 최대치에 도달하면 → connectionTimeout 동안 대기
  6. 타임아웃 초과 → ConnectionTimeoutException 발생

HikariCP — 가장 빠른 커넥션 풀

Spring Boot 2.0+ 기본 커넥션 풀입니다. 가볍고 빠르며 안정적입니다.

파라미터설명기본값권장
minimumIdle최소 유휴 커넥션maximumPoolSizemaximumPoolSize와 동일
maximumPoolSize최대 커넥션 수10CPU 코어 × 2 + 디스크 수
connectionTimeout커넥션 획득 대기30초3~5초
idleTimeout유휴 커넥션 유지600초600초 (10분)
maxLifetime커넥션 최대 수명1800초DB wait_timeout보다 짧게
validationTimeout커넥션 유효성 검사5초5초
leakDetectionThreshold리크 감지 임계값0 (비활성)60000 (60초)
HikariCP 설정 (Spring Boot)
spring:
  datasource:
    hikari:
      minimum-idle: 10
      maximum-pool-size: 10
      connection-timeout: 3000
      idle-timeout: 600000
      max-lifetime: 1740000
      leak-detection-threshold: 60000
      connection-test-query: SELECT 1
maximumPoolSize 산정 공식
HikariCP 공식 가이드
  connections = (core_count * 2) + effective_spindle_count

예시
  CPU 4코어, SSD 1개
  connections = (4 * 2) + 1 = 9 → 약 10개

주의
  → 풀 크기를 너무 크게 잡으면 오히려 성능 저하
  → 컨텍스트 스위칭 비용 증가
  → DB 서버의 총 커넥션 한계 고려

여러 애플리케이션 서버가 있을 경우:
  서버 3대 × 풀 크기 10 = 총 30개 DB 커넥션
  → DB의 max_connections보다 작아야 함

minimumIdle vs maximumPoolSize

minimumIdle 설정 전략
minimumIdle = maximumPoolSize (기본, 권장)
  → 고정 크기 풀. 항상 같은 수의 커넥션 유지
  → 트래픽 급증 시 커넥션 생성 지연 없음

minimumIdle < maximumPoolSize
  → 유동적 풀. 유휴 시 커넥션 해제
  → 리소스 절약 가능하나, 급증 시 커넥션 생성 지연
  → HikariCP는 이 설정을 권장하지 않음

결론: 특별한 이유가 없으면 두 값을 동일하게 설정

다른 커넥션 풀 비교

특징환경
HikariCP가장 빠름, Spring Boot 기본Java
Apache DBCP2안정적, 오래된 프로젝트Java
Tomcat PoolTomcat 내장Java/Tomcat
Oracle UCPOracle 전용 최적화, RAC 지원Oracle
c3p0레거시, 현재 비권장Java
pgBouncer경량 프록시, 외부 풀PostgreSQL
ProxySQLMySQL 프록시, 쿼리 라우팅MySQL

Oracle UCP (Universal Connection Pool)

Oracle은 자체 커넥션 풀인 UCP를 제공합니다. RAC(Real Application Clusters) 환경에서 노드 장애 시 자동으로 다른 노드로 커넥션을 재분배하는 기능이 있습니다.

Oracle UCP 설정
oracle.ucp.minPoolSize=5
oracle.ucp.maxPoolSize=20
oracle.ucp.initialPoolSize=10
oracle.ucp.connectionWaitTimeout=3
oracle.ucp.inactiveConnectionTimeout=300

RAC 환경 추가 설정:
  oracle.ucp.FastConnectionFailoverEnabled=true
  → 노드 장애 시 다른 노드로 자동 전환

pgBouncer (PostgreSQL 외부 풀)

pgBouncer 구성
애플리케이션 → pgBouncer → PostgreSQL

                 └→ 수천 개 앱 커넥션을 수십 개 DB 커넥션으로 다중화

풀링 모드
  session:     세션 단위 (커넥션 반환 = 세션 종료 시)
  transaction: 트랜잭션 단위 (커밋/롤백 시 반환) ← 가장 효율적
  statement:   문장 단위 (매 SQL 후 반환)

예시
  앱 서버 5대 × 풀 50개 = 250 커넥션
  → pgBouncer가 20개 DB 커넥션으로 다중화
  → PostgreSQL은 20개 프로세스만 관리

Oracle processes, sessions 파라미터

Oracle 서버 측에서도 최대 커넥션 수를 제한합니다.

Oracle 세션 설정
-- 현재 설정 확인
SHOW PARAMETER processes;
SHOW PARAMETER sessions;

-- 현재 활성 세션 수 확인
SELECT COUNT(*) FROM v$session WHERE status = 'ACTIVE';

-- 전체 세션 현황
SELECT username, status, COUNT(*)
FROM v$session
WHERE username IS NOT NULL
GROUP BY username, status
ORDER BY COUNT(*) DESC;

-- 변경 (재시작 필요)
ALTER SYSTEM SET processes = 500 SCOPE = SPFILE;
-- sessions는 자동 계산: (1.5 * processes) + 22
파라미터기본값설명
processes150OS 프로세스 최대 수
sessions247동시 세션 최대 수 (자동 계산)
MySQL 커넥션 설정
-- 현재 설정 확인
SHOW VARIABLES LIKE 'max_connections';  -- 기본 151
SHOW STATUS LIKE 'Threads_connected';   -- 현재 연결 수
SHOW STATUS LIKE 'Max_used_connections'; -- 최대 동시 연결 수

-- 변경
SET GLOBAL max_connections = 500;

-- wait_timeout: 유휴 커넥션 자동 종료 시간
SHOW VARIABLES LIKE 'wait_timeout';  -- 기본 28800 (8시간)
SET GLOBAL wait_timeout = 600;       -- 10분
풀 사이즈와 DB 설정 관계
   앱서버1        앱서버2        앱서버3
  pool=10        pool=10        pool=10
     \              |              /
      └───────── DB 서버 ─────────┘
            max_connections=50
            (30 + 여유분 20)

규칙
  모든 앱서버 pool 크기의 합 < DB의 max_connections
  (내부 프로세스, 모니터링, DBA 접속 여유분 확보)

Connection Leak 진단

커넥션을 빌려가서 반환하지 않으면 커넥션 리크(Connection Leak)가 발생합니다. 풀이 고갈되면 모든 요청이 대기 상태에 빠집니다.

증상원인
커넥션 타임아웃 빈발close() 호출 누락
활성 커넥션 수가 계속 증가try-with-resources 미사용
서버 재시작하면 일시적 해소예외 발생 시 finally에서 반환 안 함
특정 기능 사용 후 발생해당 코드에서 커넥션 미반환
Connection Leak 방지
// [나쁜 예] 예외 발생 시 커넥션 미반환
Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.executeQuery();  // 여기서 예외 발생하면?
conn.close();          // 이 줄이 실행되지 않음 → LEAK!

// [좋은 예] try-with-resources로 자동 반환
try (Connection conn = dataSource.getConnection();
     PreparedStatement pstmt = conn.prepareStatement(sql);
     ResultSet rs = pstmt.executeQuery()) {
    while (rs.next()) {
        // 결과 처리
    }
}  // 자동으로 rs, pstmt, conn.close() → 풀에 반환
// 예외가 발생해도 반드시 반환됨
커넥션 리크 탐지 방법
1. HikariCP leakDetectionThreshold
   spring.datasource.hikari.leak-detection-threshold=60000
   → 60초 이상 반환되지 않으면 경고 로그 + 스택트레이스

2. 모니터링 메트릭 확인
   HikariCP가 JMX로 노출하는 메트릭:
   * ActiveConnections: 현재 사용 중인 커넥션 수
   * IdleConnections: 유휴 커넥션 수
   * TotalConnections: 전체 커넥션 수
   * PendingThreads: 커넥션 대기 중인 스레드 수

3. DB 서버에서 확인
   -- Oracle
   SELECT username, program, COUNT(*)
   FROM v$session
   GROUP BY username, program;

   -- MySQL
   SHOW PROCESSLIST;

커넥션 풀 모니터링

핵심 모니터링 지표
정상 상태
  ActiveConnections: 전체의 10~50%
  IdleConnections: 전체의 50~90%
  PendingThreads: 0

주의 상태
  ActiveConnections: 전체의 70~80%
  PendingThreads: 간헐적 발생
  → 풀 크기 증가 또는 쿼리 최적화 검토

위험 상태
  ActiveConnections: 전체의 90%+
  PendingThreads: 지속적 증가
  → 커넥션 리크 의심 또는 풀 크기 부족

Grafana 대시보드 예시
  ┌──────────────────────────────────┐
  │ HikariCP Pool Metrics            │
  │ Active: ██████░░░░ 6/10          │
  │ Idle:   ████░░░░░░ 4/10          │
  │ Pending: 0                       │
  │ Avg Acquire: 0.5ms               │
  └──────────────────────────────────┘

커넥션 유효성 검사

네트워크 장애, DB 재시작 등으로 풀 안의 커넥션이 끊어져 있을 수 있습니다. 유효성 검사로 죽은 커넥션을 미리 제거합니다.

커넥션 유효성 검사 방식
1. connection-test-query (전통적 방식)
   connection-test-query: SELECT 1
   → 커넥션 획득 시 간단한 쿼리 실행
   → 실패하면 커넥션 폐기 후 새로 생성
   → 오버헤드 있음

2. JDBC4 isValid() (권장)
   → JDBC 4.0의 Connection.isValid() 메서드 사용
   → 네트워크 수준 검증 (쿼리 실행 없이)
   → HikariCP는 JDBC4 드라이버면 자동으로 이 방식 사용
   → connection-test-query 설정 불필요

3. maxLifetime 설정
   → 일정 시간이 지난 커넥션을 자동 교체
   → DB의 wait_timeout보다 2~3분 짧게 설정
   → MySQL wait_timeout=28800(8시간) → maxLifetime=28500

4. keepaliveTime (HikariCP 4.0+)
   → 유휴 커넥션에 주기적 핑
   → 방화벽이 유휴 커넥션을 끊는 환경에서 유용

Oracle Shared Server vs Dedicated Server

Oracle은 두 가지 서버 모드를 제공합니다. 커넥션 관리와 직접 관련됩니다.

Dedicated Server (기본)
클라이언트 1 ──→ 서버 프로세스 1
클라이언트 2 ──→ 서버 프로세스 2
클라이언트 3 ──→ 서버 프로세스 3

장점: 각 클라이언트에 전용 프로세스 → 빠른 응답
단점: 커넥션당 1개 프로세스 → 메모리 소비 큼
적합: 커넥션 수가 적고 응답 시간이 중요한 OLTP
Shared Server
                     ┌─ 공유 프로세스 1
클라이언트 1 ──→     │
클라이언트 2 ──→ 디스├─ 공유 프로세스 2
클라이언트 3 ──→ 패처│
클라이언트 4 ──→     └─ 공유 프로세스 3
클라이언트 5 ──→

장점: 적은 프로세스로 많은 커넥션 처리 → 메모리 절약
단점: 디스패처 오버헤드 → 응답 시간 증가
적합: 커넥션 수가 매우 많은 환경 (수천 이상)
Oracle 서버 모드 확인/설정
-- 현재 모드 확인
SELECT server FROM v$session WHERE sid = SYS_CONTEXT('USERENV', 'SID');

-- Shared Server 설정 (init 파라미터)
SHARED_SERVERS = 5         -- 공유 서버 프로세스 수
MAX_SHARED_SERVERS = 20    -- 최대 공유 서버 수
DISPATCHERS = '(PROTOCOL=TCP)(DISPATCHERS=2)'

컨테이너/클라우드 환경의 커넥션 관리

Kubernetes 환경의 커넥션 풀
Pod 스케일링 시 주의

  Pod 1 (pool=10) ──→
  Pod 2 (pool=10) ──→ DB (max_connections=100)
  Pod 3 (pool=10) ──→

  Auto-scaling으로 Pod 10개가 되면:
  10 × 10 = 100 커넥션 → DB 한계에 도달!

대응 방법
  1. 풀 크기를 작게 설정 (3~5개)
  2. 외부 프록시(pgBouncer, ProxySQL) 사용
  3. 서버리스 DB 사용 (Aurora Serverless, PlanetScale)
  4. max_connections을 충분히 확보

Pod 종료(graceful shutdown) 시
  → 커넥션 풀 정리(drain) 후 종료
  → Spring Boot: @PreDestroy 또는 shutdown hook
  → HikariCP: DataSource.close() 호출
연결 풀 공유 아키텍처
방법 1: 애플리케이션 내장 풀 (일반적)
  각 앱 서버마다 독립적인 HikariCP
  → 단순하지만, 총 커넥션 수 관리 어려움

방법 2: 외부 프록시 풀 (중/대규모)
  앱 서버들 → pgBouncer/ProxySQL → DB
  → 커넥션 다중화, 총 커넥션 수 제어 용이
  → 추가 인프라/운영 비용

방법 3: 사이드카 프록시 (K8s)
  각 Pod에 프록시 사이드카 컨테이너
  → Google Cloud SQL Proxy가 대표적
  → IAM 인증 자동 처리, TLS 자동 설정

커넥션 풀 트러블슈팅

자주 발생하는 문제와 해결
문제 1: "Cannot acquire connection from pool"
  원인: 풀 고갈 (리크 또는 풀 크기 부족)
  진단: ActiveConnections == maximumPoolSize인지 확인
  해결: leakDetectionThreshold 활성화, Slow Query 확인

문제 2: "Connection is not available, request timed out"
  원인: connectionTimeout 내에 커넥션 획득 실패
  진단: PendingThreads 수 확인
  해결: connectionTimeout 증가, 풀 크기 증가, 쿼리 최적화

문제 3: "Connection reset by peer"
  원인: 방화벽/DB가 유휴 커넥션 끊음
  진단: maxLifetime > DB wait_timeout인지 확인
  해결: maxLifetime을 DB wait_timeout보다 짧게 설정

문제 4: "Too many connections" (MySQL)
  원인: 모든 앱서버 풀 합 > max_connections
  진단: SHOW STATUS LIKE 'Max_used_connections';
  해결: 풀 크기 축소, max_connections 증가, pgBouncer 도입

문제 5: "ORA-12519: TNS: no appropriate service handler found"
  원인: Oracle processes/sessions 한계 초과
  진단: SELECT COUNT(*) FROM v$session;
  해결: processes 파라미터 증가, Shared Server 검토

문제 6: 간헐적 쿼리 실패
  원인: 풀 안의 커넥션이 DB 재시작으로 끊어짐
  진단: 앱 로그에서 "Broken pipe" 확인
  해결: validationTimeout 설정, maxLifetime 단축
커넥션 풀 설정 절차
1. 초기 설정
   maximumPoolSize = (CPU코어 × 2) + 디스크 수
   minimumIdle = maximumPoolSize
   connectionTimeout = 3000ms
   maxLifetime = DB의 wait_timeout - 120초
   leakDetectionThreshold = 60000ms

2. 부하 테스트
   → 예상 동시 사용자 수로 부하 테스트
   → ActiveConnections, PendingThreads, 응답 시간 측정
   → connectionTimeout 발생 횟수 확인

3. 최적화
   → PendingThreads가 빈번하면 풀 크기 증가
   → ActiveConnections가 항상 낮으면 풀 크기 감소
   → Slow Query가 많으면 풀 대신 쿼리 최적화

4. 운영
   → Grafana로 실시간 모니터링
   → 알림 설정 (PendingThreads > 0 지속 시)
   → 정기적 메트릭 리뷰

정리

커넥션 풀 설정의 핵심은 적절한 크기리크 방지입니다. 풀이 너무 작으면 대기가 발생하고, 너무 크면 DB 리소스를 낭비합니다. HikariCP의 공식에 따라 (CPU 코어 × 2 + 디스크 수)를 기본으로 하고, 부하 테스트를 통해 최적값을 찾아야 합니다.

모든 커넥션은 try-with-resources로 반드시 반환하고, leakDetectionThreshold를 활성화하여 리크를 조기에 탐지해야 합니다. 여러 앱 서버의 풀 크기 합이 DB의 max_connections를 초과하지 않도록 전체 아키텍처 관점에서 관리하는 것이 중요합니다.

시험에서는 커넥션 풀의 개념과 필요성, 주요 설정 파라미터(maximumPoolSize, connectionTimeout, maxLifetime), 커넥션 리크의 원인과 방지 방법이 자주 출제됩니다.

다음 절에서는 모니터링과 성능 진단을 다루겠습니다.

목차