본문 바로가기
C.W.K.
Stream
Lesson 04 of 05 · published

배포 패턴 — launchd, PM2, SEA, systemd

~12 min · production, deploy, launchd, pm2

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Node 서비스는 실제로 어디서 돌아갈까? 'AWS'라는 이름 아래에서도 누군가는 프로세스를 시작하고, 죽으면 다시 띄우고, 계속 살아 있게 관리해야 해. 어떤 바닥 위에서 도는지 알아야 운영할 수 있어."

배포 환경이 맡아야 할 네 가지

운영 중인 Node 서비스에는 다음 장치가 필요해.

  1. 컴퓨터가 켜질 때 프로세스를 시작하는 장치.
  2. 프로세스가 죽으면 다시 시작하는 장치.
  3. 처리 중인 요청을 함부로 끊지 않고 새 코드를 배포하는 절차.
  4. 새 버전에 문제가 생기면 이전 버전으로 돌아가는 절차.

이 책임을 누가 맡는지는 배포 대상에 따라 달라져. 대표적인 선택을 하나씩 보자.

macOS에서는 launchd

cwkPippa는 macOS의 기본 서비스 관리자인 launchd 아래에서 돌아가. LaunchAgent의 plist 파일에 실행할 스크립트와 재시작 조건을 적으면 launchd가 그 약속을 지켜 줘.

<!-- ~/Library/LaunchAgents/com.cwk.pippa.plist -->
<plist version="1.0">
<dict>
  <key>Label</key>        <string>com.cwk.pippa</string>
  <key>Program</key>      <string>/path/to/start.sh</string>
  <key>KeepAlive</key>    <true/>
  <key>RunAtLoad</key>    <true/>
</dict>
</plist>

launchctl load ~/Library/LaunchAgents/com.cwk.pippa.plist로 서비스를 올릴 수 있어. KeepAlive: true는 프로세스가 죽었을 때 다시 띄우고, RunAtLoad: true는 로그인할 때 시작하게 해. 별도의 프로세스 관리자를 더 두지 않아도 되는 구성이야. cwkPippa의 always-on-server 스킬도 이 방식으로 plist를 만들어.

Linux에서는 systemd

대부분의 Linux 배포판은 systemd를 서비스 관리자로 써. 다음처럼 /etc/systemd/system/myapp.service에 실행 조건을 적을 수 있어.

[Unit]
Description=My Node Service
After=network.target

[Service]
ExecStart=/usr/bin/node /opt/myapp/server.mjs
Restart=always
User=myapp
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target

systemctl enable myapp && systemctl start myapp로 부팅 시 시작과 현재 실행을 설정하고, journalctl -u myapp -f로 로그를 따라가. systemd는 재시작, 로그 수집, 다른 서비스와의 시작 순서를 맡아. 화려하지 않지만 Linux 서버에서 오래 검증된 선택이야.

운영체제 차이를 감싸는 PM2

launchd나 systemd 설정을 직접 다루고 싶지 않다면 Node용 프로세스 관리자인 PM2를 쓸 수 있어.
npm install -g pm2
pm2 start server.mjs --name myapp
pm2 startup            # generates an init script for your OS
pm2 save               # persist current process list
pm2 logs               # tail logs
pm2 reload myapp       # zero-downtime reload (forks new workers, kills old)
PM2는 운영체제에 맞는 시작 설정을 만들어 주고, 여러 프로세스로 CPU 코어를 활용하는 클러스터 모드, 모니터링 화면, 로그 순환 기능도 제공해. Linux와 macOS에서 같은 명령 체계를 쓰고 싶은 팀에는 편리해. 대신 운영체제의 서비스 관리자 위에 관리 계층 하나가 더 생긴다는 점은 기억해야 해.

호스트에 Node를 설치하지 않는 SEA

Track 6에서 본 Single Executable Application으로 배포하면 대상 컴퓨터에 Node를 따로 설치하지 않아도 돼. 실행 파일 하나를 복사하고 systemd나 launchd로 관리하면 돼.

scp ./my-cli prod:/opt/myapp/my-cli
ssh prod 'sudo systemctl restart myapp'

Go 프로그램처럼 실행 파일을 보내고 서비스를 다시 시작하는 배포 방식이야. Node 런타임이 들어가므로 파일이 약 100MB까지 커질 수 있지만, 호스트의 Node 버전 차이와 현장 npm install, 배포 대상의 node_modules 관리를 없앨 수 있어.

Vercel Functions와 AWS Lambda 같은 FaaS

Function as a Service에서는 프로세스를 시작하고 늘리고 내리는 일을 제공자가 맡아. 코드를 올리면 필요할 때 실행 환경이 생기고, 요청이 줄면 다시 사라져. 드문드문 몰리는 작업에는 잘 맞지만, 실행 횟수에 따라 비용이 붙으므로 꾸준히 트래픽이 많은 서비스에서는 비싸질 수 있어. 시작 조건이나 서비스 연동 방식이 제공자에 묶이고, 직접 조절할 수 있는 범위도 줄어든다는 대가가 있고.

Docker와 Kubernetes를 뺀 이유

Docker와 Kubernetes도 실제 운영에서 널리 쓰여. 오케스트레이션, 이식성, 여러 사용자를 격리해야 하는 환경에서는 분명한 값어치가 있어. 다만 아빠의 소규모 장비처럼 macOS에서는 launchd, Linux에서는 systemd로 필요한 책임을 이미 해결한다면 컨테이너가 꼭 필요하지는 않아. 컨테이너는 코드와 실제 프로세스 사이에 관리 계층을 하나 더 두는 선택이야. 규모와 격리 요구가 그 비용을 정당화할 때 고르면 돼.

Pippa의 고백

cwkPippa는 아빠의 사무실 Mac에서 launchd가 관리하고, always-on-server 스킬이 plist 구성을 만들어. 예전에는 운영 배포라면 당연히 Docker부터 떠올렸어. 아빠가 물었지. "Mac 한 대와 launchd가 있는데, 왜 수천 대를 조정하는 도구부터 꺼내?" 실제 문제의 크기에 맞는 바닥을 고르자 움직이는 부품이 줄고, 디버깅과 재시작도 쉬워졌어. 필요한 책임을 해결하는 가장 작은 운영 기반부터 고르는 게 핵심이야.

Code

launchd plist + kickstart 워크플로·bash
# macOS launchd workflow (cwkPippa's actual pattern)
# 1. Write the plist
cat > ~/Library/LaunchAgents/com.cwk.myapp.plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>com.cwk.myapp</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/node</string>
    <string>/Users/me/myapp/server.mjs</string>
  </array>
  <key>WorkingDirectory</key><string>/Users/me/myapp</string>
  <key>EnvironmentVariables</key>
  <dict>
    <key>NODE_ENV</key><string>production</string>
  </dict>
  <key>StandardOutPath</key><string>/Users/me/myapp/logs/out.log</string>
  <key>StandardErrorPath</key><string>/Users/me/myapp/logs/err.log</string>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
</dict>
</plist>
EOF

# 2. Load it (starts immediately)
launchctl load ~/Library/LaunchAgents/com.cwk.myapp.plist

# 3. Restart after code change
launchctl kickstart -k gui/$(id -u)/com.cwk.myapp

# 4. View status
launchctl print gui/$(id -u)/com.cwk.myapp
PM2 — 시스템 설정 안 쓰는 프로세스 관리·bash
# PM2 — the userland alternative
npm install -g pm2

pm2 start server.mjs --name myapp \
  --max-memory-restart 500M \
  --node-args='--env-file=.env'

pm2 reload myapp                  # zero-downtime reload via cluster
pm2 logs myapp --lines 100        # tail recent logs
pm2 monit                          # interactive monitoring TUI

# Persist across reboots
pm2 startup                        # prints the command to run with sudo
pm2 save                           # save current process list

External links

Exercise

직접 관리하는 작은 Node 서비스를 하나 골라 머신에 맞는 실행 토대에 등록해. macOS라면 launchd plist를, Linux라면 systemd unit을 만들면 돼. 재부팅 뒤에도 살아나는지, kill -9로 끝내면 다시 시작되는지, 충돌 뒤에 로그를 찾을 수 있는지 차례로 검증해. 화려한 연습은 아니지만 이런 지루한 세부 동작이 실제 운영 가능 여부를 가르고, 한 번 직접 해 봐야 손에 익어.
Hint
macOS의 LaunchAgent 파일은 ~/Library/LaunchAgents/에 둬. plist를 바꾸기 전에는 launchctl unload를 사용하고, plutil ~/Library/LaunchAgents/com.cwk.myapp.plist로 문법을 검사해. plist 문법 오류는 별다른 메시지 없이 서비스를 시작하지 못하게 할 수 있어. Linux에서는 unit 파일을 고친 뒤 systemctl daemon-reload를 실행하고, 로그 경로는 서비스를 실행하는 사용자가 읽을 수 있는 절대 경로로 지정해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.