커넥션 풀이 중요한 이유
MySQL 커넥션 생성은 TCP 3-way 핸드셰이크 + 인증 + 세션 초기화를 포함하는 비용이 큰 작업입니다. 매 요청마다 커넥션을 생성하면 수백 밀리초의 오버헤드가 발생합니다. 커넥션 풀은 미리 생성해둔 커넥션을 재사용하여 이 비용을 제거하지만, 잘못된 풀 설정은 커넥션 고갈, 리소스 낭비, 장애를 유발합니다.
이 글에서는 커넥션 라이프사이클, 최적 풀 크기 공식, MySQL 서버 설정과의 연동, 커넥션 누수 탐지, 그리고 HikariCP·TypeORM 실전 설정까지 심층적으로 다룹니다.
커넥션 라이프사이클
커넥션 풀 내부에서 커넥션은 다음 상태를 순환합니다:
생성(Create) → 유휴(Idle) → 대여(Active) → 반납(Return) → 유휴(Idle)
↓ ↓
만료/제거(Evict) 검증(Validate)
↓
폐기(Destroy)
핵심 파라미터들:
- minimumIdle: 풀에 유지할 최소 유휴 커넥션 수
- maximumPoolSize: 최대 커넥션 수 (유휴 + 활성)
- connectionTimeout: 풀에서 커넥션을 얻기까지 대기 시간
- idleTimeout: 유휴 커넥션 유지 시간 (minimumIdle 초과분)
- maxLifetime: 커넥션 최대 수명 (강제 재생성)
MySQL 서버 설정과 풀의 관계
커넥션 풀 설정은 MySQL 서버 설정과 반드시 연동되어야 합니다. 가장 흔한 장애 원인은 이 둘의 불일치입니다:
-- MySQL 서버 설정 확인
SHOW VARIABLES LIKE 'max_connections'; -- 기본 151
SHOW VARIABLES LIKE 'wait_timeout'; -- 기본 28800 (8시간)
SHOW VARIABLES LIKE 'interactive_timeout'; -- 기본 28800
SHOW VARIABLES LIKE 'connect_timeout'; -- 기본 10초
-- 현재 커넥션 상태
SHOW STATUS LIKE 'Threads_connected'; -- 현재 연결 수
SHOW STATUS LIKE 'Threads_running'; -- 실행 중인 쿼리 수
SHOW STATUS LIKE 'Max_used_connections'; -- 최대 동시 연결 기록
SHOW STATUS LIKE 'Aborted_connects'; -- 실패한 연결 시도
SHOW STATUS LIKE 'Aborted_clients'; -- 비정상 종료된 연결
| MySQL 설정 | 풀 설정 | 관계 |
|---|---|---|
max_connections |
maximumPoolSize |
모든 앱 인스턴스의 합 < max_connections |
wait_timeout |
maxLifetime |
maxLifetime < wait_timeout (30초 이상 여유) |
connect_timeout |
connectionTimeout |
connectionTimeout ≤ connect_timeout |
핵심 규칙: maxLifetime은 MySQL wait_timeout보다 반드시 짧아야 합니다. 그렇지 않으면 풀이 서버에서 이미 끊긴 커넥션을 재사용하여 Communications link failure 에러가 발생합니다.
최적 풀 크기 공식
커넥션 풀 크기는 직관과 다르게 작을수록 성능이 좋은 경우가 많습니다. PostgreSQL 공식 문서에서 제안하는 공식:
// 최적 커넥션 수 공식 (PostgreSQL 위키, MySQL에도 적용 가능)
connections = (core_count * 2) + effective_spindle_count
// 예시: 4코어 서버, SSD 1개
connections = (4 * 2) + 1 = 9
// 예시: 8코어 서버, SSD 1개
connections = (8 * 2) + 1 = 17
앱 인스턴스가 여러 개일 때:
// MySQL max_connections = 200
// 앱 인스턴스 4개 + 모니터링 여유 20개
pool_size_per_instance = (200 - 20) / 4 = 45
// 하지만! 실제로 45가 필요한 경우는 드뭄
// 대부분 10~20이면 충분하고, 그 이상은 오히려 성능 저하
// → DB 서버의 컨텍스트 스위칭 비용 증가
HikariCP 프로덕션 설정 (Spring Boot)
# application.yml
spring:
datasource:
hikari:
# 풀 크기
maximum-pool-size: 15 # 최대 커넥션 수
minimum-idle: 5 # 최소 유휴 커넥션 (= maximum이 이상적)
# 타이밍
connection-timeout: 3000 # 커넥션 대기 최대 3초 (기본 30초는 너무 김)
idle-timeout: 600000 # 유휴 커넥션 10분 후 제거 (minimumIdle 초과분)
max-lifetime: 1800000 # 커넥션 최대 수명 30분
# (MySQL wait_timeout=3600보다 짧게!)
# 검증
connection-test-query: SELECT 1 # MySQL에서는 생략 가능 (JDBC4 isValid 사용)
validation-timeout: 3000 # 검증 쿼리 타임아웃
# 누수 탐지
leak-detection-threshold: 30000 # 30초 이상 반납 안 된 커넥션 경고
# 메트릭
register-mbeans: true # JMX MBean 등록
pool-name: MainHikariPool # 풀 이름 (로그 식별용)
TypeORM 커넥션 풀 설정 (NestJS)
// typeorm.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
export const typeOrmConfig: TypeOrmModuleOptions = {
type: 'mysql',
host: process.env.DB_HOST,
port: 3306,
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
// 커넥션 풀 설정
extra: {
// mysql2 드라이버 풀 옵션
connectionLimit: 15, // 최대 커넥션 수
waitForConnections: true, // 풀 가득 차면 대기
queueLimit: 0, // 대기열 무제한 (0)
enableKeepAlive: true, // TCP Keep-Alive 활성화
keepAliveInitialDelay: 30000, // 30초마다 Keep-Alive 패킷
// 커넥션 유효성 검증
connectTimeout: 3000, // 연결 타임아웃 3초
},
// TypeORM 레벨 설정
poolSize: 15, // extra.connectionLimit와 동일하게
// 로깅 (개발 환경에서만)
logging: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
};
Prisma 커넥션 풀 설정
# .env — Prisma는 URL 파라미터로 풀 설정
DATABASE_URL="mysql://user:pass@host:3306/db?connection_limit=15&pool_timeout=3&connect_timeout=3"
# connection_limit: 최대 커넥션 수 (기본: CPU 코어 수 * 2 + 1)
# pool_timeout: 풀에서 커넥션 대기 시간 (초)
# connect_timeout: MySQL 서버 연결 타임아웃 (초)
// Prisma 커넥션 풀 모니터링
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
log: [
{ level: 'query', emit: 'event' },
{ level: 'warn', emit: 'stdout' },
{ level: 'error', emit: 'stdout' },
],
});
// 느린 쿼리 감지 (500ms 이상)
prisma.$on('query', (e) => {
if (e.duration > 500) {
console.warn(`Slow query (${e.duration}ms): ${e.query}`);
}
});
커넥션 누수 탐지와 해결
커넥션 누수는 커넥션을 빌려간 후 반납하지 않는 것입니다. 풀이 고갈되어 전체 앱이 멈추는 원인이 됩니다:
// HikariCP 누수 탐지 — 30초 이상 반납 안 되면 스택트레이스 출력
spring.datasource.hikari.leak-detection-threshold=30000
// 로그 출력 예시:
// WARN com.zaxxer.hikari.pool.ProxyLeakTask -
// Connection leak detection triggered for connection com.mysql.cj.jdbc.ConnectionImpl@1234,
// on thread http-nio-8080-exec-1, stack trace follows
// java.lang.Exception: Apparent connection leak detected
// at com.example.service.OrderService.processOrder(OrderService.java:45)
// at com.example.controller.OrderController.create(OrderController.java:23)
흔한 누수 패턴과 해결:
// ❌ 누수 패턴: 수동 커넥션 관리에서 예외 시 반납 누락
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
stmt.executeUpdate("INSERT ...");
conn.close(); // 예외 발생 시 여기에 도달하지 못함!
// ✅ 해결: try-with-resources
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
stmt.executeUpdate("INSERT ...");
} // 자동으로 close → 풀에 반납
// ❌ NestJS 누수 패턴: QueryRunner 수동 관리
const qr = dataSource.createQueryRunner();
await qr.connect();
await qr.startTransaction();
await qr.query('INSERT ...');
await qr.commitTransaction();
// release 누락!
// ✅ 해결: finally 블록
const qr = dataSource.createQueryRunner();
try {
await qr.connect();
await qr.startTransaction();
await qr.query('INSERT ...');
await qr.commitTransaction();
} catch (e) {
await qr.rollbackTransaction();
throw e;
} finally {
await qr.release(); // 반드시 반납
}
커넥션 풀 모니터링
-- MySQL 서버에서 커넥션 상태 모니터링
SELECT
user,
host,
db,
command,
time,
state,
info
FROM information_schema.processlist
WHERE command != 'Sleep'
ORDER BY time DESC;
-- 커넥션 통계 요약
SELECT
@@max_connections AS max_conn,
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Threads_connected') AS current_conn,
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Max_used_connections') AS peak_conn,
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Aborted_clients') AS aborted;
// Spring Actuator + Micrometer로 HikariCP 메트릭 수집
// /actuator/metrics/hikaricp.connections.active — 활성 커넥션
// /actuator/metrics/hikaricp.connections.idle — 유휴 커넥션
// /actuator/metrics/hikaricp.connections.pending — 대기 중인 요청
// /actuator/metrics/hikaricp.connections.timeout — 타임아웃 횟수
// Grafana 알림 설정 추천:
// - active/max > 80% → 경고
// - pending > 0 지속 → 위험
// - timeout 증가 → 즉시 확인
마무리
커넥션 풀은 “크게 잡으면 안전하다”는 직관과 달리, 적정 크기를 유지하는 것이 핵심입니다. MySQL wait_timeout과 풀의 maxLifetime을 연동하고, 누수 탐지를 활성화하며, Micrometer 메트릭으로 실시간 모니터링하면 커넥션 풀 관련 장애를 예방할 수 있습니다.
관련 글로 Spring Boot HikariCP 커넥션 풀과 MySQL InnoDB Buffer Pool 튜닝도 함께 참고하세요.