MySQL Connection Pool 운영 심화

커넥션 풀이 중요한 이유

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 튜닝도 함께 참고하세요.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux