홈시리즈

© 2026 Ki Chang. All rights reserved.

본 블로그의 콘텐츠는 CC BY-NC-SA 4.0 라이선스를 따릅니다.

☕후원하기소개JSON Formatter러닝 대기질개인정보처리방침이용약관

© 2026 Ki Chang. All rights reserved.

콘텐츠: CC BY-NC-SA 4.0

☕후원하기
소개|JSON Formatter|러닝 대기질|개인정보처리방침|이용약관

MySQL 커넥션 풀, 설정했다가 뺀 이유 — TCP Keep-Alive와 네트워크 토폴로지

정기창·2026년 3월 24일

발단: Cron Health Check를 제거하면서 생긴 의문

프로덕션 백엔드에서 MySQL HeatWave 연결 상태를 확인하는 Cron 모듈을 정리하고 있었습니다. 매일 09:00 KST에 커넥션을 테스트하고 결과를 Slack으로 알리는 기능이었는데, 알림 코드가 전부 주석 처리된 채 방치되어 있었습니다.

@Cron('0 0 * * *')
async checkMysqlConnection(): Promise<void> {
  // ...
  const connection = await this.pool.getConnection();
  connection.release();
  
  this.logger.log('MySQL HeatWave connection OK');
  // this.notificationService.notify(...) ← 주석 처리됨
}

Cron은 돌고 있었지만 알림은 꺼져 있으니, 사실상 아무도 확인하지 않는 코드였습니다. Grafana Alert으로 대체할 수 있으니 삭제하기로 했습니다.

그런데 삭제하고 나니 한 가지가 걸렸습니다. 커넥션 풀에 TCP Keep-Alive 설정이 없다는 것이었습니다.

TCP Keep-Alive란 무엇인가

TCP Keep-Alive는 유휴 상태인 TCP 커넥션이 아직 살아있는지 확인하는 OS 레벨의 메커니즘입니다. 핵심은 애플리케이션이 아닌 OS 커널이 동작을 수행한다는 점입니다.

Node.js의 mysql2 라이브러리에서는 두 가지 옵션으로 제어합니다.

const pool = createPool({
  host,
  port,
  user,
  password,
  database,
  enableKeepAlive: true,        // TCP Keep-Alive 활성화
  keepAliveInitialDelay: 10000, // 10초 후 첫 프로브 전송
});

enableKeepAlive: true를 설정하면 Node.js가 소켓에 SO_KEEPALIVE 옵션을 켭니다. 그 이후 실제 프로브 전송은 OS 커널의 TCP 스택이 담당합니다.

커넥션 생성
    ↓ (10초 대기 — keepAliveInitialDelay)
첫 TCP Keep-Alive 프로브 전송
    ↓ (OS 기본 간격, Linux ~75초)
두 번째 프로브 전송
    ↓
... 반복 ...
    ↓ (응답 없으면)
커넥션 끊어진 것으로 판단 → 풀에서 제거

프로브는 데이터가 없는 아주 작은 ACK 패킷입니다. MySQL 쿼리가 아니라 TCP 레벨의 신호이므로 커넥션 풀을 점유하지 않고, MySQL 서버에 쿼리 부하도 주지 않습니다.

Linux 커널의 Keep-Alive 기본값

파라미터 기본값 의미
tcp_keepalive_time 7200초 (2시간) 유휴 후 첫 프로브까지 대기
tcp_keepalive_intvl 75초 프로브 간 간격
tcp_keepalive_probes 9회 응답 없을 때 재시도 횟수

keepAliveInitialDelay: 10000은 tcp_keepalive_time을 오버라이드합니다. 나머지 값은 컨테이너의 Linux 커널 기본값을 따릅니다.

TCP Keep-Alive ≠ MySQL 활동

여기서 중요한 구분이 있습니다. TCP Keep-Alive 프로브는 MySQL 서버 입장에서 쿼리 활동으로 카운트되지 않습니다.

TCP Keep-Alive 실제 쿼리
네트워크 경로 유지 O O
MySQL wait_timeout 리셋 X O
클라우드 유휴 판정 방지 가능성 낮음 O

MySQL의 wait_timeout(기본 8시간)은 마지막 쿼리 시점부터 카운트합니다. TCP 프로브를 아무리 보내도 이 타이머는 리셋되지 않습니다. 따라서 TCP Keep-Alive만으로는 MySQL 서버가 유휴 커넥션을 끊는 것을 막을 수 없습니다.

OCI(Oracle Cloud Infrastructure)의 Always Free 리소스 회수 정책도 마찬가지입니다. Oracle이 HeatWave 인스턴스의 활동량을 판단할 때 TCP 프로브는 의미 있는 활동으로 보지 않을 가능성이 높습니다.

그러면 TCP Keep-Alive는 언제 필요한가

TCP Keep-Alive의 진짜 역할은 중간 네트워크 장비가 유휴 커넥션을 조용히 끊는 것을 방지하는 것입니다.

필요한 경우: SSH 터널, NAT, 방화벽 경유

Node.js → SSH 터널 → Tailscale VPN → HeatWave

중간 경로의 장비들은 일정 시간 트래픽이 없는 커넥션을 드롭합니다.
풀은 커넥션이 살아있다고 생각하고 쿼리를 보내면 → ECONNRESET

로컬 개발 환경에서 SSH 터널을 통해 프로덕션 DB에 접속하는 경우가 대표적입니다. 터널이나 VPN 장비가 유휴 커넥션을 정리해버리면, 애플리케이션은 이미 끊어진 커넥션으로 쿼리를 시도하다 ECONNRESET 에러를 받게 됩니다.

불필요한 경우: 같은 VPC 내 직접 연결

Coolify (OCI) → 직접 연결 → HeatWave (OCI)

같은 네트워크 내에서는 중간에 커넥션을 끊을 장비가 없습니다.

프로덕션 서버와 DB가 같은 클라우드 네트워크(VPC) 안에 있다면, 중간에 터널이나 NAT가 없으므로 커넥션이 조용히 끊길 이유가 거의 없습니다.

실제 의사결정 과정

이 글의 배경이 된 실제 상황을 정리하면 이렇습니다.

1단계: Health Check Cron 제거

알림이 주석 처리된 MySQL Health Check 모듈을 삭제했습니다. Cron은 동작했지만, 로그만 남기고 아무에게도 알리지 않는 코드였습니다.

2단계: TCP Keep-Alive 추가

삭제 후 커넥션 풀 설정을 검토하니 enableKeepAlive가 없었습니다. SSH 터널 경유 환경에서 유휴 커넥션 끊김을 방지하기 위해 추가했습니다.

const pool = createPool({
  // ...기존 설정
  enableKeepAlive: true,
  keepAliveInitialDelay: 10000,
});

3단계: 네트워크 토폴로지 확인

그런데 한 가지를 놓치고 있었습니다. 프로덕션 환경의 네트워크 구조를 확인하지 않았던 것입니다.

확인해보니 Coolify(배포 플랫폼)와 HeatWave가 모두 OCI 안에 있었습니다. SSH 터널은 로컬 개발 환경에서만 사용하고, 프로덕션에서는 같은 네트워크 내 직접 연결이었습니다.

4단계: TCP Keep-Alive 제거

같은 OCI 네트워크 내 직접 연결이라면 중간에 커넥션을 끊을 장비가 없습니다. 불필요한 설정이므로 제거했습니다.

판단 기준 정리

MySQL 커넥션 풀에 TCP Keep-Alive를 설정할지 결정할 때, 핵심은 네트워크 경로에 중간 장비가 있는가입니다.

네트워크 환경 enableKeepAlive 이유
SSH 터널 경유 필요 터널이 유휴 커넥션 드롭
VPN(Tailscale 등) 경유 권장 VPN 장비가 세션 정리 가능
NAT/방화벽 경유 권장 NAT 테이블 만료로 커넥션 끊김
같은 VPC 직접 연결 불필요 중간 장비 없음
localhost (로컬 DB) 불필요 네트워크 경유 없음

설정 자체가 해롭지는 않습니다. 같은 VPC에서 켜둬도 부하가 생기지 않습니다. 다만 왜 이 설정이 있는지 모르는 상태로 남겨두는 것은 다른 문제입니다. 나중에 코드를 볼 사람이 "이 설정이 왜 있지?"라고 의문을 가질 수 있고, 불필요한 설정은 코드의 의도를 흐리게 합니다.

돌이켜보며

이번 작업에서 얻은 교훈은 간단합니다. 코드를 수정하기 전에 프로덕션 환경의 네트워크 토폴로지를 먼저 확인하자는 것입니다.

TCP Keep-Alive가 필요한지 여부는 코드만 봐서는 알 수 없습니다. 애플리케이션이 DB에 어떤 경로로 연결되는지, 중간에 어떤 장비가 있는지를 파악해야 판단할 수 있습니다. 같은 코드라도 로컬 개발 환경(SSH 터널 경유)에서는 필요하고, 프로덕션(같은 VPC)에서는 불필요할 수 있습니다.

결국 인프라 설정은 코드 레벨의 판단만으로는 부족하고, 실제 배포 환경의 네트워크 구조까지 고려해야 한다는 생각이 들었습니다.

MySQLTCPNode.jsOCIDevOps

관련 글

macOS에서 autossh로 SSH 터널 자동화하기: 운영 DB 안전하게 접근하는 방법

로컬 개발 환경에서 운영 MySQL에 접근할 때마다 SSH 터널을 수동으로 여는 건 번거롭습니다. autossh와 macOS LaunchAgent를 활용하면 부팅 시 자동으로 터널이 열리고, 끊어져도 재연결됩니다. 포트 충돌 방지와 보안 고려사항까지 정리했습니다.

관련도 90%

OCI HeatWave MySQL + NestJS 연동: Bastion을 통한 보안 연결 가이드

Oracle Cloud HeatWave MySQL을 로컬 개발 환경에서 안전하게 연결하고, NestJS + TypeORM으로 연동하는 전체 과정. Bastion SSH 터널 설정부터 Security List 트러블슈팅까지 실제 경험을 바탕으로 정리했습니다.

관련도 89%

Docker가 내 DB 서브넷을 점유하고 있었다 — OCI 네트워크 충돌 삽질기

Tailscale로 HeatWave에 접속하려는데 No route to host 에러가 발생했습니다. 원인은 Docker가 HeatWave와 같은 IP 대역을 점유하고 있었기 때문입니다. Docker 네트워크 대역 변경과 Coolify 재설치까지의 삽질 기록.

관련도 89%