이미지 로딩 중...

Next.js 실전 운영 2편 - PM2로 Next.js 서버 무중단 배포하기 - 슬라이드 1/9
A

AI Generated

2025. 11. 8. · 3 Views

Next.js 실전 운영 2편 - PM2로 Next.js 서버 무중단 배포하기

PM2를 활용한 Next.js 프로덕션 서버의 무중단 배포 전략을 다룹니다. 실시간 서비스 중단 없이 안전하게 배포하는 방법과 프로세스 모니터링, 자동 재시작 설정까지 실무에서 바로 활용할 수 있는 내용을 제공합니다.


목차

  1. PM2 설치 및 기본 설정 - 프로덕션 서버의 첫 단추
  2. PM2 Ecosystem 파일 설정 - 설정을 코드로 관리하기
  3. 무중단 배포 (Graceful Reload) - 사용자에게 영향 없이 배포하기
  4. 프로세스 모니터링과 로그 관리 - 서버 상태를 실시간으로 파악하기
  5. 서버 재부팅 시 자동 시작 설정 - 영구적인 프로세스 관리
  6. 클러스터 모드와 로드밸런싱 - CPU 코어를 최대한 활용하기
  7. 환경변수 관리와 보안 - 민감한 정보를 안전하게 다루기
  8. 메모리 누수 방지와 자동 재시작 - 장기 실행 안정성 확보

1. PM2 설치 및 기본 설정 - 프로덕션 서버의 첫 단추

시작하며

여러분이 Next.js 프로젝트를 개발하고 드디어 배포했는데, 서버가 예고 없이 중단되거나 새로운 버전을 배포할 때마다 서비스가 몇 분씩 멈추는 경험을 해본 적 있나요? 사용자들은 에러 페이지를 보게 되고, 여러분은 급하게 서버를 재시작하느라 식은땀을 흘리게 됩니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. Node.js 프로세스는 예상치 못한 에러나 메모리 누수로 인해 갑자기 종료될 수 있고, 일반적인 npm start 명령어로는 프로세스 관리나 자동 재시작 기능이 없기 때문입니다.

특히 실서비스 환경에서는 24시간 안정적인 운영이 필수적입니다. 바로 이럴 때 필요한 것이 PM2입니다.

PM2는 Node.js 애플리케이션을 위한 프로세스 매니저로, 애플리케이션이 중단되면 자동으로 재시작하고, 무중단 배포를 지원하며, 실시간 모니터링까지 제공합니다.

개요

간단히 말해서, PM2는 Node.js 애플리케이션의 생명주기를 관리하는 프로덕션 레벨의 프로세스 매니저입니다. PM2가 필요한 이유는 실무에서 서버는 단순히 "실행"만으로는 부족하기 때문입니다.

서버가 크래시되면 자동으로 재시작되어야 하고, 새 버전 배포 시 기존 요청을 처리 중인 프로세스는 안전하게 종료되어야 하며, CPU와 메모리 사용량을 실시간으로 모니터링할 수 있어야 합니다. 예를 들어, 메모리 누수로 인해 서버가 멈추더라도 PM2가 자동으로 재시작해서 서비스 중단을 최소화할 수 있습니다.

기존에는 nohup node server.js & 같은 명령어로 백그라운드 실행하거나 systemd로 직접 서비스를 등록했다면, 이제는 PM2 한 줄로 프로세스 관리, 로그 관리, 클러스터 모드, 무중단 재시작까지 모두 해결할 수 있습니다. PM2의 핵심 특징은 첫째, 자동 재시작으로 애플리케이션 크래시 시 즉시 복구되고, 둘째, 클러스터 모드로 CPU 코어 수만큼 프로세스를 생성해 성능을 극대화하며, 셋째, 무중단 배포(Zero-downtime deployment)로 사용자에게 영향 없이 새 버전을 배포할 수 있습니다.

이러한 특징들이 프로덕션 환경에서 안정성과 가용성을 크게 높여줍니다.

코드 예제

# PM2 전역 설치
npm install -g pm2

# Next.js 프로젝트 빌드
npm run build

# PM2Next.js 서버 시작 (프로덕션 모드)
pm2 start npm --name "nextjs-app" -- start

# PM2 프로세스 목록 확인
pm2 list

# 실시간 로그 확인
pm2 logs nextjs-app

# 프로세스 상태 모니터링
pm2 monit

설명

이것이 하는 일: PM2를 전역으로 설치하고, Next.js 애플리케이션을 프로덕션 모드로 실행하며, 프로세스를 모니터링하는 전체 워크플로우를 구성합니다. 첫 번째로, npm install -g pm2로 PM2를 전역 설치합니다.

전역 설치가 필요한 이유는 PM2가 시스템 전체의 프로세스를 관리하는 데몬(daemon)으로 동작하기 때문입니다. 그 다음 npm run build로 Next.js 프로젝트를 프로덕션 빌드하여 최적화된 코드를 .next 디렉토리에 생성합니다.

그 다음으로, pm2 start npm --name "nextjs-app" -- start 명령어가 실행되면서 PM2가 npm start 명령어를 래핑하여 프로세스를 시작합니다. --name 옵션으로 프로세스에 식별 가능한 이름을 부여하고, -- start는 npm 스크립트의 start 명령어를 실행하라는 의미입니다.

내부에서 PM2는 이 프로세스를 감시하며 크래시 시 자동으로 재시작합니다. 마지막으로, pm2 list로 현재 실행 중인 모든 프로세스의 상태(online/stopped/errored), CPU/메모리 사용량, 재시작 횟수 등을 한눈에 확인할 수 있습니다.

pm2 logs는 실시간 로그를 스트리밍하여 에러 추적이 가능하고, pm2 monit은 터미널 기반 대시보드로 더 상세한 모니터링을 제공합니다. 여러분이 이 코드를 사용하면 서버가 예기치 않게 종료되더라도 자동으로 복구되고, 언제든지 프로세스 상태를 확인하고 로그를 추적할 수 있습니다.

실무에서의 이점은 첫째, 서버 관리 시간이 크게 단축되고, 둘째, 장애 발생 시 빠른 원인 파악이 가능하며, 셋째, 프로덕션 환경에서의 안정성이 대폭 향상됩니다.

실전 팁

💡 pm2 savepm2 startup을 함께 사용하면 서버 재부팅 시에도 PM2가 자동으로 시작되고 저장된 프로세스들을 복원합니다. 운영 서버에서는 필수입니다.

💡 프로세스 이름을 명확하게 지정하지 않으면 "npm"이나 "node"로 표시되어 여러 애플리케이션 관리 시 혼란스러울 수 있습니다. 항상 --name 옵션을 사용하세요.

💡 pm2 logs --lines 200으로 최근 로그 200줄을 확인할 수 있어 에러 발생 직전의 상황을 빠르게 파악할 수 있습니다.

💡 개발 환경에서는 PM2보다 npm run dev가 더 편리합니다. PM2는 프로덕션 또는 스테이징 서버에서만 사용하세요.

💡 pm2 delete nextjs-app으로 프로세스를 완전히 제거할 수 있으며, 단순히 pm2 stop은 프로세스를 중지만 할 뿐 PM2 목록에는 남아있습니다.


2. PM2 Ecosystem 파일 설정 - 설정을 코드로 관리하기

시작하며

여러분이 PM2로 여러 개의 Next.js 애플리케이션을 운영하거나, 환경변수와 클러스터 설정을 매번 명령어로 입력하는 것이 번거롭다고 느낀 적 있나요? 배포할 때마다 긴 명령어를 타이핑하다가 오타가 나거나, 팀원마다 다른 설정으로 실행해서 문제가 발생하는 경우도 있습니다.

이런 문제는 설정이 코드화되지 않고 구두로만 전달되거나 문서에만 적혀있을 때 발생합니다. 명령어 기반 설정은 재현성이 낮고, 버전 관리가 어려우며, 복잡한 설정을 표현하는 데 한계가 있습니다.

특히 여러 환경(개발/스테이징/프로덕션)에서 다른 설정을 사용해야 할 때 관리가 매우 어렵습니다. 바로 이럴 때 필요한 것이 PM2 Ecosystem 파일입니다.

JavaScript 또는 JSON 형식의 설정 파일로 모든 PM2 설정을 선언하고, Git으로 버전 관리하며, 한 번에 여러 애플리케이션을 일관되게 실행할 수 있습니다.

개요

간단히 말해서, PM2 Ecosystem 파일은 PM2의 모든 실행 옵션을 코드로 정의하는 설정 파일입니다. Ecosystem 파일이 필요한 이유는 인프라스트럭처를 코드로 관리하는 IaC(Infrastructure as Code) 원칙 때문입니다.

설정을 파일로 관리하면 버전 관리가 가능하고, 코드 리뷰를 통해 설정 변경을 검증할 수 있으며, CI/CD 파이프라인에 쉽게 통합할 수 있습니다. 예를 들어, 프로덕션 서버에서 4개의 인스턴스를 클러스터 모드로 실행하고, 메모리 제한을 2GB로 설정하며, 특정 환경변수를 주입하는 복잡한 설정도 파일 하나로 관리할 수 있습니다.

기존에는 pm2 start ... --instances 4 --max-memory-restart 2G --env production 같은 긴 명령어를 매번 입력했다면, 이제는 pm2 start ecosystem.config.js --env production 한 줄로 모든 설정을 적용할 수 있습니다.

Ecosystem 파일의 핵심 특징은 첫째, 클러스터 모드, 환경변수, 메모리 제한 등 모든 설정을 선언적으로 정의하고, 둘째, 여러 환경(dev/staging/prod)별 설정을 하나의 파일로 관리하며, 셋째, 여러 애플리케이션을 동시에 관리할 수 있습니다. 이러한 특징들이 팀 협업과 배포 자동화를 크게 개선해줍니다.

코드 예제

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'nextjs-app',
    script: 'npm',
    args: 'start',
    instances: 'max', // CPU 코어 수만큼 인스턴스 생성
    exec_mode: 'cluster', // 클러스터 모드
    max_memory_restart: '2G', // 2GB 초과 시 재시작
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    env_staging: {
      NODE_ENV: 'staging',
      PORT: 3001
    },
    error_file: './logs/pm2-error.log',
    out_file: './logs/pm2-out.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
    autorestart: true, // 크래시 시 자동 재시작
    watch: false, // 파일 변경 감지 비활성화 (프로덕션)
    max_restarts: 10, // 1분 내 10회 재시작 시 중단
    min_uptime: '10s' // 10초 이상 실행되어야 정상으로 간주
  }]
};

설명

이것이 하는 일: Next.js 애플리케이션을 클러스터 모드로 실행하고, 환경별 설정을 관리하며, 자동 재시작과 로그 관리를 설정하는 완전한 프로덕션 환경을 구성합니다. 첫 번째로, apps 배열 안에 애플리케이션 설정을 정의합니다.

instances: 'max'exec_mode: 'cluster'는 CPU 코어 수만큼 프로세스를 생성하여 멀티코어를 최대한 활용합니다. 예를 들어 4코어 서버라면 4개의 Next.js 인스턴스가 로드밸런싱되어 처리량이 크게 증가합니다.

max_memory_restart: '2G'는 메모리 누수 방지를 위해 2GB를 초과하면 자동으로 프로세스를 재시작합니다. 그 다음으로, envenv_staging 객체로 환경별 설정을 분리합니다.

pm2 start ecosystem.config.js는 기본 env를 사용하고, pm2 start ecosystem.config.js --env stagingenv_staging을 사용합니다. 내부에서 PM2는 해당 환경변수를 process.env에 주입하여 Next.js 애플리케이션에서 사용할 수 있게 합니다.

세 번째로, 로그 설정인 error_fileout_file로 에러 로그와 일반 로그를 분리하여 저장합니다. log_date_format으로 로그에 타임스탬프가 추가되어 문제 발생 시점을 정확히 추적할 수 있습니다.

로그 파일은 자동으로 로테이션되지 않으므로 pm2 install pm2-logrotate 모듈을 설치하는 것이 좋습니다. 마지막으로, 안정성 설정인 autorestart: true로 크래시 시 자동 재시작하고, max_restarts: 10min_uptime: '10s'로 무한 재시작을 방지합니다.

만약 애플리케이션이 시작 후 10초 이내에 계속 크래시된다면 설정 오류나 심각한 버그가 있는 것이므로 PM2가 재시작을 중단하여 무의미한 리소스 낭비를 막습니다. 여러분이 이 설정을 사용하면 멀티코어 서버의 성능을 최대한 활용하고, 메모리 누수로부터 보호받으며, 환경별 설정을 체계적으로 관리할 수 있습니다.

실무에서의 이점은 첫째, 배포 시 설정 실수가 줄어들고, 둘째, 팀원 모두가 동일한 설정으로 서버를 실행하며, 셋째, Git으로 설정 변경 이력을 추적할 수 있습니다.

실전 팁

💡 instances: 'max' 대신 구체적인 숫자(예: 2)를 지정하여 서버 리소스를 다른 애플리케이션과 공유할 수 있습니다. 모든 코어를 사용하면 다른 프로세스가 리소스 부족을 겪을 수 있습니다.

💡 watch: true는 개발 환경에서만 사용하세요. 프로덕션에서 활성화하면 로그 파일 변경으로도 재시작되어 서비스가 불안정해집니다.

💡 min_uptimemax_restarts를 함께 설정하지 않으면 시작 오류가 있는 애플리케이션이 무한히 재시작되어 CPU를 100% 사용할 수 있습니다.

💡 환경변수를 Ecosystem 파일에 직접 넣지 말고 .env 파일을 사용하거나 dotenv 패키지로 로드하는 것이 보안상 더 안전합니다. 특히 API 키나 비밀번호는 절대 Git에 커밋하지 마세요.

💡 pm2 start ecosystem.config.js --only nextjs-app으로 Ecosystem 파일에 여러 앱이 정의되어 있어도 특정 앱만 시작할 수 있습니다.


3. 무중단 배포 (Graceful Reload) - 사용자에게 영향 없이 배포하기

시작하며

여러분이 새로운 기능을 개발하고 프로덕션에 배포하려는데, 서버를 재시작하는 순간 진행 중이던 사용자 요청이 모두 끊기고 에러가 발생하는 상황을 겪어본 적 있나요? 특히 결제 처리나 파일 업로드 같은 중요한 작업 중에 서버가 재시작되면 사용자에게 치명적인 영향을 미칩니다.

이런 문제는 전통적인 배포 방식에서 필연적으로 발생합니다. 서버를 중단하고 새 코드를 배포한 후 다시 시작하는 동안(보통 10~30초) 모든 요청이 실패하고, 사용자는 503 Service Unavailable 에러를 보게 됩니다.

이는 사용자 경험을 크게 해치고, 비즈니스 크리티컬한 서비스에서는 절대 용납할 수 없는 다운타임입니다. 바로 이럴 때 필요한 것이 PM2의 Graceful Reload(무중단 배포)입니다.

새로운 프로세스를 먼저 시작하고, 기존 프로세스는 진행 중인 요청을 완료한 후에만 종료하여 사용자에게는 전혀 영향을 주지 않고 배포할 수 있습니다.

개요

간단히 말해서, Graceful Reload는 Zero-downtime 배포를 구현하는 PM2의 핵심 기능으로, 새 버전과 구 버전을 중첩 실행하여 서비스 중단 없이 전환하는 메커니즘입니다. 무중단 배포가 필요한 이유는 현대 웹 서비스는 24시간 가용성이 기본 요구사항이기 때문입니다.

사용자는 언제든지 서비스에 접속할 수 있어야 하고, 개발팀은 하루에도 여러 번 배포할 수 있어야 합니다. 예를 들어, 전자상거래 사이트에서 결제 버그를 긴급 수정해야 할 때, 무중단 배포를 사용하면 진행 중인 결제를 방해하지 않고 즉시 패치를 적용할 수 있습니다.

기존에는 블루-그린 배포나 롤링 업데이트를 위해 복잡한 인프라(로드밸런서, 다중 서버)를 구축했다면, 이제는 PM2의 reload 명령어 한 줄로 단일 서버에서도 무중단 배포를 구현할 수 있습니다. Graceful Reload의 핵심 특징은 첫째, 새 프로세스가 준비될 때까지 기존 프로세스가 계속 요청을 처리하고, 둘째, 기존 프로세스는 진행 중인 요청을 모두 완료한 후 종료되며, 셋째, 클러스터 모드에서는 인스턴스를 하나씩 순차적으로 교체하여 최소 1개 이상의 프로세스가 항상 실행 중입니다.

이러한 특징들이 다운타임 없는 배포를 가능하게 합니다.

코드 예제

# 1. 최신 코드 가져오기
git pull origin main

# 2. 의존성 업데이트 (필요한 경우)
npm install

# 3. Next.js 프로덕션 빌드
npm run build

# 4. PM2 무중단 재시작 (기본 설정)
pm2 reload ecosystem.config.js

# 또는 특정 앱만 리로드
pm2 reload nextjs-app

# 5. 배포 후 로그 확인
pm2 logs nextjs-app --lines 50

# Graceful shutdown 설정 (ecosystem.config.js에 추가)
# kill_timeout: 5000 (5초 내 기존 프로세스 종료)
# listen_timeout: 3000 (3초 내 새 프로세스 준비)

설명

이것이 하는 일: Git으로 최신 코드를 가져오고, Next.js를 빌드한 후, PM2가 서비스 중단 없이 새 버전으로 전환하는 완전한 배포 프로세스를 실행합니다. 첫 번째로, git pullnpm install로 최신 코드와 의존성을 가져온 후 npm run build로 프로덕션 빌드를 생성합니다.

이 단계는 PM2와 무관하게 일반적인 배포 프로세스이며, 빌드 중에도 기존 서버는 정상적으로 실행 중입니다. 빌드가 완료되어 .next 디렉토리가 업데이트되면 배포 준비가 완료된 것입니다.

그 다음으로, pm2 reload 명령어가 실행되면 PM2는 클러스터 모드의 인스턴스를 하나씩 순차적으로 교체합니다. 예를 들어 4개의 인스턴스가 있다면, 첫 번째 인스턴스에 SIGINT 시그널을 보내 새 요청을 받지 않도록 하고, 동시에 새로운 인스턴스를 시작합니다.

새 인스턴스가 listen_timeout(기본 3초) 내에 준비되면 첫 번째 구 인스턴스는 kill_timeout(기본 1.6초) 내에 진행 중인 요청을 완료하고 종료됩니다. 세 번째로, Next.js 애플리케이션은 SIGINT 시그널을 받으면 server.close()를 호출하여 새 연결을 거부하고 기존 연결이 종료될 때까지 대기합니다.

이것이 "Graceful Shutdown"의 핵심이며, PM2는 이 프로세스가 완료될 때까지 최대 kill_timeout 시간을 기다립니다. 만약 이 시간을 초과하면 강제로 종료(SIGKILL)합니다.

마지막으로, 나머지 3개의 인스턴스도 동일한 방식으로 하나씩 교체됩니다. 이 전체 과정 동안 최소 3개 이상의 인스턴스가 항상 요청을 처리하고 있으므로 사용자는 전혀 서비스 중단을 느끼지 못합니다.

pm2 logs로 "Graceful reload"와 "Process exited gracefully" 메시지를 확인하여 정상적으로 완료되었는지 검증할 수 있습니다. 여러분이 이 방법을 사용하면 하루에도 여러 번 안전하게 배포할 수 있고, 사용자 경험을 해치지 않으며, 긴급 패치를 즉시 적용할 수 있습니다.

실무에서의 이점은 첫째, 배포 시간대를 새벽으로 제한할 필요가 없고, 둘째, 롤백이 필요할 때도 동일한 방식으로 즉시 가능하며, 셋째, 복잡한 인프라 없이 단일 서버에서도 엔터프라이즈급 배포를 구현할 수 있습니다.

실전 팁

💡 pm2 reload 대신 pm2 restart를 사용하면 모든 프로세스를 동시에 중단했다가 재시작하여 다운타임이 발생합니다. 반드시 reload를 사용하세요.

💡 클러스터 모드가 아닌 단일 인스턴스(instances: 1)에서는 무중단 배포가 불가능합니다. 최소 2개 이상의 인스턴스를 실행해야 합니다.

💡 kill_timeout을 너무 짧게 설정하면 긴 요청(파일 업로드, 복잡한 쿼리)이 강제 종료될 수 있습니다. 애플리케이션의 평균 응답 시간을 고려하여 설정하세요.

💡 Next.js의 API Routes에서 long-running 작업을 처리한다면 SIGINT 핸들러를 직접 구현하여 작업 완료를 보장하세요. PM2는 프로세스 레벨에서만 관리합니다.

💡 배포 스크립트를 작성할 때 pm2 reload의 exit code를 확인하여 실패 시 자동 롤백하는 로직을 추가하면 더 안전합니다.


4. 프로세스 모니터링과 로그 관리 - 서버 상태를 실시간으로 파악하기

시작하며

여러분이 서버를 운영하면서 갑자기 응답이 느려지거나 메모리 사용량이 급증하는데, 정확히 언제부터 문제가 시작되었는지, CPU는 얼마나 사용 중인지, 에러 로그는 어디에 있는지 찾느라 허둥대는 경험을 해본 적 있나요? 문제를 발견했을 때는 이미 사용자들이 불편을 겪고 있고, 원인을 파악하기 위해 여러 파일을 뒤지며 소중한 시간을 낭비하게 됩니다.

이런 문제는 체계적인 모니터링과 로그 관리 시스템이 없을 때 발생합니다. 서버는 24시간 실행되면서 수많은 이벤트와 에러를 발생시키는데, 이를 추적하지 않으면 문제의 원인을 찾을 수 없습니다.

특히 메모리 누수나 CPU 과부하는 서서히 진행되므로 실시간 모니터링 없이는 감지가 거의 불가능합니다. 바로 이럴 때 필요한 것이 PM2의 모니터링과 로그 관리 기능입니다.

CPU/메모리 사용량을 실시간으로 추적하고, 에러 로그와 일반 로그를 분리하여 관리하며, 필요할 때 즉시 확인할 수 있는 통합 시스템을 제공합니다.

개요

간단히 말해서, PM2는 프로세스의 리소스 사용량과 로그를 실시간으로 수집하고 시각화하는 내장 모니터링 시스템을 제공합니다. 모니터링이 필요한 이유는 문제를 사후에 해결하는 것보다 사전에 예방하는 것이 훨씬 효율적이기 때문입니다.

CPU 사용률이 80%를 넘어가는 추세를 미리 파악하면 서버 증설을 준비할 수 있고, 특정 시간대에 메모리가 급증한다면 코드 최적화나 캐싱 전략을 세울 수 있습니다. 예를 들어, 매일 오후 2시에 메모리가 1.5GB에서 2.5GB로 증가한다면 특정 배치 작업이나 트래픽 패턴을 분석하여 대응할 수 있습니다.

기존에는 top, htop 명령어로 전체 시스템 상태를 보거나, 애플리케이션 로그를 tail -f로 추적했다면, 이제는 PM2가 프로세스별로 세분화된 정보를 제공하고, 여러 프로세스의 로그를 통합하여 보여줍니다. PM2 모니터링의 핵심 특징은 첫째, 실시간 CPU/메모리/네트워크 사용량을 프로세스별로 추적하고, 둘째, 로그를 stdout과 stderr로 분리하여 에러 추적을 용이하게 하며, 셋째, 재시작 횟수와 업타임을 기록하여 안정성을 평가할 수 있습니다.

이러한 특징들이 proactive한 서버 관리를 가능하게 합니다.

코드 예제

# 모든 프로세스 목록과 상태 확인
pm2 list

# 실시간 대시보드 (CPU, 메모리, 로그 통합)
pm2 monit

# 특정 프로세스의 상세 정보
pm2 show nextjs-app

# 실시간 로그 스트리밍 (모든 프로세스)
pm2 logs

# 특정 프로세스의 로그만 보기
pm2 logs nextjs-app --lines 100

# 에러 로그만 보기 (stderr)
pm2 logs nextjs-app --err

# 로그 파일 비우기
pm2 flush

# 프로세스 메트릭 리셋 (재시작 횟수 등)
pm2 reset nextjs-app

# JSON 형식으로 프로세스 정보 출력 (스크립트 활용)
pm2 jlist

설명

이것이 하는 일: PM2의 다양한 모니터링 명령어로 프로세스 상태를 실시간으로 확인하고, 로그를 효율적으로 추적하며, 문제 발생 시 빠르게 원인을 파악하는 종합 관찰 시스템을 구축합니다. 첫 번째로, pm2 list는 모든 프로세스의 현재 상태를 테이블 형식으로 보여줍니다.

프로세스 ID, 이름, 모드(fork/cluster), 상태(online/stopped/errored), CPU/메모리 사용률, 재시작 횟수, 업타임이 한눈에 표시됩니다. 예를 들어 재시작 횟수가 비정상적으로 많다면 크래시를 반복하고 있다는 신호이므로 즉시 로그를 확인해야 합니다.

그 다음으로, pm2 monit은 터미널 기반의 실시간 대시보드를 제공합니다. 화면이 좌우로 분할되어 왼쪽에는 프로세스 목록과 리소스 사용량 그래프, 오른쪽에는 선택된 프로세스의 실시간 로그가 스트리밍됩니다.

내부에서 PM2는 1초마다 프로세스 메트릭을 수집하여 업데이트하므로 CPU 스파이크나 메모리 증가를 즉시 감지할 수 있습니다. 세 번째로, pm2 logs 명령어는 모든 프로세스의 stdout과 stderr를 실시간으로 합쳐서 보여줍니다.

--lines 100 옵션으로 최근 100줄만 먼저 출력하고, 이후 새 로그는 계속 스트리밍됩니다. --err 플래그를 사용하면 에러 로그만 필터링하여 문제 해결에 집중할 수 있습니다.

로그는 기본적으로 ~/.pm2/logs/ 디렉토리에 프로세스별로 분리되어 저장됩니다. 마지막으로, pm2 show nextjs-app은 특정 프로세스의 모든 상세 정보를 출력합니다.

실행 경로, 스크립트, 인자, 환경변수, 리소스 제한, 로그 파일 위치, 시작 시간, 재시작 이력 등 디버깅에 필요한 모든 정보가 포함됩니다. pm2 jlist를 사용하면 JSON 형식으로 출력되므로 쉘 스크립트나 모니터링 도구와 연동할 수 있습니다.

여러분이 이 도구들을 활용하면 서버 상태를 실시간으로 파악하고, 문제 발생 시 몇 초 내에 원인을 찾아낼 수 있습니다. 실무에서의 이점은 첫째, 장애 대응 시간이 크게 단축되고, 둘째, 성능 최적화를 위한 데이터를 수집할 수 있으며, 셋째, 외부 모니터링 도구 없이도 기본적인 관찰 가능성(Observability)을 확보할 수 있습니다.

실전 팁

💡 pm2 install pm2-logrotate로 로그 로테이션 모듈을 설치하면 로그 파일이 무한정 커지는 것을 방지할 수 있습니다. 기본적으로 10MB마다 로테이션됩니다.

💡 프로덕션 환경에서는 pm2 logs를 계속 켜두지 말고, 필요할 때만 확인하세요. 로그 스트리밍도 리소스를 소비합니다.

💡 pm2 monit에서 프로세스를 선택하고 r 키를 누르면 즉시 재시작할 수 있어 빠른 대응이 가능합니다.

💡 pm2 jlist | jq '.[] | select(.pm2_env.restart_time > 5)'로 재시작이 5회 이상인 문제 프로세스만 필터링할 수 있습니다(jq 설치 필요).

💡 Ecosystem 파일의 error_fileout_file에 날짜 패턴(예: ./logs/pm2-error-%Y-%m-%d.log)을 사용하면 자동으로 날짜별 로그 파일이 생성됩니다.


5. 서버 재부팅 시 자동 시작 설정 - 영구적인 프로세스 관리

시작하며

여러분이 서버 업데이트나 예기치 않은 재부팅 후에 PM2로 관리하던 애플리케이션들이 모두 사라지고, 서버에 SSH로 접속해서 수동으로 다시 시작해야 하는 경험을 해본 적 있나요? 특히 새벽에 자동 재부팅이 발생하거나 전원 문제로 서버가 꺼졌다가 켜졌을 때, 아침에 출근해서야 서비스가 중단되었다는 것을 알게 되는 최악의 상황도 발생할 수 있습니다.

이런 문제는 PM2 프로세스가 운영체제의 부팅 프로세스와 통합되지 않았기 때문에 발생합니다. PM2로 실행한 프로세스는 현재 세션에만 존재하고, 시스템이 재부팅되면 모든 정보가 사라집니다.

일반적인 시스템 서비스(systemd, init.d)처럼 자동으로 시작되지 않으므로 관리자가 직접 개입해야 합니다. 바로 이럴 때 필요한 것이 PM2의 startup 기능입니다.

운영체제의 init 시스템(systemd, launchd 등)과 PM2를 연결하여 부팅 시 자동으로 PM2 데몬을 시작하고, 저장된 프로세스 목록을 복원하는 완전한 자동화 시스템을 구축합니다.

개요

간단히 말해서, PM2 startup은 서버 재부팅 시 PM2와 관리하던 모든 프로세스를 자동으로 시작하도록 운영체제에 등록하는 기능입니다. startup 설정이 필요한 이유는 프로덕션 서버는 영구적으로 실행되어야 하지만, 서버 하드웨어나 OS는 언제든지 재부팅될 수 있기 때문입니다.

보안 패치, 커널 업데이트, 전원 문제, 하드웨어 장애 복구 등 다양한 이유로 재부팅이 발생하는데, 이때마다 수동으로 애플리케이션을 시작하는 것은 비현실적입니다. 예를 들어, AWS EC2 인스턴스가 유지보수로 재부팅되거나, 쿠버네티스 노드가 교체될 때도 PM2가 자동으로 애플리케이션을 복원해야 합니다.

기존에는 systemd 서비스 파일을 직접 작성하거나, crontab의 @reboot를 사용했다면, 이제는 PM2가 자동으로 운영체제에 맞는 startup 스크립트를 생성하고 등록해줍니다. startup 기능의 핵심 특징은 첫째, OS를 자동 감지하여 적합한 init 시스템(Ubuntu는 systemd, macOS는 launchd)에 등록하고, 둘째, pm2 save로 현재 실행 중인 프로세스 목록을 저장하여 재부팅 시 동일하게 복원하며, 셋째, PM2 데몬 자체도 부팅 시 자동으로 시작됩니다.

이러한 특징들이 진정한 의미의 "무인 운영"을 가능하게 합니다.

코드 예제

# 1. Startup 스크립트 생성 (OS 자동 감지)
pm2 startup

# 위 명령어가 출력하는 명령어를 복사해서 실행 (sudo 권한 필요)
# 예시 (Ubuntu/systemd):
# sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u username --hp /home/username

# 2. 현재 실행 중인 프로세스 목록 저장
pm2 save

# 3. 저장된 프로세스 목록 확인
pm2 list

# 4. Startup 설정 제거 (필요한 경우)
pm2 unstartup

# 5. 서버 재부팅 후 자동으로 PM2와 프로세스들이 시작됨
# 재부팅 후 확인:
pm2 list

# 저장된 프로세스 목록 삭제 (새로 시작하려면)
pm2 delete all
pm2 save --force

설명

이것이 하는 일: 운영체제의 부팅 프로세스에 PM2를 통합하고, 재부팅 후에도 모든 애플리케이션이 자동으로 복원되는 완전한 영속성(persistence)을 구현합니다. 첫 번째로, pm2 startup 명령어를 실행하면 PM2는 현재 운영체제를 감지합니다(Linux의 경우 systemd, macOS는 launchd, FreeBSD는 rcd).

그리고 해당 init 시스템에 PM2를 등록하는 명령어를 생성하여 출력합니다. 이 명령어는 시스템 레벨 설정을 변경하므로 sudo 권한이 필요하며, 사용자 이름과 홈 디렉토리를 올바르게 지정해야 합니다.

그 다음으로, 출력된 명령어를 복사해서 실행하면 systemd의 경우 /etc/systemd/system/pm2-<username>.service 파일이 생성됩니다. 이 서비스 파일은 부팅 시 PM2 데몬을 해당 사용자 권한으로 시작하도록 설정되어 있습니다.

내부적으로 systemctl enable pm2-<username>이 실행되어 부팅 시 자동 시작이 활성화됩니다. 세 번째로, pm2 save 명령어로 현재 실행 중인 모든 프로세스의 정보를 ~/.pm2/dump.pm2 파일에 JSON 형식으로 저장합니다.

이 파일에는 프로세스 이름, 스크립트 경로, 환경변수, 인스턴스 수 등 완전한 복원에 필요한 모든 정보가 담겨 있습니다. PM2 데몬이 시작될 때 이 파일을 읽어 모든 프로세스를 자동으로 재시작합니다.

마지막으로, 서버가 재부팅되면 다음 순서로 복원됩니다. 1) 운영체제 부팅 → 2) init 시스템이 pm2 서비스 시작 → 3) PM2 데몬이 dump.pm2 파일 로드 → 4) 저장된 모든 프로세스를 동일한 설정으로 재시작.

이 전체 과정은 몇 초 내에 자동으로 완료되어 서비스 다운타임을 최소화합니다. 여러분이 이 설정을 완료하면 서버 재부팅을 두려워할 필요가 없고, 새벽에 긴급 호출을 받을 일도 없으며, 진정한 의미의 자동화된 인프라를 구축할 수 있습니다.

실무에서의 이점은 첫째, 관리자의 수동 개입 없이 서버가 스스로 복구되고, 둘째, 재부팅이 필요한 보안 패치를 주저 없이 적용할 수 있으며, 셋째, 클라우드 환경의 인스턴스 교체나 오토스케일링과도 완벽히 호환됩니다.

실전 팁

💡 pm2 save는 프로세스를 추가하거나 설정을 변경할 때마다 실행해야 합니다. 저장하지 않으면 재부팅 시 이전 상태로 복원됩니다.

💡 여러 사용자가 PM2를 사용하는 서버라면 각 사용자마다 pm2 startup을 실행해야 합니다. 사용자별로 독립적인 PM2 데몬이 실행됩니다.

💡 Docker 컨테이너 내에서는 pm2-runtime 명령어를 사용하는 것이 더 적합합니다. startup 설정은 호스트 OS의 init 시스템을 사용하므로 컨테이너와는 맞지 않습니다.

💡 pm2 resurrect로 마지막으로 저장된 프로세스 목록을 수동으로 복원할 수 있어 설정 테스트 시 유용합니다.

💡 클라우드 환경에서 Auto Scaling을 사용한다면 User Data나 Cloud-init 스크립트에 startup 설정을 포함시켜 새 인스턴스가 생성될 때마다 자동으로 설정되도록 하세요.


6. 클러스터 모드와 로드밸런싱 - CPU 코어를 최대한 활용하기

시작하며

여러분이 강력한 8코어 서버를 사용하는데, pm2 list를 확인해보니 Next.js 프로세스 하나만 실행되고 있고 CPU 사용률이 12.5%(1/8 코어)에 불과한 상황을 본 적 있나요? 트래픽이 증가해도 나머지 7개의 코어는 놀고 있고, 결국 하나의 코어만 과부하되어 응답 속도가 느려지는 병목 현상이 발생합니다.

이런 문제는 Node.js의 단일 스레드 특성 때문에 발생합니다. JavaScript는 기본적으로 하나의 CPU 코어만 사용하므로, 아무리 강력한 멀티코어 서버라도 프로세스 하나로는 성능을 제대로 활용할 수 없습니다.

특히 CPU 집약적인 작업(이미지 처리, 대량 데이터 변환 등)이 있다면 병목이 더욱 심각해집니다. 바로 이럴 때 필요한 것이 PM2의 클러스터 모드입니다.

Node.js의 cluster 모듈을 활용하여 여러 개의 프로세스를 생성하고, 들어오는 요청을 자동으로 분산하여 모든 CPU 코어를 최대한 활용하는 내장 로드밸런서를 제공합니다.

개요

간단히 말해서, 클러스터 모드는 애플리케이션의 여러 인스턴스를 동시에 실행하고 PM2가 요청을 자동으로 분산하여 멀티코어 서버의 성능을 극대화하는 기능입니다. 클러스터 모드가 필요한 이유는 현대 서버는 최소 2코어에서 수십 코어까지 보유하지만, 단일 Node.js 프로세스로는 이를 활용할 수 없기 때문입니다.

클러스터 모드를 사용하면 4코어 서버에서 4배, 8코어 서버에서 8배의 처리량을 얻을 수 있습니다. 예를 들어, 단일 프로세스로 초당 1000개 요청을 처리하던 서버가 4개의 인스턴스로 초당 4000개 요청을 처리할 수 있게 됩니다.

기존에는 Nginx나 HAProxy 같은 외부 로드밸런서를 구축하고, 여러 포트에서 애플리케이션을 실행한 후 수동으로 연결했다면, 이제는 PM2가 클러스터 모드 활성화만으로 자동 로드밸런싱과 포트 공유를 제공합니다. 클러스터 모드의 핵심 특징은 첫째, 단일 포트(예: 3000)를 여러 프로세스가 공유하여 외부에서는 단일 서버처럼 보이고, 둘째, 라운드로빈 알고리즘으로 요청을 균등하게 분산하며, 셋째, 한 인스턴스가 크래시되어도 다른 인스턴스들이 계속 요청을 처리하여 가용성이 향상됩니다.

이러한 특징들이 수평적 확장(horizontal scaling)과 고가용성(high availability)을 단일 서버에서 구현합니다.

코드 예제

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'nextjs-cluster',
    script: 'npm',
    args: 'start',
    instances: 'max', // CPU 코어 수만큼 (또는 구체적 숫자: 4)
    exec_mode: 'cluster', // 클러스터 모드 활성화
    wait_ready: true, // 애플리케이션이 준비 신호를 보낼 때까지 대기
    listen_timeout: 10000, // 10초 내 준비되어야 함
    kill_timeout: 5000, // 종료 시 5초 대기
    // 로드밸런싱 최적화 옵션
    max_memory_restart: '1G',
    instance_var: 'INSTANCE_ID' // 각 인스턴스에 고유 ID 환경변수 주입
  }]
};

# 클러스터 모드로 시작
pm2 start ecosystem.config.js

# 인스턴스별 상태 확인 (nextjs-cluster-0, nextjs-cluster-1, ...)
pm2 list

# 특정 인스턴스만 재시작
pm2 reload nextjs-cluster-0

# 인스턴스 수 동적 조정 (4개로 늘리기)
pm2 scale nextjs-cluster 4

설명

이것이 하는 일: Ecosystem 파일로 클러스터 모드를 설정하고, CPU 코어 수만큼 프로세스를 생성하며, 자동 로드밸런싱으로 서버 성능을 극대화하는 고가용성 아키텍처를 구축합니다. 첫 번째로, instances: 'max'는 PM2가 자동으로 CPU 코어 수를 감지하여 동일한 개수의 프로세스를 생성합니다.

예를 들어 4코어 서버라면 nextjs-cluster-0, nextjs-cluster-1, nextjs-cluster-2, nextjs-cluster-3 네 개의 인스턴스가 생성됩니다. exec_mode: 'cluster'를 지정하지 않으면 기본값인 'fork' 모드로 실행되어 로드밸런싱이 작동하지 않으므로 반드시 명시해야 합니다.

그 다음으로, PM2는 Node.js의 cluster 모듈을 사용하여 마스터 프로세스와 워커 프로세스를 생성합니다. 마스터 프로세스는 포트 3000을 리스닝하고, 들어오는 연결을 라운드로빈 방식으로 워커 프로세스들에게 분배합니다.

내부적으로 각 워커는 동일한 코드를 실행하지만 독립적인 메모리 공간을 가지므로, 하나의 워커에서 메모리 누수가 발생해도 다른 워커들은 영향을 받지 않습니다. 세 번째로, wait_ready: truelisten_timeout 설정은 Next.js 서버가 완전히 준비된 후에만 트래픽을 받도록 합니다.

Next.js는 시작 시 빌드 검증, 라우팅 초기화 등을 수행하는데, 이 과정이 완료되기 전에 요청을 받으면 에러가 발생할 수 있습니다. process.send('ready') 신호를 애플리케이션이 보내거나, 포트 리스닝이 시작되면 PM2는 해당 인스턴스를 로드밸런서에 추가합니다.

마지막으로, pm2 scale nextjs-cluster 4 명령어로 실행 중에도 인스턴스 수를 동적으로 조정할 수 있습니다. 트래픽이 증가하면 인스턴스를 추가하고, 감소하면 제거하는 수동 오토스케일링이 가능합니다.

instance_var: 'INSTANCE_ID'로 각 워커에게 고유 ID(0, 1, 2, ...)가 환경변수로 주입되어, 로그나 메트릭에서 인스턴스를 구분할 수 있습니다. 여러분이 클러스터 모드를 사용하면 서버의 모든 CPU 코어를 효율적으로 활용하고, 한 인스턴스의 장애가 전체 서비스에 영향을 주지 않으며, 별도의 로드밸런서 없이도 고성능 아키텍처를 구축할 수 있습니다.

실무에서의 이점은 첫째, 처리량이 코어 수에 비례하여 선형적으로 증가하고, 둘째, 롤링 업데이트 시 최소 1개 인스턴스가 항상 실행되어 무중단 배포가 가능하며, 셋째, 인프라 비용을 절감하면서도 엔터프라이즈급 성능을 제공합니다.

실전 팁

💡 instances: 'max' 대신 instances: -1을 사용하면 (코어 수 - 1)개의 인스턴스를 생성하여 시스템 여유를 남겨둡니다. OS와 다른 프로세스를 위한 배려입니다.

💡 클러스터 모드에서는 인메모리 상태(세션, 캐시 등)가 인스턴스 간 공유되지 않습니다. Redis나 데이터베이스를 사용하여 상태를 외부화하세요.

💡 CPU 코어가 2개 미만인 서버에서는 클러스터 모드의 이점이 거의 없습니다. 오히려 오버헤드만 증가하므로 fork 모드를 사용하세요.

💡 pm2 reload를 사용하면 인스턴스를 하나씩 순차적으로 재시작하여 무중단 배포가 가능하지만, pm2 restart는 모든 인스턴스를 동시에 재시작하여 다운타임이 발생합니다.

💡 Next.js API Routes에서 무거운 계산 작업이 있다면 Worker Threads를 추가로 사용하여 단일 인스턴스 내에서도 병렬 처리를 구현할 수 있습니다.


7. 환경변수 관리와 보안 - 민감한 정보를 안전하게 다루기

시작하며

여러분이 Next.js 프로젝트에 데이터베이스 비밀번호, API 키, JWT 시크릿 같은 민감한 정보를 하드코딩하거나 Ecosystem 파일에 평문으로 넣어서 Git에 커밋한 적 있나요? 혹은 환경변수를 명령어로 매번 입력하다가 오타로 인해 서버가 제대로 작동하지 않는 경험을 해본 적 있나요?

더 심각하게는 실수로 API 키가 포함된 설정 파일을 공개 저장소에 푸시해서 보안 사고가 발생할 수도 있습니다. 이런 문제는 환경변수 관리 전략이 없거나 부적절할 때 발생합니다.

환경변수는 애플리케이션의 동작을 제어하고 민감한 정보를 저장하는 핵심 요소인데, 이를 코드에 섞거나 버전 관리 시스템에 노출하면 보안 취약점이 됩니다. 특히 여러 환경(개발/스테이징/프로덕션)에서 다른 값을 사용해야 할 때 관리가 매우 복잡해집니다.

바로 이럴 때 필요한 것이 체계적인 환경변수 관리 전략입니다. PM2의 Ecosystem 파일과 .env 파일, 그리고 환경 분리 기법을 결합하여 안전하고 유지보수 가능한 방식으로 환경변수를 관리할 수 있습니다.

개요

간단히 말해서, 환경변수 관리는 애플리케이션 설정과 비밀 정보를 코드와 분리하여 안전하게 주입하고, 환경별로 다르게 적용하는 시스템입니다. 환경변수 관리가 필요한 이유는 12 Factor App의 핵심 원칙 중 하나인 "설정을 환경에 저장"하기 때문입니다.

코드는 불변이지만 설정은 환경에 따라 달라야 하므로, 데이터베이스 URL, API 엔드포인트, 기능 플래그 등을 환경변수로 관리하면 코드 변경 없이 배포할 수 있습니다. 예를 들어, 개발 환경에서는 로컬 PostgreSQL을, 프로덕션에서는 AWS RDS를 사용하는 경우 환경변수만 바꾸면 됩니다.

기존에는 .env 파일을 Git에 커밋하거나, 환경변수를 서버에 직접 export로 설정했다면, 이제는 .env.example로 템플릿을 제공하고, 실제 값은 .gitignore로 보호하며, PM2 Ecosystem 파일로 환경별 설정을 관리할 수 있습니다. 환경변수 관리의 핵심 특징은 첫째, 민감한 정보를 코드와 완전히 분리하여 Git 히스토리에 남지 않게 하고, 둘째, 환경별(dev/staging/prod) 설정을 선언적으로 관리하며, 셋째, dotenv 같은 도구로 로컬 개발과 프로덕션 환경을 일관되게 유지합니다.

이러한 특징들이 보안과 유연성을 동시에 제공합니다.

코드 예제

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'nextjs-app',
    script: 'npm',
    args: 'start',
    instances: 2,
    exec_mode: 'cluster',
    // 공통 환경변수 (민감하지 않은 정보만)
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
      // 주의: 민감한 정보는 여기에 넣지 말 것!
    },
    // 스테이징 환경 전용
    env_staging: {
      NODE_ENV: 'staging',
      PORT: 3001,
      API_URL: 'https://api.staging.example.com'
    },
    // .env 파일 사용 (권장)
    env_file: '.env.production'
  }]
};

// .env.production (Git에 커밋하지 말 것! .gitignore에 추가)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=your-super-secret-jwt-key-here
STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxxx
NEXT_PUBLIC_API_URL=https://api.example.com

// .env.example (Git에 커밋하여 팀원들에게 템플릿 제공)
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
JWT_SECRET=your-jwt-secret-here
STRIPE_SECRET_KEY=sk_test_or_live_key
NEXT_PUBLIC_API_URL=https://api.example.com

# .gitignore에 추가
.env
.env.local
.env.production
.env.staging

설명

이것이 하는 일: 민감한 정보를 .env 파일로 분리하고, PM2가 환경별 설정을 자동으로 로드하며, Git에는 템플릿만 저장하여 안전하고 유지보수 가능한 환경변수 관리 시스템을 구축합니다. 첫 번째로, Ecosystem 파일의 env, env_staging, env_production 객체로 환경별 설정을 선언합니다.

pm2 start ecosystem.config.js --env staging으로 실행하면 env의 기본값에 env_staging이 병합되어 process.env에 주입됩니다. 하지만 여기에는 포트 번호나 API URL 같은 비민감 정보만 넣어야 합니다.

비밀번호나 API 키를 Ecosystem 파일에 넣으면 Git에 커밋될 위험이 있습니다. 그 다음으로, env_file: '.env.production' 옵션으로 외부 .env 파일에서 환경변수를 로드합니다.

이 파일에는 데이터베이스 비밀번호, JWT 시크릿, 서드파티 API 키 같은 민감한 정보를 저장하고, .gitignore에 추가하여 절대 Git에 커밋되지 않도록 합니다. 내부적으로 PM2는 이 파일을 파싱하여 key=value 형식의 각 줄을 환경변수로 변환합니다.

세 번째로, .env.example 파일을 Git에 커밋하여 팀원들에게 필요한 환경변수 목록과 형식을 알려줍니다. 실제 값 대신 플레이스홀더나 예시 값을 넣어서, 새로운 팀원이나 CI/CD 시스템이 어떤 환경변수를 설정해야 하는지 쉽게 파악할 수 있습니다.

각 개발자는 이 파일을 복사하여 .env.local을 만들고 실제 값을 채워 넣습니다. 마지막으로, Next.js의 환경변수 규칙을 따라야 합니다.

NEXT_PUBLIC_ 접두사가 붙은 변수만 브라우저에 노출되고, 나머지는 서버 사이드에서만 접근 가능합니다. 예를 들어 DATABASE_URL은 API Routes에서만 사용되고 클라이언트 JavaScript에는 포함되지 않지만, NEXT_PUBLIC_API_URL은 브라우저에서도 process.env.NEXT_PUBLIC_API_URL로 접근할 수 있습니다.

이를 혼동하면 민감한 정보가 클라이언트에 노출될 수 있습니다. 여러분이 이 전략을 사용하면 API 키가 Git 히스토리에 남지 않고, 환경별 설정을 명확히 분리하며, 팀원들과 안전하게 협업할 수 있습니다.

실무에서의 이점은 첫째, 보안 감사를 통과할 수 있는 수준의 비밀 관리가 가능하고, 둘째, 환경 변경 시 코드 수정 없이 배포만으로 적용되며, 셋째, CI/CD 파이프라인에서 환경변수를 동적으로 주입할 수 있습니다.

실전 팁

💡 프로덕션 환경에서는 AWS Secrets Manager, HashiCorp Vault 같은 전문 비밀 관리 도구를 사용하는 것이 더 안전합니다. .env 파일은 로컬 개발이나 소규모 프로젝트에 적합합니다.

💡 환경변수가 제대로 로드되었는지 확인하려면 pm2 show nextjs-app의 "env" 섹션을 확인하세요. 단, 민감한 정보가 노출되므로 프로덕션에서는 주의하세요.

💡 Next.js 빌드 시점(npm run build)에 환경변수가 필요하다면 .env.production을 빌드 전에 생성하거나, Vercel 같은 플랫폼의 환경변수 UI를 사용하세요.

💡 dotenv-cli를 사용하면 dotenv -e .env.staging -- npm start 형식으로 명령어 실행 시 환경변수를 로드할 수 있어 PM2 없이도 테스트할 수 있습니다.

💡 절대 .env 파일을 Docker 이미지에 포함시키지 마세요. 대신 docker run -e나 Docker Compose의 environment 섹션으로 런타임에 주입하세요.


8. 메모리 누수 방지와 자동 재시작 - 장기 실행 안정성 확보

시작하며

여러분이 Next.js 서버를 배포하고 처음 며칠은 잘 작동하다가, 1주일 정도 지나니 점점 느려지고 결국 메모리 부족으로 크래시되는 경험을 해본 적 있나요? 서버를 재시작하면 다시 빨라지지만, 며칠 후 똑같은 문제가 반복됩니다.

메모리 사용량을 모니터링해보니 시간이 지날수록 계속 증가하는 것을 발견하게 됩니다. 이런 문제는 메모리 누수(memory leak)라는 일반적이면서도 해결하기 어려운 버그 때문에 발생합니다.

이벤트 리스너를 제거하지 않거나, 캐시가 무한정 증가하거나, 클로저가 불필요한 참조를 유지하는 등의 코드 패턴이 원인입니다. 근본 원인을 찾아 수정하는 것이 최선이지만, 복잡한 애플리케이션에서 모든 메모리 누수를 완벽히 제거하기는 매우 어렵습니다.

바로 이럴 때 필요한 것이 PM2의 자동 재시작 기능입니다. 메모리 사용량이 임계값을 초과하면 자동으로 프로세스를 재시작하여 메모리를 해제하고, 크래시를 사전에 방지하여 서비스 가용성을 유지합니다.

이것은 임시방편이 아니라 실무에서 필수적인 방어 전략입니다.

개요

간단히 말해서, PM2의 메모리 기반 자동 재시작은 프로세스의 메모리 사용량을 지속적으로 모니터링하다가 설정한 한계에 도달하면 안전하게 재시작하는 메커니즘입니다. 자동 재시작이 필요한 이유는 완벽한 코드는 존재하지 않고, 특히 메모리 누수는 오랜 시간 동안만 드러나는 버그이기 때문입니다.

Node.js의 V8 엔진은 가비지 컬렉션을 자동으로 수행하지만, 여전히 참조되는 객체는 회수할 수 없습니다. 예를 들어, 전역 배열에 계속 데이터를 추가하거나, 웹소켓 연결 객체를 Map에 저장하고 제거하지 않으면 메모리가 계속 증가합니다.

메모리 한계에 도달하면 OOM(Out Of Memory) 에러로 프로세스가 강제 종료되는데, PM2의 자동 재시작은 이를 사전에 방지합니다. 기존에는 Cron으로 매일 밤 서버를 재시작하거나, 모니터링 알림을 받고 수동으로 재시작했다면, 이제는 PM2가 메모리 임계값을 설정하면 자동으로 graceful하게 재시작하여 사용자에게 영향을 주지 않습니다.

자동 재시작의 핵심 특징은 첫째, 메모리 사용량을 주기적으로 체크하여 임계값 초과 시 즉시 대응하고, 둘째, 클러스터 모드에서는 인스턴스를 하나씩 재시작하여 무중단으로 진행되며, 셋째, 재시작 이력이 기록되어 메모리 누수의 심각성을 평가할 수 있습니다. 이러한 특징들이 장기 실행 안정성을 크게 향상시킵니다.

코드 예제

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'nextjs-app',
    script: 'npm',
    args: 'start',
    instances: 2,
    exec_mode: 'cluster',

    // 메모리 기반 자동 재시작 (가장 중요!)
    max_memory_restart: '1G', // 1GB 초과 시 재시작

    // 크래시 재시작 제어
    autorestart: true, // 크래시 시 자동 재시작
    max_restarts: 10, // 1분 내 최대 10회 재시작
    min_uptime: '10s', // 10초 이상 실행되어야 정상으로 간주

    // Cron 기반 재시작 (선택적)
    cron_restart: '0 3 * * *', // 매일 새벽 3시 재시작

    // 재시작 지연 (선택적)
    restart_delay: 4000, // 재시작 간 4초 대기

    // 메모리 모니터링 강화
    pmx: true, // PM2+ 메트릭 활성화
  }]
};

# PM2로 시작
pm2 start ecosystem.config.js

# 재시작 이력 확인 (메모리 누수 진단)
pm2 show nextjs-app | grep -i restart

설명

이것이 하는 일: 메모리 사용량을 지속적으로 모니터링하고, 임계값 초과 시 안전하게 재시작하며, 무한 재시작을 방지하는 다층 방어 시스템을 구축합니다. 첫 번째로, max_memory_restart: '1G' 설정으로 프로세스가 1GB 메모리를 초과하면 PM2가 자동으로 graceful reload를 실행합니다.

PM2는 내부적으로 매 초마다 process.memoryUsage().heapUsed를 체크하여 임계값과 비교합니다. 1GB를 초과하면 새로운 프로세스를 시작하고, 기존 프로세스는 진행 중인 요청을 완료한 후 종료됩니다.

이 값은 서버의 총 메모리와 인스턴스 수를 고려하여 설정해야 합니다. 예를 들어 4GB 서버에서 4개 인스턴스라면 각각 800MB 정도가 적절합니다.

그 다음으로, autorestart: true는 예상치 못한 크래시(예외 처리되지 않은 에러)가 발생했을 때 즉시 프로세스를 재시작합니다. 하지만 max_restartsmin_uptime으로 무한 재시작을 방지합니다.

만약 프로세스가 시작 후 10초 이내에 계속 크래시된다면(예: 데이터베이스 연결 실패), PM2는 1분에 10회 재시도 후 재시작을 중단하여 CPU를 소진하지 않습니다. 이는 시작 오류와 런타임 오류를 구분하는 중요한 메커니즘입니다.

세 번째로, cron_restart 옵션은 예방적 재시작을 스케줄링합니다. 예를 들어 '0 3 * * *'는 매일 새벽 3시에 재시작하여 메모리를 완전히 해제하고 새롭게 시작합니다.

이것은 메모리 누수가 확인되었지만 즉시 수정하기 어려운 경우, 또는 캐시를 주기적으로 비워야 하는 경우에 유용합니다. 클러스터 모드에서는 인스턴스를 순차적으로 재시작하여 다운타임이 없습니다.

마지막으로, restart_delay: 4000으로 재시작 간에 4초의 지연을 두어 연쇄 재시작을 방지합니다. 데이터베이스나 외부 서비스의 일시적 장애로 모든 인스턴스가 동시에 크래시되는 경우, 지연 없이 재시작하면 다시 동시에 크래시되어 서비스가 완전히 중단될 수 있습니다.

지연을 두면 시간차를 두고 재시작하여 최소 1개 인스턴스는 실행 중일 가능성이 높아집니다. 여러분이 이 설정을 적용하면 메모리 누수로 인한 장애를 사전에 차단하고, 예상치 못한 크래시에서 자동으로 복구되며, 장기 실행 안정성이 크게 향상됩니다.

실무에서의 이점은 첫째, 새벽에 서버 크래시로 긴급 호출받는 일이 없어지고, 둘째, 서비스 가용성이 99.9% 이상으로 유지되며, 셋째, 재시작 이력을 분석하여 메모리 누수의 심각성을 측정하고 우선순위를 정할 수 있습니다.

실전 팁

💡 max_memory_restart 값을 너무 낮게 설정하면 정상적인 메모리 사용도 재시작되어 오히려 불안정해집니다. 애플리케이션의 평균 메모리 사용량을 먼저 측정하세요.

💡 메모리 기반 재시작이 자주 발생한다면(하루에 여러 번) 코드에 심각한 메모리 누수가 있다는 신호입니다. 근본 원인을 찾아 수정하는 것이 최우선입니다.

💡 Node.js의 --max-old-space-size 플래그로 V8 힙 크기를 제한할 수 있지만, PM2의 max_memory_restart가 더 안전합니다. V8 제한을 초과하면 즉시 크래시되지만, PM2는 graceful하게 재시작합니다.

💡 pm2 logs에서 "Script ... reached memory limit"라는 메시지를 보면 메모리 기반 재시작이 실행된 것입니다. 이 메시지의 빈도를 추적하세요.

💡 개발 환경에서 메모리 누수를 찾으려면 Chrome DevTools의 Memory Profiler나 clinic.js 같은 도구를 사용하여 힙 스냅샷을 비교하고 증가하는 객체를 찾아내세요.


#Next.js#PM2#무중단배포#프로세스관리#서버운영

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.