커넥션 관리
개발 환경에서는 잘 동작하던 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 메모리커넥션 풀의 개념
커넥션 풀은 미리 커넥션을 생성해두고 재사용하는 기법입니다. 매 요청마다 커넥션을 생성/종료하는 대신, 풀에서 빌리고 반환합니다.
커넥션 풀 없이
요청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 | 최소 유휴 커넥션 | maximumPoolSize | maximumPoolSize와 동일 |
| maximumPoolSize | 최대 커넥션 수 | 10 | CPU 코어 × 2 + 디스크 수 |
| connectionTimeout | 커넥션 획득 대기 | 30초 | 3~5초 |
| idleTimeout | 유휴 커넥션 유지 | 600초 | 600초 (10분) |
| maxLifetime | 커넥션 최대 수명 | 1800초 | DB wait_timeout보다 짧게 |
| validationTimeout | 커넥션 유효성 검사 | 5초 | 5초 |
| leakDetectionThreshold | 리크 감지 임계값 | 0 (비활성) | 60000 (60초) |
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 1HikariCP 공식 가이드
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 = maximumPoolSize (기본, 권장)
→ 고정 크기 풀. 항상 같은 수의 커넥션 유지
→ 트래픽 급증 시 커넥션 생성 지연 없음
minimumIdle < maximumPoolSize
→ 유동적 풀. 유휴 시 커넥션 해제
→ 리소스 절약 가능하나, 급증 시 커넥션 생성 지연
→ HikariCP는 이 설정을 권장하지 않음
결론: 특별한 이유가 없으면 두 값을 동일하게 설정다른 커넥션 풀 비교
| 풀 | 특징 | 환경 |
|---|---|---|
| HikariCP | 가장 빠름, Spring Boot 기본 | Java |
| Apache DBCP2 | 안정적, 오래된 프로젝트 | Java |
| Tomcat Pool | Tomcat 내장 | Java/Tomcat |
| Oracle UCP | Oracle 전용 최적화, RAC 지원 | Oracle |
| c3p0 | 레거시, 현재 비권장 | Java |
| pgBouncer | 경량 프록시, 외부 풀 | PostgreSQL |
| ProxySQL | MySQL 프록시, 쿼리 라우팅 | MySQL |
Oracle UCP (Universal Connection Pool)
Oracle은 자체 커넥션 풀인 UCP를 제공합니다. RAC(Real Application Clusters) 환경에서 노드 장애 시 자동으로 다른 노드로 커넥션을 재분배하는 기능이 있습니다.
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 → PostgreSQL
│
└→ 수천 개 앱 커넥션을 수십 개 DB 커넥션으로 다중화
풀링 모드
session: 세션 단위 (커넥션 반환 = 세션 종료 시)
transaction: 트랜잭션 단위 (커밋/롤백 시 반환) ← 가장 효율적
statement: 문장 단위 (매 SQL 후 반환)
예시
앱 서버 5대 × 풀 50개 = 250 커넥션
→ pgBouncer가 20개 DB 커넥션으로 다중화
→ PostgreSQL은 20개 프로세스만 관리Oracle processes, sessions 파라미터
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| 파라미터 | 기본값 | 설명 |
|---|---|---|
| processes | 150 | OS 프로세스 최대 수 |
| sessions | 247 | 동시 세션 최대 수 (자동 계산) |
-- 현재 설정 확인
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분 앱서버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 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은 두 가지 서버 모드를 제공합니다. 커넥션 관리와 직접 관련됩니다.
클라이언트 1 ──→ 서버 프로세스 1
클라이언트 2 ──→ 서버 프로세스 2
클라이언트 3 ──→ 서버 프로세스 3
장점: 각 클라이언트에 전용 프로세스 → 빠른 응답
단점: 커넥션당 1개 프로세스 → 메모리 소비 큼
적합: 커넥션 수가 적고 응답 시간이 중요한 OLTP ┌─ 공유 프로세스 1
클라이언트 1 ──→ │
클라이언트 2 ──→ 디스├─ 공유 프로세스 2
클라이언트 3 ──→ 패처│
클라이언트 4 ──→ └─ 공유 프로세스 3
클라이언트 5 ──→
장점: 적은 프로세스로 많은 커넥션 처리 → 메모리 절약
단점: 디스패처 오버헤드 → 응답 시간 증가
적합: 커넥션 수가 매우 많은 환경 (수천 이상)-- 현재 모드 확인
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)'컨테이너/클라우드 환경의 커넥션 관리
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), 커넥션 리크의 원인과 방지 방법이 자주 출제됩니다.
다음 절에서는 모니터링과 성능 진단을 다루겠습니다.