Nest.js를 배워보자

Next.js 서버 헬스 모니터링: 안정적인 서비스 운영을 위한 필수 전략

_Blue_Sky_ 2025. 12. 3. 13:23
728x90

 


🚀 왜 Next.js 서버 헬스 모니터링이 중요할까요?

Next.js는 React 프론트엔드와 Node.js 기반의 서버 기능을 결합한 강력한 프레임워크입니다. 특히 API Routes, Server-Side Rendering (SSR), Server Components 등을 사용하면 애플리케이션의 상당 부분이 서버 환경에서 실행됩니다.
헬스 체크(Health Check)는 애플리케이션이 현재 정상적으로 작동하고 있는지 외부 시스템(로드 밸런서, 모니터링 도구 등)에 알려주는 핵심 메커니즘입니다. 이것이 없으면 장애가 발생했을 때 로드 밸런서가 트래픽을 비정상 서버로 계속 전송하거나, 문제가 발생했는지조차 인지하지 못하고 사용자 경험에 치명적인 영향을 줄 수 있습니다.

주요 이점:

  • 로드 밸런싱(Load Balancing): 비정상 서버를 트래픽 분배 대상에서 자동으로 제외합니다.
  • 자동 복구(Auto-Recovery): 컨테이너 오케스트레이션 도구(예: Kubernetes)에서 비정상 포드를 재시작하는 트리거가 됩니다.
  • 가동 시간(Uptime) 기록: 서비스의 안정성을 객관적인 지표로 확인하고 SLA를 준수합니다.

🛠️ Next.js에서의 헬스 체크 구현 방법 (API Routes 활용)

Next.js에서 가장 간단하고 표준적인 헬스 체크 엔드포인트를 만드는 방법은 API Routes를 활용하는 것입니다.

1. 기본 헬스 체크 엔드포인트 구현

/pages/api/health.js (혹은 App Router 사용 시 /app/api/health/route.js) 파일에 아래와 같이 간단한 API 핸들러를 작성합니다.

 
// pages/api/health.js

/**
 * @param {import('next').NextApiRequest} req
 * @param {import('next').NextApiResponse} res
 */
export default function handler(req, res) {
  // 200 OK 상태 코드와 함께 간단한 JSON 응답을 보냅니다.
  res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
}

이 엔드포인트는 외부에서 접근했을 때 HTTP 상태 코드 200을 반환하며, 이는 서버가 요청을 받고 응답할 수 있는 상태임을 의미합니다.

접근 엔드포인트: YOUR_DOMAIN/api/health

2. 정교한 헬스 체크(Deep Health Check)

단순히 HTTP 요청을 받는 것 외에, 데이터베이스 연결, 외부 캐시(Redis), 종속된 외부 API 서비스 등 주요 구성 요소의 상태를 함께 확인하여 보다 정확한 서비스 상태를 판단할 수 있습니다.

 
// pages/api/deep-health.js (가정)

import db from 'path/to/database';

export default async function handler(req, res) {
  const healthStatus = {
    server: 'ok',
    database: 'pending',
    // redis: 'pending',
    timestamp: new Date().toISOString(),
  };

  try {
    // 예: 데이터베이스 연결 테스트
    await db.query('SELECT 1'); 
    healthStatus.database = 'ok';
    
    // 다른 종속 서비스 체크 로직 추가...
    
    // 모든 체크 통과 시 200 응답
    res.status(200).json(healthStatus);
  } catch (error) {
    // 하나라도 실패하면 503 Service Unavailable 응답
    console.error('Deep health check failed:', error);
    healthStatus.database = 'error';
    res.status(503).json(healthStatus);
  }
}

📊 모니터링 도구 연동 전략

헬스 체크 엔드포인트를 만들었다면, 이제 이 엔드포인트를 정기적으로 확인하고 시각화해 줄 모니터링 도구를 연결해야 합니다.

1. Uptime Kuma/Pingdom/Statuspage (가동 시간 모니터링)

가장 기본적이며 필수적인 도구입니다. 설정된 간격(예: 1분)으로 헬스 체크 엔드포인트(YOUR_DOMAIN/api/health)에 요청을 보내 응답 상태 코드(200)를 확인하고, 실패 시(4xx, 5xx) 알림을 보냅니다.

2. Sentry/APM (오류 및 성능 모니터링)

Next.js 서버에서 발생하는 런타임 오류API 응답 시간 같은 성능 지표를 추적합니다. 헬스 체크 엔드포인트가 정상(200)을 반환하더라도, 내부에서 메모리 누수나 특정 API의 지연이 발생할 수 있습니다. Sentry, DataDog, New Relic 같은 APM(Application Performance Monitoring) 도구를 사용하면 서버의 더 깊은 문제를 진단할 수 있습니다.

3. Prometheus & Grafana (메트릭 수집 및 대시보드)

대규모 환경에서는 Prometheus를 사용해 Next.js 서버의 Node.js 메트릭(CPU 사용량, 메모리 사용량, 이벤트 루프 지연, HTTP 요청 수/지연 시간)을 수집하고, Grafana를 이용해 이를 시각화합니다. Next.js 서버 측에서 prom-client 같은 라이브러리를 사용하여 커스텀 메트릭을 노출하는 엔드포인트를 구현할 수 있습니다.


💡 결론 및 다음 단계

Next.js 서버 헬스 모니터링은 단순히 장애를 감지하는 것을 넘어, 안정적인 아키텍처를 구축하는 첫걸음입니다.

  1. 기본 헬스 체크 엔드포인트(status: 200)를 구현하여 로드 밸런서와 가동 시간 모니터링을 설정합니다.
  2. 딥 헬스 체크를 구현하여 데이터베이스와 주요 외부 서비스의 상태를 통합적으로 점검합니다.
  3. APM 도구(Sentry 등)를 연동하여 서버 런타임 오류와 성능 저하를 능동적으로 파악합니다.

이러한 모니터링 체계를 갖추면 Next.js 기반 서비스의 안정성신뢰성을 크게 향상시킬 수 있습니다.


Prometheus 연동: 커스텀 메트릭 노출 방법

키워드: Prometheus, Grafana, Next.js, Node.js, prom-client, 커스텀 메트릭, 모니터링 엔드포인트


1. ⚙️ 준비 단계: prom-client 설치 및 설정

Next.js는 Node.js 런타임 위에서 실행되므로, Node.js 환경에서 널리 사용되는 Prometheus 클라이언트 라이브러리인 prom-client를 사용합니다.

1-1. 라이브러리 설치

 
npm install prom-client
# 또는
yarn add prom-client

1-2. 메트릭 초기화 유틸리티 작성

Prometheus 메트릭은 전역적으로 등록되어야 합니다. 서버가 여러 번 핫 리로드(Hot Reload)될 때 메트릭이 중복 등록되는 것을 방지하기 위해 초기화 로직을 분리하는 것이 좋습니다.
utils/metrics.js 파일에 다음과 같이 작성합니다.

 
import { Registry, collectDefaultMetrics, Counter, Histogram } from 'prom-client';

// 메트릭 레지스트리 생성
const register = new Registry();

// 기본 Node.js 메트릭 수집 (CPU, Memory, Event Loop Lag 등)
collectDefaultMetrics({ register });

// --- 커스텀 메트릭 정의 ---

// 1. 카운터 (Counter): 누적되는 값 (예: 총 요청 수)
const httpRequestCounter = new Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests processed by Next.js server',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register],
});

// 2. 히스토그램 (Histogram): 요청 처리 시간 및 분포 (예: API 응답 시간)
const httpRequestDurationSeconds = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  // 시간 버킷 정의 (0.1초, 0.5초, 1초, 5초)
  buckets: [0.1, 0.5, 1, 5], 
  registers: [register],
});

// --- 메트릭 객체 내보내기 ---

export { 
  register, 
  httpRequestCounter, 
  httpRequestDurationSeconds 
};

2. 📡 메트릭 노출 엔드포인트 구현

Prometheus 서버가 메트릭을 스크랩(Scrape)할 수 있도록 HTTP 엔드포인트를 만들어야 합니다. 이 엔드포인트는 Prometheus 포맷에 맞는 텍스트 데이터를 반환합니다.
/pages/api/metrics.js (혹은 App Router 사용 시 /app/api/metrics/route.js) 파일에 아래와 같이 작성합니다.

 
// pages/api/metrics.js

import { register } from '../utils/metrics'; // 위에서 작성한 유틸리티 파일 임포트

/**
 * @param {import('next').NextApiRequest} req
 * @param {import('next').NextApiResponse} res
 */
export default async function handler(req, res) {
  try {
    // 1. Prometheus 포맷의 텍스트 데이터 가져오기
    const metrics = await register.metrics();

    // 2. HTTP 헤더 설정
    res.setHeader('Content-Type', register.contentType);
    
    // 3. 메트릭 데이터 응답
    res.status(200).send(metrics);

  } catch (error) {
    console.error('Failed to get metrics:', error);
    res.status(500).end('Internal Server Error while retrieving metrics');
  }
}

접근 엔드포인트: YOUR_DOMAIN/api/metrics

이제 이 엔드포인트에 접속하면 Prometheus가 스크랩할 수 있는 형식의 메트릭 데이터가 출력됩니다.


3. 📝 API 요청에 커스텀 메트릭 로깅 추가

실제 Next.js API Routes나 미들웨어에서 요청이 들어올 때마다 정의한 커스텀 메트릭을 증가시켜야 합니다. 여기서는 모든 API Routes에 적용할 수 있는 미들웨어 패턴을 예시로 들어보겠습니다.

3-1. Next.js 미들웨어 (middleware.js 또는 라우트 핸들러) 활용

Next.js 12 이후에 도입된 middleware.js 파일을 사용하거나, 개별 API 핸들러에서 직접 로직을 추가할 수 있습니다.
API Route 핸들러에 직접 적용하는 예시:

 
// pages/api/users/[id].js

import { httpRequestCounter, httpRequestDurationSeconds } from '../../utils/metrics';

export default async function handler(req, res) {
  // 요청 시작 시간 기록
  const end = httpRequestDurationSeconds.startTimer();
  
  const method = req.method;
  const route = '/api/users/[id]';
  let statusCode = 200; 

  try {
    // 1. 실제 API 로직 처리
    // ... 데이터베이스에서 사용자 정보 가져오기 로직 ...

    if (req.query.id === '404') {
        statusCode = 404;
        res.status(404).json({ message: 'User not found' });
        return;
    }

    res.status(200).json({ id: req.query.id, name: 'Sample User' });
    
  } catch (error) {
    // 2. 오류 발생 시 상태 코드 변경
    statusCode = 500;
    res.status(500).json({ message: 'Internal Server Error' });
  } finally {
    // 3. 요청 카운터 증가
    httpRequestCounter.inc({ method, route, status_code: statusCode });
    
    // 4. 요청 처리 시간 기록 (타이머 종료)
    end({ method, route, status_code: statusCode });
  }
}

4. 🔗 Prometheus 서버 설정 (간략 예시)

Prometheus 서버의 설정 파일 (prometheus.yml)에 Next.js 애플리케이션의 메트릭 엔드포인트를 스크랩하도록 설정합니다.

YAML
 
# prometheus.yml 설정 예시

scrape_configs:
  - job_name: 'nextjs_app'
    # 15초마다 메트릭을 수집
    scrape_interval: 15s 
    static_configs:
      - targets: ['localhost:3000'] # Next.js 서버 주소
        # /api/metrics 엔드포인트로 메트릭을 수집
        metrics_path: '/api/metrics' 

5. 📊 Grafana 대시보드 시각화

Prometheus가 데이터를 수집하기 시작하면, Grafana에서 이 데이터를 사용하여 대시보드를 구축할 수 있습니다.

메트릭 이름 Grafana 쿼리 (PromQL 예시) 설명
API 요청 처리량 rate(http_requests_total[5m]) 5분 동안의 초당 평균 요청 수
API 오류율 sum by (route) (rate(http_requests_total{status_code!~"2.."}[5m])) 5분 동안 2xx가 아닌 상태 코드의 요청 처리율
P95 응답 시간 histogram_quantile(0.95, sum by (le, route) (rate(http_request_duration_seconds_bucket[5m]))) 요청의 95%가 처리되는 데 걸리는 시간 (성능 지표)

이러한 단계를 통해 Next.js 서버의 내부 동작 및 성능을 정확하게 파악하고, Grafana 대시보드를 통해 실시간으로 모니터링할 수 있게 됩니다.
 
 


🛡️ Next.js에 Sentry 통합: 클라이언트-서버 전방위 오류 및 성능 추적

키워드: Sentry, Next.js, 오류 추적, APM, 성능 모니터링, distributed tracing, Session Replay, next.config.js


1. 🔍 Sentry의 역할: 왜 Next.js에 필수인가?

Next.js는 클라이언트(브라우저)와 서버(Node.js, Edge Runtime) 코드가 혼재하는 하이브리드 애플리케이션입니다. 헬스 체크가 서버의 가동 여부를 확인한다면, Sentry는 서버 및 클라이언트에서 발생하는 모든 종류의 런타임 오류를 실시간으로 포착하고 성능 병목 현상(Performance Bottlenecks)을 추적하는 역할을 합니다.

Sentry의 주요 이점:

  • 전방위 오류 커버리지: 클라이언트 측 JS 오류, 서버 측 API Route 오류, SSR 중 발생하는 오류, Edge Runtime 오류까지 모두 추적합니다.
  • 풍부한 Context: 오류 발생 시 사용자 정보, 브라우저 정보, 스택 트레이스(Stack Trace), 그리고 오류 직전 사용자가 취한 동작 경로(Breadcrumbs) 등을 제공합니다.
  • 성능 모니터링 (APM): 페이지 로드 시간, API 응답 시간, 데이터베이스 쿼리 지연 등 서비스의 전반적인 성능을 측정합니다.

2. 🚀 Sentry 통합 단계

Sentry는 Next.js 공식 SDK를 제공하여 통합 과정을 매우 단순화했습니다.

2-1. SDK 설치

Next.js용 Sentry SDK를 설치합니다.

 
npm install @sentry/nextjs
# 또는
yarn add @sentry/nextjs

2-2. 마법사(Wizard)를 통한 설정 (권장)

가장 쉬운 방법은 Sentry CLI 마법사를 사용하는 것입니다. 이는 필요한 모든 설정 파일을 자동으로 생성하고 next.config.js를 수정해 줍니다.

 
npx @sentry/wizard@latest -i nextjs

마법사를 실행하면 DSN(Data Source Name) 키 설정과 함께 다음과 같은 핵심 설정 파일들이 자동으로 생성됩니다.

  • sentry.client.config.js (클라이언트 측 - 브라우저에서 실행)
  • sentry.server.config.js (서버 측 - API Routes, SSR에서 실행)
  • sentry.edge.config.js (Edge Runtime - Middleware에서 실행)

2-3. next.config.js 설정

Sentry는 빌드 과정에서 소스 맵(Source Maps) 업로드, 환경별 설정 로드 등을 위해 Next.js 설정을 래핑해야 합니다. 마법사를 사용하면 자동으로 추가되지만, 수동으로 설정할 경우 다음과 같습니다.

 
// next.config.js

const { withSentryConfig } = require('@sentry/nextjs');

const nextConfig = {
  // 기존 Next.js 설정
  // ...
};

// Sentry 설정을 래핑합니다.
module.exports = withSentryConfig(
  nextConfig, 
  {
    // Sentry 빌드 옵션
    silent: true, // Sentry 로깅 숨기기 (선택 사항)
    org: 'your-sentry-organization',
    project: 'your-sentry-project',
  }
);

2-4. 환경별 초기화 파일 설정

각 환경별 설정 파일(sentry.{runtime}.config.js)에서는 DSN 설정, 샘플링 비율 조정 등 핵심 모니터링 옵션을 정의합니다.

 
// sentry.server.config.js (예시)

import * as Sentry from '@sentry/nextjs';

Sentry.init({
  // 환경 변수를 사용해 DSN 설정 (필수)
  dsn: process.env.SENTRY_DSN || 'YOUR_DSN_HERE', 
  
  // 성능 추적 샘플링 비율 (1.0 = 100% 추적)
  tracesSampleRate: 0.2, // 20%의 요청에 대해 성능 모니터링 수행
  
  // 세션 리플레이 샘플링 비율 (사용자 세션 기록)
  replaysSessionSampleRate: 0.05, // 5%의 세션 기록

  // 환경 구분 (production, staging 등)
  environment: process.env.NEXT_PUBLIC_VERCEL_ENV || 'development',
});

3. 📝 코드 내 오류 수동 추적 (Error Capturing)

Sentry는 대부분의 처리되지 않은(unhandled) 오류를 자동으로 포착하지만, 특정 비즈니스 로직 내에서 발생하는 오류를 수동으로 기록하여 Context를 풍부하게 할 수 있습니다.

3-1. API Route (서버 측) 예시

 
// pages/api/checkout.js

import * as Sentry from '@sentry/nextjs'; 

export default async function handler(req, res) {
  try {
    const orderId = req.body.orderId;
    
    // 1. Context 추가 (오류 발생 시 유용한 데이터)
    Sentry.setContext("order", { id: orderId }); 
    
    // ... 결제 처리 로직 ...
    
    if (!validatePayment(req.body.paymentInfo)) {
      // 2. 중요하지만 throw하지 않은 오류를 수동으로 기록
      Sentry.captureMessage(`Payment Validation Failed for Order: ${orderId}`, 'warning');
      return res.status(400).json({ message: 'Invalid payment details' });
    }
    
    // 3. 치명적인 오류 발생 (자동으로 Sentry에 전송됨)
    throw new Error("Database connection timed out during final commit");

  } catch (error) {
    // 4. 잡은(Caught) 오류를 Sentry에 전송
    Sentry.captureException(error); 
    res.status(500).json({ message: 'Internal Server Error' });
  }
}

3-2. 클라이언트 컴포넌트 (오류 경계, Error Boundary)

React 컴포넌트 트리 내에서 발생하는 렌더링 오류는 Sentry의 ErrorBoundary 컴포넌트를 사용하여 포착하는 것이 표준입니다.

 
// components/SentryErrorBoundary.jsx

import { ErrorBoundary } from "@sentry/nextjs";

function App() {
  return (
    <ErrorBoundary fallback={<div>Something went wrong.</div>}>
      <PageLayout />
    </ErrorBoundary>
  );
}

// _app.js 또는 Layout 파일에 적용

4. 🌐 Next.js APM (Performance Monitoring)

Sentry는 Next.js의 APM 기능을 통해 페이지 로드부터 서버 함수 실행까지 모든 과정을 분산 추적(Distributed Tracing)합니다.

  • 자동 추적: Sentry SDK는 기본적으로 Next.js의 서버 컴포넌트, 서버 사이드 렌더링(SSR), API Routes에 대한 트랜잭션을 자동으로 생성합니다.
  • 병목 현상 식별: Sentry 대시보드에서 특정 API Route의 응답 시간이 얼마나 느린지, 그중 데이터베이스 쿼리가 몇 초를 차지했는지 등 상세한 스팬(Span) 정보를 시각적으로 확인할 수 있습니다.

Sentry를 통합함으로써, 개발자는 사용자 환경에서 발생하는 문제를 실시간으로 파악하고, 재현하기 어려운 버그도 Context 정보를 통해 신속하게 해결할 수 있게 됩니다.


Sentry를 Next.js에 설정하는 방법 이 영상은 Next.js 애플리케이션에서 Sentry를 설정하고 첫 번째 오류를 모니터링하는 과정을 시연합니다.


서버 모니터링: 추가 고려 사항 및 심화 전략

1. 🔑 보안: 헬스 체크 엔드포인트 보호

GET /api/health나 GET /api/metrics 같은 엔드포인트는 서비스의 상태를 외부에 노출합니다. 이 엔드포인트가 무분별하게 악용되거나 불필요하게 트래픽을 유발하는 것을 방지해야 합니다.

  • 접근 제한 (IP Whitelisting): 가장 확실한 방법은 로드 밸런서(L4), 방화벽(WAF), 또는 Ingress Controller 설정에서 Prometheus 서버나 모니터링 도구의 IP 주소에서 오는 요청만 해당 엔드포인트에 접근하도록 허용하는 것입니다.
  • 비밀 키 인증 (Secret Key): 간단한 Bearer Token이나 HTTP Basic Authentication을 사용하여 비밀 키를 아는 모니터링 도구만 접근할 수 있게 할 수 있습니다. Next.js API Route에서 요청 헤더를 검사하여 키가 일치하지 않으면 401 Unauthorized를 반환합니다.

2. 🚦 재시작 준비 및 종료 (Graceful Shutdown)

Next.js 서버가 배포 또는 재시작될 때, 기존 요청을 모두 처리하고 새로운 요청을 받지 않도록 우아하게 종료되는 것이 중요합니다. 이는 헬스 체크와 밀접하게 관련됩니다.

  • Liveness Probe vs. Readiness Probe:
    • Liveness Probe: 현재 서버 프로세스가 살아있는지 확인합니다. 실패 시 컨테이너를 재시작합니다.
    • Readiness Probe: 서버가 트래픽을 받을 준비가 되었는지 확인합니다. 배포 시 헬스 체크가 실패하면 로드 밸런서가 이 인스턴스로 트래픽을 보내지 않습니다.
  • 종료 핸들링: Node.js 환경에서 SIGTERM 신호를 받았을 때, 헬스 체크 엔드포인트가 즉시 503 Service Unavailable을 반환하도록 변경하고, 남아있는 DB 커넥션이나 비동기 작업을 안전하게 마무리할 시간을 벌어주는 로직을 구현해야 합니다. PM2나 Kubernetes 같은 환경에서는 이 과정이 매우 중요합니다.

3. 💾 로그 관리 중앙화 (Centralized Logging)

오류 추적(Sentry)이 '무슨' 문제가 발생했는지 알려준다면, 로그 관리는 '왜' 문제가 발생했는지 추적하는 데 필요한 시간 순서의 기록을 제공합니다.

  • 표준화된 로깅: Next.js 서버(API Routes, SSR)에서 발생하는 모든 console.log, console.error는 JSON 형식으로 출력하도록 표준화하는 것이 좋습니다.
  • Log Aggregation: Fluentd, Logstash, Vector 등을 사용하여 Next.js 서버 인스턴스들의 로그를 한 곳(예: ELK Stack (Elasticsearch, Logstash, Kibana), Datadog, CloudWatch Logs)으로 모읍니다.
  • Contextual Logging: Sentry와 마찬가지로, 요청 ID, 사용자 ID 등 요청 Context를 로그에 포함시켜 특정 요청의 전체 흐름을 따라가며 디버깅할 수 있도록 합니다.

4. 📈 서버 리소스 모니터링 (OS & Runtime Metrics)

Prometheus에서 Node.js 기본 메트릭을 수집했지만, 운영체제(OS) 레벨의 지표를 함께 모니터링해야 합니다.

모니터링 영역 지표 (Metric) 설명
CPU CPU Usage (User/System), Load Average CPU 과부하로 인한 응답 지연 확인
메모리 Memory Utilization, Swap Usage 메모리 누수나 OOM(Out of Memory) 방지
디스크 I/O Disk Latency, Disk Usage 로그 파일 쓰기나 스토리지 접근 병목 현상 확인
네트워크 Network I/O, Dropped Packets 외부 API 통신 문제나 네트워크 부하 확인

이러한 지표는 클라우드 서비스(AWS CloudWatch, GCP Monitoring 등)의 에이전트나 Prometheus의 Node Exporter를 통해 수집하여 Grafana에서 시각화합니다.


결론적으로, 안정적인 Next.js 서비스 운영을 위해서는 헬스 체크 API 구현을 넘어, 보안, 우아한 재시작/종료, 중앙화된 로그 관리, 그리고 OS 리소스 모니터링까지 포괄하는 다층적인 모니터링 전략이 필요합니다.
 


우아한 종료 (Graceful Shutdown) 전략

1. ⚡ 우아한 종료가 중요한 이유

우아한 종료란, 서버 인스턴스를 종료해야 할 때 (예: 배포, 스케일 다운, 시스템 장애 대응) 현재 처리 중인 요청은 안전하게 마무리하고, 더 이상 새로운 요청을 받지 않도록 처리한 후 프로세스를 종료하는 절차를 말합니다.

구분 일반 종료 (강제 종료) 우아한 종료 (Graceful Shutdown)
진행 중인 요청 강제 종료되어 500 오류 발생 완료될 때까지 대기 후 정상 응답
새로운 요청 로드 밸런서가 비정상 서버로 계속 보낼 수 있음 헬스 체크 실패를 통해 즉시 차단
데이터 안정성 DB 트랜잭션 등 데이터 손실 위험 존재 리소스 정리 후 종료하여 데이터 안정성 보장

헬스 체크와의 연관성 (Readiness Probe)

우아한 종료의 핵심은 종료 신호를 받자마자 헬스 체크 엔드포인트의 상태를 변경하는 것입니다.

  1. 배포 시스템(Kubernetes, Vercel 등)이 서버에 SIGTERM 신호를 보냅니다.
  2. 서버는 이 신호를 감지하고 종료 모드로 전환합니다.
  3. /api/health 엔드포인트가 즉시 200 OK에서 503 Service Unavailable로 응답을 변경합니다.
  4. 로드 밸런서/오케스트레이터는 /api/health 응답이 503이 되는 것을 보고 해당 인스턴스로의 트래픽 라우팅을 중단합니다.
  5. 트래픽이 차단된 후, 서버는 남은 작업(진행 중인 요청, DB 연결 종료)을 정리하고 종료합니다.

2. 📝 Next.js Node.js 환경 구현 예시

Next.js의 기본 서버 실행 환경(Node.js)에서 SIGTERM 신호를 처리하고 헬스 체크 상태를 변경하는 로직을 구현합니다.

2-1. 서버 상태 관리 (State Management)

Next.js에서 커스텀 서버를 사용하지 않고도 상태를 관리할 수 있도록, 상태를 저장할 모듈을 만듭니다.

 
// utils/serverStatus.js

let isShuttingDown = false;

export function setShuttingDown(status) {
  isShuttingDown = status;
}

export function getShuttingDownStatus() {
  return isShuttingDown;
}

2-2. 헬스 체크 엔드포인트 수정

SIGTERM 신호가 오면 헬스 체크 상태가 503으로 바뀌도록 수정합니다.

 
// pages/api/health.js

import { getShuttingDownStatus } from '../../utils/serverStatus';

/**
 * 서버 헬스 체크 엔드포인트
 */
export default function handler(req, res) {
  // 서버 종료 중이면 503 반환
  if (getShuttingDownStatus()) {
    return res.status(503).json({ 
      status: 'stopping', 
      message: 'Server is shutting down. Please wait or retry on another instance.' 
    });
  }

  // 정상 작동 중이면 200 OK 반환
  res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
}

2-3. 프로세스 종료 핸들러 (shutdown.js) 구현

process.on('SIGTERM', ...)을 사용하여 종료 신호를 잡고, 우아한 종료 로직을 실행하는 파일을 만듭니다.

 
// shutdown.js

import { setShuttingDown } from './utils/serverStatus';
// 데이터베이스, 메시지 큐 등 핵심 리소스 임포트 (가정)
// import { disconnectDB } from './path/to/db'; 

// 우아한 종료 로직
async function gracefulShutdown(signal) {
  console.log(`\n[Server] Received signal ${signal}. Starting graceful shutdown...`);

  // 1. 헬스 체크 상태 변경 (새 트래픽 차단)
  setShuttingDown(true);
  console.log('[Server] Health check status changed to 503 (Stopping).');

  // 2. 진행 중인 요청 완료 대기 (필요하다면 HTTP 서버 close 로직 추가)
  // Next.js는 보통 next dev/start로 실행되므로, process manager가 이 부분을 처리함. 
  // 그러나 만약 커스텀 서버라면 server.close()를 호출해야 함.

  // 3. 리소스 정리 작업
  try {
    console.log('[Server] Disconnecting from database...');
    // await disconnectDB(); // 예: DB 연결 종료
    // await closeMessageQueue(); // 예: 메시지 큐 연결 종료
    
    // 정리 작업에 5초의 시간 제한을 둡니다.
    await new Promise(resolve => setTimeout(resolve, 5000)); 

    console.log('[Server] All resources cleaned up successfully.');
  } catch (error) {
    console.error('[Server] Resource cleanup failed:', error.message);
    // 실패하더라도 종료는 진행해야 안정성이 확보됨
  }

  // 4. 프로세스 종료
  console.log('[Server] Process exiting.');
  process.exit(0);
}

// 종료 신호 핸들러 등록
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Ctrl+C 등

2-4. 서버 시작 시 핸들러 등록

이 종료 핸들러 코드가 서버 시작 시 한 번 실행되도록 해야 합니다.
방법 1: next start (Package.json)
가장 간단한 방법은 package.json의 start 스크립트를 변경하여 shutdown.js 파일이 Next.js 앱 실행 전에 실행되도록 하는 것입니다.

JSON
 
// package.json (가정)
"scripts": {
  "start": "node -r ./shutdown.js node_modules/.bin/next start"
}

주의: 이 방법은 환경 구성에 따라 복잡해질 수 있습니다. 커스텀 서버를 사용하거나 Docker 환경이라면 진입점 스크립트에 해당 코드를 포함하는 것이 더 확실합니다.

방법 2: 커스텀 서버 (server.js) 사용 시
만약 Next.js를 커스텀 Node.js 서버로 실행한다면, 해당 서버 파일 내에 신호 처리 로직을 직접 통합합니다.

 
// server.js (일부)

const server = http.createServer(handler); // 실제 HTTP 서버 인스턴스
// ... (Next.js 관련 설정) ...

process.on('SIGTERM', () => {
    setShuttingDown(true);
    // 모든 요청이 끝날 때까지 기다린 후 서버 인스턴스 종료
    server.close(async () => {
        await disconnectDB();
        console.log('HTTP server closed. Process exiting.');
        process.exit(0);
    });
});

이러한 우아한 종료 로직모니터링 연동을 통해, 사용자는 서버 종료 또는 배포 과정에서 발생하는 500 오류를 경험하지 않고 서비스의 안정성을 유지할 수 있습니다.


앞서 Next.js에서 Prometheus 연동을 위해 prom-client를 설치하고 커스텀 메트릭을 정의하는 방법을 설명해 드렸습니다. CPU, 메모리 부하와 같은 서버 리소스 모니터링은 이 prom-client를 통해 Node.js 런타임 자체의 상태를 수집하는 것이 핵심입니다.


💻 Next.js 서버 CPU/메모리 부하 모니터링 예제

1. ⚙️ Node.js 런타임 기본 메트릭 수집

가장 먼저 할 일은 prom-client에서 제공하는 기본 메트릭 수집 함수를 활성화하는 것입니다. 이 함수는 Next.js가 실행되는 Node.js 프로세스의 상태를 자동으로 측정합니다.

구현 코드 (utils/metrics.js 재확인)

이전 단계에서 작성한 utils/metrics.js 파일에서 이미 collectDefaultMetrics를 호출하여 기본 메트릭을 활성화했습니다.

JavaScript
 
// utils/metrics.js (재확인)

import { Registry, collectDefaultMetrics } from 'prom-client';

const register = new Registry();

// Node.js 런타임의 CPU, Memory, GC(가비지 컬렉션) 등을 자동으로 수집합니다.
collectDefaultMetrics({ 
  register,
  // Node.js 프로세스 관련 메트릭만 수집하려면 prefix를 사용할 수도 있습니다.
  prefix: 'nodejs_', 
});

// ... (이전에 정의한 커스텀 HTTP 메트릭: httpRequestCounter, httpRequestDurationSeconds)

export { register, /* ... */ };

이 코드가 실행되면, /api/metrics 엔드포인트에 접속했을 때 아래와 같은 중요한 리소스 메트릭들이 자동으로 포함되어 노출됩니다.

2. 📊 핵심 Node.js 리소스 모니터링 지표 (Prometheus Metrics)

collectDefaultMetrics가 수집하는 수많은 지표 중 Next.js 서버의 부하 상태를 판단하는 데 가장 중요한 지표들입니다.

지표 이름 (Metric Name) 유형 설명 부하 판단 기준
nodejs_process_memory_bytes Gauge Node.js 프로세스가 사용 중인 메모리(Heap, RSS) 바이트 heap_used가 지속적으로 증가하면 메모리 누수 의심
nodejs_eventloop_lag_seconds Gauge 이벤트 루프(Event Loop)의 지연 시간 지연 시간이 높을수록 서버가 요청 처리에 막혀있음 (I/O 병목 또는 CPU 집약적 작업)
process_cpu_seconds_total Counter 프로세스가 사용한 누적 CPU 시간 (초 단위) Rate로 변환하여 CPU 사용률을 모니터링
nodejs_gc_runs_total Counter 가비지 컬렉션(GC) 실행 횟수 잦은 GC는 메모리 사용 패턴에 문제가 있음을 시사

3. 📉 Grafana 시각화 및 부하 경고 설정 (PromQL 예제)

수집된 지표를 Grafana에서 PromQL(Prometheus Query Language)을 사용해 시각화하고 경고 임계값(Alert Threshold)을 설정하는 예시입니다.
4. 🌐 OS 레벨 모니터링 (Node Exporter 도입)prom-client는 Node.js 프로세스 내부의 상태만 알려줍니다. 서버 인스턴스 전체의 상태(전체 시스템 CPU 부하, 디스크 I/O, 네트워크 상태 등)를 파악하려면 별도의 도구가 필요합니다.Next.js 서버가 실행되는 리눅스/컨테이너 환경에서 Node Exporter를 실행합니다.

  • Node Exporter: Node.js 애플리케이션과는 완전히 별개로, 서버의 운영체제(OS) 수준의 메트릭을 수집하여 포트 9100 등 별도의 포트로 Prometheus에 노출합니다.
  • 활용 지표: node_cpu_seconds_total (OS 전체 CPU), node_memory_MemTotal_bytes, node_disk_read_bytes_total 등.

Prometheus 설정 (prometheus.yml 예시):Node Exporter를 도입하면 Prometheus 설정 파일에 Next.js 앱 스크랩 외에 새로운 job을 추가해야 합니다. 

# prometheus.yml

scrape_configs:
  - job_name: 'nextjs_app_process' # Next.js 프로세스 (prom-client)
    metrics_path: '/api/metrics'
    static_configs:
      - targets: ['nextjs-server-ip:3000'] 

  - job_name: 'nextjs_app_os' # OS 수준 (Node Exporter)
    metrics_path: '/metrics'
    static_configs:
      - targets: ['nextjs-server-ip:9100'] # Node Exporter 기본 포트

이처럼 Node.js 내부의 런타임 메트릭과 OS 레벨 메트릭을 모두 수집하고 Grafana로 시각화하면, 서버의 부하 원인(코드가 문제인지, 혹은 서버 자원 자체가 부족한지)을 명확하게 진단할 수 있습니다.

모니터링 목표 PromQL 쿼리 설명
CPU 사용률 100 * (rate(process_cpu_seconds_total[5m])) 5분 동안의 Node.js 프로세스 CPU 사용률 (%)
Heap 메모리 사용량 nodejs_process_memory_bytes{job="nextjs_app", lebel="heap_used"} 힙 메모리가 얼마나 사용되고 있는지 절대값으로 확인
이벤트 루프 지연 nodejs_eventloop_lag_seconds 서버가 요청을 즉시 처리하지 못하고 얼마나 지연되는지 확인
경고 예시 nodejs_eventloop_lag_seconds > 0.1 이벤트 루프 지연이 100ms를 초과하면 즉시 알림
728x90