당신의 Deployments가 과거에 머물고 있다: Hot Restart의 잃어버린 예술

발행: (2025년 12월 29일 오전 06:38 GMT+9)
20 분 소요
원문: Dev.to

Source: Dev.to

번역하려는 전체 텍스트를 제공해 주시면, 요청하신 대로 한국어로 번역해 드리겠습니다.

배포가 과거에 머물러 있다: 핫 리스타트의 잃어버린 예술

나는 아직도 그 금요일 자정이 생생하게 기억난다. 주말을 집에서 즐겨야 할 40대 남성이었지만, 차가운 서버실에 있었고, 팬 소리가 귀에 울리며, 터미널 앞에 오류 로그가 끝없이 스크롤되고 있었다. “simple” 라고 불렸던 버전 업데이트가 재앙으로 변해버렸다. 서비스는 시작되지 않았고, 롤백 스크립트는 실패했으며, 전화기 반대편에서는 고객의 격분한 울부짖음이 들려왔다. 그 순간 화면을 바라보며 나는 단 하나의 생각만 했다: “더 나은 방법이 있어야 한다.”

우리는 maintenance window 라는 개념이 일상이던 시절에 자라난 구세대다. 한밤중에 서비스를 멈추고 파일을 교체한 뒤, 모든 것이 순조롭게 진행되기를 기도하던 시절을 기억한다. 배포는 고위험 도박과도 같았다. 승리하면 새벽까지 무사히 버틸 수 있었고, 패배하면 온밤을 전쟁처럼 싸워야 했다. 이러한 경험은 우리에게 안정성신뢰성을 거의 편집증적으로 추구하게 만들었다.

기술이 발전하면서 우리는 배포라는 야수를 길들이기 위해 수많은 도구를 얻게 되었다—손수 작성한 쉘 스크립트부터 강력한 프로세스 매니저, 그리고 컨테이너화의 물결까지. 각 단계마다 개선이 있었지만, 언제나 궁극적인 꿈인 seamless, imperceptible, zero‑downtime updates 에는 한 발짝 부족했다.

오늘은 거의 사라져 가는 hot restart 의 예술에 대해 이야기하고자 한다. 그리고 현대 Rust 프레임워크 생태계 안에서 이 우아함과 침착함을 어떻게 다시 발견했는지 공유하고자 한다.

배포의 “와일드 웨스트”: SSH와 셸 스크립트에 대한 사랑‑증오 관계

여러분 중에 아래와 같은 배포 스크립트를 작성하거나 유지보수해 본 사람 있나요? 손 들어 주세요. 🙋‍♂️

#!/bin/bash
# Simple deployment script

# Stop the old process
PID=$(cat myapp.pid)
kill $PID
sleep 5
kill -9 $PID

# Pull latest code and build
git pull origin main
mvn clean install

# Start the new process
./myapp &
echo $! > myapp.pid

이 스크립트가 익숙하게 느껴지나요? 간단하고 직관적이며 대부분의 경우 **“동작한다”**는 점이 장점입니다. 하지만 수많은 함정을 겪어온 베테랑 입장에서 보면, 최소한 열 곳 이상의 문제가 발생할 수 있습니다:

  • 좀비 프로세스kill $PID는 단순히 SIGTERM을 보냅니다. 프로세스가 응답하지 못하면(버그 또는 I/O 블록) 5초 대기 후 kill -9로 강제 종료됩니다. 데이터가 저장되지 않거나, 연결이 닫히지 않거나, 상태가 동기화되지 않을 수 있는 시한폭탄입니다.
  • 동기화되지 않은 PID 파일 – 서비스가 비정상 종료되면 myapp.pid에 오래된, 유효하지 않은 PID가 남아 있을 수 있습니다. 스크립트는 존재하지 않는 프로세스를 kill하려 시도하고, 이후 새로운 인스턴스를 시작해 포트와 자원을 두고 두 인스턴스가 충돌하게 됩니다.
  • 빌드 실패git pullmvn clean install 모두 실패할 수 있습니다(네트워크 문제, 병합 충돌, 의존성 누락 등). 어느 단계에서든 오류가 발생하면 스크립트가 중단되고, 중지된 서비스와 교체할 새 서비스가 없게 됩니다.
  • 원자성 부족 – 전체 과정이 원자적이지 않습니다. “기존 프로세스 정지”와 “새 프로세스 시작” 사이에 명확한 다운타임 윈도우가 존재합니다. 사용자 입장에서는 서비스가 완전히 사용 불가능해집니다.
  • 플랫폼 의존성 – 스크립트는 *nix 명령과 파일 시스템 구조에 크게 의존합니다. Windows에서 실행하려면 거의 불가능에 가깝습니다.

저는 이 방식을 “무차별” 배포라고 부릅니다. 위험이 도사리고 있으며, 실행할 때마다 손에 땀을 쥐게 합니다. 동작은 하지만, 우아하지도 않고 신뢰할 수도 없습니다.

Source:

“문명의 새벽”: 전문 프로세스 매니저의 부상

이후 우리는 Node.js 세계의 PM2나 범용 systemd와 같은 보다 전문적인 도구들을 얻게 되었습니다. 이는 큰 도약이었습니다. 이 도구들은 프로세스 데몬화, 로그 관리, 성능 모니터링과 같은 강력한 기능을 제공했습니다.

PM2를 사용하면 배포가 한 줄의 명령으로 간소화될 수 있습니다:

pm2 reload my-app

pm2 reload는 애플리케이션 인스턴스를 하나씩 재시작하려 시도하여, 이른바 “무중단” 재로드를 달성합니다. systemd의 경우 서비스 유닛 파일을 수정한 뒤 다음을 실행합니다:

systemctl restart my-app.service

이 도구들은 훌륭하고, 오늘도 많은 프로젝트에서 여전히 사용하고 있습니다. 하지만 아직 완벽한 해결책은 아닙니다. 왜일까요?

  • 외부 의존성 – 이들은 애플리케이션 외부의 도구입니다. 코드 로직과 서비스 관리 로직이 분리되어 있습니다. PM2의 CLI 인자나 systemd의 복잡한 유닛 파일 구문을 배워야 합니다. 애플리케이션은 자신이 “관리”되고 있다는 사실을 알지 못합니다.
  • 언어·생태계 종속 – PM2는 주로 Node.js 생태계에 맞춰져 있습니다. 다른 언어의 프로그램도 실행할 수는 있지만 “네이티브”하게 느껴지지는 않습니다. systemd는 리눅스 시스템의 일부이며, 크로스 플랫폼이 아닙니다.
  • “블랙 박스” 동작pm2 reload가 어떻게 무중단을 구현하는 걸까요? “클러스터 모드”에 의존하지만, 설정과 내부 동작은 많은 개발자에게 블랙 박스로 남아 있습니다. 문제가 발생하면 디버깅이 매우 어렵습니다.

이 도구들은 애플리케이션을 위한 보모를 고용하는 것과 같습니다. 보모는 매우 유능하지만 가족은 아닙니다. 보모는 애플리케이션이 무엇을 생각하고 있는지 진정으로 이해하지 못하고, 재시작 전에 애플리케이션이 남길 “마지막 말”이 있는지도 모릅니다.

“가족에게 돌아가기”: 애플리케이션의 일부로 서비스 관리 내부화

이제 Hyperlane 생태계의 server-manager 가 이 문제를 어떻게 해결하는지 살펴보겠습니다. 완전히 다른 접근 방식을 취합니다: 외부 도구에 의존하지 말고 애플리케이션이 스스로를 관리하도록 하라.

use hyperlane_server_manager::{ServerManager, Hook};

fn main() {
    // Create a manager with a PID file location
    let mut manager = ServerManager::new("/var/run/myapp.pid");

    // Register a hook that runs before shutdown
    manager.register_hook(Hook::PreShutdown, || {
        // Gracefully close DB connections, flush caches, etc.
        println!("Running pre‑shutdown cleanup...");
    });

    // Start the server (this blocks until a shutdown signal is received)
    manager.run(|| {
        // Your actual application logic goes here
        hyperlane::run_server();
    });
}

이 코드의 철학은 완전히 다릅니다. 서비스 관리 로직(PID 파일 처리, 훅, 데몬화)은 Rust 라이브러리로 완벽히 캡슐화되어 애플리케이션의 일부가 됩니다. 이제 PID를 추측하거나 systemd 유닛을 설정하기 위한 셸 스크립트를 작성할 필요가 없습니다. server-manager를 통해 애플리케이션은 스스로를 관리하는 고유한 능력을 얻게 됩니다.

내부화 접근 방식의 장점

  • 코드가 설정이다 – 모든 관리 동작이 코드 안에 존재하며, 애플리케이션의 다른 부분과 함께 버전 관리됩니다. 별도의 스크립트나 유닛 파일을 동기화할 필요가 없습니다.
  • 크로스‑플랫폼 – 이 라이브러리는 Rust가 지원하는 모든 플랫폼에서 동작하므로 systemd의 Linux 전용 제한을 없앨 수 있습니다.
  • 가시성 및 디버깅 용이성 – 재시작 로직이 Rust로 작성되었기 때문에 단위 테스트를 작성하고, 상세 로그를 남기며, 디버거로 단계별로 확인할 수 있습니다.
  • 우아한 훅 – 사전 종료(pre‑shutdown)와 사후 시작(post‑startup) 훅을 등록해 자원을 올바르게 정리하거나 재초기화할 수 있습니다.
  • 무중단 재로드 – 기존 인스턴스를 종료하기 전에 새 인스턴스를 스폰하거나 소켓 활성화 패턴을 사용하면 외부 오케스트레이션 없이도 진정한 핫 재시작을 구현할 수 있습니다.

마무리 생각

“핫 리스타트”는 신화가 아니라, 애플리케이션에 자체 라이프사이클에 대한 소유권을 부여하면 깔끔하게 구현할 수 있는 패턴입니다. **server-manager**와 같은 도구로 서비스 관리를 내부화하면 배포 프로세스를 연결하는 부서진 접착제를 없애고, 우리 모두가 추구해 온 끊김 없는 제로‑다운타임 경험에 한 걸음 다가갈 수 있습니다.

핫 리스타트의 기술을 황무지에서 되돌려 현대 Rust 서비스의 일등 시민으로 만들자.

라이프사이클 훅

set_start_hookset_stop_hook은 핵심 포인트입니다. 서비스가 시작되기 전에 설정을 로드하거나, 서비스가 중지되기 전에 데이터베이스 연결을 우아하게 닫고 메모리 데이터를 저장할 수 있습니다. 애플리케이션은 “마지막 말”을 전달할 기회를 얻게 되며, 이는 데이터 일관성을 보장하는 데 필수적입니다.

크로스‑플랫폼

server-manager는 Windows와 Unix‑like 시스템 모두를 염두에 두고 설계되었으며, 플랫폼 차이를 내부적으로 처리합니다. 동일한 코드가 어디서든 실행됩니다.

“궁극형”: 무중단 핫 리스타트의 예술

이것이 hot-restart 가 진정으로 빛을 발하는 지점입니다. server-manager 와 동일한 설계 철학을 따르며, 업데이트 로직을 애플리케이션 내부에 내재화합니다.

애플리케이션에 업데이트가 필요하다고 가정해 보세요. 실행 중인 프로세스에 신호(예: SIGHUP)를 보내거나 다른 IPC 메커니즘을 통해 알리기만 하면 됩니다. 그러면 애플리케이션 내부의 hot_restart 로직이 트리거됩니다.

아래는 일반적으로 hot_restart 함수 내부에서 일어나는 흐름을 정리한 것입니다:

  1. 재시작 신호 수신hot_restart 로직을 포함한 실행 중인 서버가 특정 신호를 기다립니다.
  2. 재시작 전 훅 실행 – 신호를 받으면 서버는 즉시 종료하지 않습니다. 대신 우리가 제공한 before_restart_hookawait 합니다. 이것이 가장 중요한 단계이며, “미완료된 작업”을 처리할 소중한 기회를 제공합니다.
  3. 새 버전 컴파일 – 훅이 실행되는 동안(또는 이후에) hot_restart 가 백그라운드에서 cargo 명령(check, build)을 실행해 새로운 코드를 컴파일합니다.
    • 컴파일에 실패하면 재시작 과정이 중단되고 기존 프로세스가 중단 없이 서비스를 계속 제공합니다. 결코 결함이 있는 버전을 배포하지 마세요.
  4. “주권” 넘겨주기 – 새 버전이 성공적으로 컴파일되면, 기존 프로세스는 청취 중인 TCP 포트의 파일 디스크립터를 특수 메커니즘(보통 Unix 도메인 소켓)을 통해 새로 시작된 자식 프로세스에 전달합니다.
  5. 무결점 전환 – 새 프로세스는 즉시 해당 포트에서 accept‑ing 을 시작합니다. 커널 입장에서는 포트를 청취하던 엔티티가 한 프로세스에서 다른 프로세스로 바뀐 것뿐입니다. 이미 대기 중인 요청은 손실되지 않으며, 클라이언트는 변화가 없음을 느낍니다.
  6. 우아한 종료 – 파일 디스크립터를 넘긴 뒤, 기존 프로세스는 새로운 연결을 받지 않고 기존에 맺어진 모든 연결이 종료될 때까지 기다렸다가 평화롭게 종료합니다.

이것이 진정한 무중단 핫 리스타트이며, 단순한 롤링 재시작이 아니라 신중하게 조율된 원자적인 “대관식”입니다. 우아하고 안전하며, 개발자가 완전히 제어할 수 있습니다.

Deployment Should Be a Confident Declaration, Not a Prayer

클umsy한 셸 스크립트에서 강력한 외부 매니저로, 그리고 이제는 완전히 내부화된 server-managerhot-restart에 이르기까지, 우리는 명확한 진화 경로를 보고 있습니다. 이 경로의 최종 목적은 배포를 기도가 필요한 불확실한 의식에서 자신감 있고 결정론적인 엔지니어링 작업으로 바꾸는 것입니다.

이 통합된 철학은 Rust 생태계가 나에게 준 가장 큰 놀라움 중 하나입니다. 이는 단순히 성능과 안전성에 관한 것이 아니라, 소프트웨어를 구축하고 유지하는 새로운, 보다 신뢰할 수 있는 철학에 관한 것입니다. 한때 “ops” 영역에 있던 복잡하고 비즈니스 로직과 분리된 지식을 다시 애플리케이션 내부로 가져와, 개발자들이 가장 잘 아는 언어, 즉 코드를 사용합니다.

다음에 늦은 밤 배포가 걱정되거나 서비스 중단 위험이 두려울 때, 우리는 더 나은 도구와 보다 차분하고 우아한 개발 경험을 누릴 자격이 있다는 것을 기억하세요. 과거의 황야를 떠나 새로운 배포 시대를 맞이할 때입니다. 😊

GitHub Home

Back to Blog

관련 글

더 보기 »