Node.js 앱이 추하게 죽지 않게 하세요: 완벽한 Graceful Shutdown을 위한 가이드

발행: (2025년 12월 26일 오후 11:43 GMT+9)
8 min read
원문: Dev.to

Source: Dev.to

Nse569h

우리는 CI/CD 파이프라인을 완벽하게 다듬고, 데이터베이스 쿼리를 최적화하며, 깨끗한 코드를 작성하는 데 시간을 많이 투자합니다. 하지만 애플리케이션 라이프사이클에서 죽음이라는 중요한 부분을 종종 간과합니다.

새 버전의 앱을 Kubernetes, Docker Swarm, 혹은 PM2와 같은 오케스트레이터에 배포하면, 오케스트레이터는 실행 중인 프로세스에 SIGTERM 신호를 보냅니다.

이 신호를 처리하지 않으면 Node.js가 즉시 종료될 수 있습니다. 활성 HTTP 요청은 중단되고, 데이터베이스 연결은 열려 있는 채로 남아(고스트 연결) 트랜잭션이 미완료된 상태로 남을 수 있습니다.

오늘은 ds‑express‑errors를 사용해 이 문제를 전문적으로 처리하는 방법을 보여드리며, 특히 Graceful Shutdown API에 대해 깊이 파고들어 보겠습니다.

“Zero‑Dependency” 접근법

저는 주로 오류 매핑을 위해 ds‑express‑errors를 만들었지만, SIGINT를 잡기 위해 추가 의존성을 끌어오는 것이 지겨워서 강력한 종료 관리자를 포함시켰습니다.

이 라이브러리는 의존성이 0개이며, 가볍고 안전합니다.

전체 구성 (기본만이 아니라)

대부분의 튜토리얼은 서버를 종료하는 방법만 보여줍니다. 실제 프로덕션 애플리케이션은 더 많은 것이 필요합니다: **“예정된 종료”(배포)**와 **“크래시”(예외 미포착)**를 구분해야 하며, 종료 동작에 대한 세밀한 제어가 필요합니다.

아래는 initGlobalHandlers에 대한 전체 API 레퍼런스입니다.

설정

패키지 설치

npm install ds-express-errors

파워‑유저 구성 (예: index.js)

const express = require('express');
const mongoose = require('mongoose');
const {
  initGlobalHandlers,
  gracefulHttpClose,
} = require('ds-express-errors');

const app = express();
const server = app.listen(3000);

// The Complete Configuration
initGlobalHandlers({
  // 1️⃣  HTTP Draining Mechanism
  // Wraps server.close() in a promise that waits for active requests to finish.
  closeServer: gracefulHttpClose(server),

  // 2️⃣  Normal Shutdown Logic (SIGINT, SIGTERM)
  // Runs when you redeploy or stop the server manually.
  onShutdown: async () => {
    console.log('SIGTERM received. Closing external connections...');

    // Close DB, Redis, Socket.io, etc.
    await mongoose.disconnect();

    console.log('Cleanup finished. Exiting.');
  },

  // 3️⃣  Crash Handling (Uncaught Exceptions / Unhandled Rejections)
  // Runs when your code throws an error you didn’t catch.
  onCrash: async (error) => {
    console.error('CRITICAL ERROR:', error);

    // Critical: Send alert to Sentry/Slack/PagerDuty immediately
    // await sendAlert(error);
  },

  // 4️⃣  Exit Strategy for Unhandled Rejections
  // Default: true.
  // If false, the process continues running even after an unhandled promise rejection.
  // (Recommended: true, because an unhandled rejection can leave the app in an unstable state)
  exitOnUnhandledRejection: true,

  // 5️⃣  Exit Strategy for Uncaught Exceptions
  // Default: true.
  // If false, the app tries to stay alive after a sync error.
  // (High risk of memory leaks or corrupted state if set to false).
  exitOnUncaughtException: true,
});

옵션 세부 사항

옵션타입설명
closeServerFunction (Async)HTTP 레이어를 처리합니다. 헬퍼 gracefulHttpClose(server)는 abort 신호를 듣고 네이티브 server.close()의 콜백‑헬을 추상화합니다. 기존 요청을 완료하도록 허용하면서 새로운 연결을 차단합니다.
onShutdownFunction (Async)시스템 신호(SIGINT, SIGTERM, SIGQUIT)에 의해서만 트리거되는 비즈니스 로직 정리입니다. 이는 “행복한 종료” 경로입니다. 일반적인 작업:
• 데이터베이스 연결 종료
• 로그 플러시
• 장기 실행 작업 취소
onCrashFunction (Async)uncaughtException 또는 unhandledRejection에 의해 트리거됩니다. 크래시를 정상적인 종료와 다르게 처리할 수 있게 해줍니다(예: 즉시 알림을 보내고 종료).
exitOnUnhandledRejectionBoolean (default true)true이면, 처리되지 않은 거부가 발생한 후 프로세스가 코드 1로 종료됩니다(빠른 실패). 매우 구체적인 복원 전략이 있는 경우에만 false로 설정하세요.
exitOnUncaughtExceptionBoolean (default true)위와 동일하지만 동기식 uncaught 예외에 적용됩니다.

onShutdownonCrash를 구분하나요?

  • onShutdown – 차분하게 리소스를 정상적으로 종료할 시간이 있습니다.
  • onCrash – 상황이 급박합니다; 즉시 알림을 보내고 바로 종료하고 싶을 수 있습니다.

안전망: 타임아웃

만약 onShutdown 로직이 멈춘다면 어떻게 할까요? mongoose.disconnect()가 절대 해결되지 않는다면요? 파드가 “Terminating” 상태에 영원히 머무르는 것을 원하지 않을 겁니다 (Kubernetes가 결국 SIGKILL을 보낼 것입니다).

ds-express-errors는 종료 로직을 10‑second timeout으로 감쌉니다. 정리 작업이 이 제한을 초과하면 라이브러리가 강제로 종료시켜, 컨테이너가 배포 파이프라인을 방해하지 않도록 합니다.

TL;DR

  • ds‑express‑errorsinitGlobalHandlers를 사용하여 무의존성, 프로덕션 준비된 그레이스풀 셧다운 솔루션을 얻으세요.
  • 그레이스풀 셧다운(onShutdown)과 크래시 처리(onCrash)를 분리하세요.
  • 내장된 HTTP 드레인 헬퍼 gracefulHttpClose(server)를 활용하세요.
  • 치명적인 오류 후에도 살아 있어야 할 설득력 있는 이유가 없는 한, 기본 “빠른 실패” 동작(exitOn… = true)을 유지하세요.

이제 Node.js 앱이 우아하게 종료될 수 있어, 사용자들을 만족시키고 인프라를 깔끔하게 유지할 수 있습니다.

시간 내에 해결되지 않는 함수는 강제로 종료되어 좀비 프로세스를 방지합니다.

결론

프로세스 종료를 처리하는 것은 시니어 엔지니어의 일부분입니다. 이는 **“취미 프로젝트”**와 신뢰할 수 있는 분산 시스템을 구분합니다.

ds-express-errors를 사용하면 복잡한 보일러플레이트가 필요 없습니다. 완전한 타입 지원과 의존성 제로 솔루션을 제공하여, 정상적인 종료와 치명적인 충돌을 모두 즉시 처리합니다.

링크

즐거운 코딩 되세요 (그리고 즐거운 종료도!) 🔌

Back to Blog

관련 글

더 보기 »