일기 slecs

배포 일관성 확보해 팀 배포 사고 줄이기

목차

배포 스크립트를 팀 표준으로 정의하는 작업을 했다. npm ci 필수 사용, 정적 병합, 그리고 실패 시 자동 재시작 방지라는 세 가지 원칙을 담은 것인데, 각각이 왜 필요했고 어떤 문제를 해결하는지 회고해본다.

npm ci로 재현 가능한 배포 만들기

가장 먼저 건드린 부분이 npm installnpm ci (clean install) 로 강제하는 것이다. 표면상으로는 사소한 차이지만 프로덕션 배포에서는 큰 차이가 난다.

npm install 은 package.json 의 버전 범위를 존중하면서도 유연하게 작동한다. 예를 들어 "express": "^4.17.0" 이라고 명시하면 4.17.0 에서 4.x.x 범위 내에서 최신 패치를 깔 수 있다. 로컬에서는 편하지만, 배포 환경에서는 문제다. 어제는 4.17.5 를 깔았는데 오늘은 4.18.0 을 깔 수 있다는 뜻이고, 이게 예상치 못한 호환성 문제를 일으킬 수 있다.

반면 npm ci 는 lock 파일 (package-lock.json) 을 철저히 따른다. 개발자 로컬에서 lock 파일을 커밋했다면, 배포 환경은 그 lock 파일과 정확히 같은 버전의 패키지들을 설치한다. 재현성이 보장된다.

# 문제 있던 방식
npm install  # package.json의 범위를 존중하며 설치
#  배포할 때마다 다른 버전이 설치될  있음

# 개선된 방식
npm ci  # package-lock.json을 정확히 따름
#  개발 환경과 배포 환경의 버전이 완벽히 일치

이게 특히 중요한 이유는 팀 규모가 커질수록 "내 로컬에서는 되는데" 같은 현상이 빈번해지기 때문이다. npm ci 를 강제하면 그런 허수를 줄일 수 있다.

정적 병합으로 동시 배포 사고 방지

두 번째는 "정적 병합" 이다. 여러 팀원이 동시에 배포를 시도하거나, 배포 중에 다른 배포가 실행되는 상황을 방지하는 것이다.

실제로 일어났던 일은 이렇다. A 팀원이 배포를 시작했는데 조금 느렸고, B 팀원이 "아, 배포가 안 됐나?" 하고 다시 배포를 눌렀다. 그러면 npm install 이 동시에 두 번 실행되고, node_modules 디렉토리가 부분적으로 write 되다가 충돌할 수 있다. 또는 배포 스크립트가 같은 포트에 여러 프로세스를 bind 하려고 할 수 있다.

정적 병합 (또는 lock 파일 기반 동기화) 을 도입하면, 한 번에 하나의 배포만 실행되도록 보장한다. 배포 스크립트가 시작할 때 lock 을 획득하고, 끝날 때 해제하는 방식이다.

# 배포 스크립트의 핵심 로직 패턴
LOCK_FILE="/tmp/deploy.lock"

acquire_lock() {
  while [ -f "$LOCK_FILE" ]; do
    echo "다른 배포가 진행 중입니다. 대기 중..."
    sleep 2
  done
  touch "$LOCK_FILE"
}

release_lock() {
  rm -f "$LOCK_FILE"
}

# 배포 시작
acquire_lock
npm ci
npm run build
# ... 배포 로직
release_lock

이렇게 하면 동시 배포는 자동으로 방지되고, 팀원들도 "배포가 진행 중이면 기다린다" 는 명확한 규칙을 따른다.

실패 시 자동 재시작 중단으로 좀비 프로세스 방지

세 번째가 "실패 시 restart 중단" 이다. 이건 조금 더 디버깅 관점의 결정이다.

예전에는 배포 스크립트가 실패해도 자동으로 restart 를 시도했다. "일단 프로세스를 재시작해보면 되겠지" 하는 생각이었을 것 같다. 그런데 문제는 배포가 실패한 이유를 파악하지 못한 채 프로세스를 띄워버린다는 것이다.

예를 들어:
- 빌드가 타입스크립트 에러로 실패했는데, 재시작하면 이전 버전의 프로세스가 띄워진다.
- 데이터베이스 마이그레이션이 실패했는데, 프로세스는 돈다.
- 환경 변수가 잘못되어 연결이 안 되는데, 자동 재시작이 몇 번 반복되면서 문제를 더 복잡하게 만든다.

실패 시 restart 를 하지 않도록 했다. 대신 배포 스크립트가 명확한 exit code 로 종료된다. 배포가 실패했으면 모니터링 시스템이 알림을 보내고, 개발자가 로그를 확인해서 원인을 파악한다.

#!/bin/bash
set -e  # 어느 단계든 실패하면 즉시 종료

npm ci || { echo "npm ci 실패"; exit 1; }
npm run build || { echo "빌드 실패"; exit 1; }
npm run migrate || { echo "마이그레이션 실패"; exit 1; }

# 여기까지 성공하면
npm run start:prod &

echo "배포 완료"
exit 0

이제 배포 실패는 명확하고, 로그도 깔끔하다. 팀원들이 "뭐가 잘못 됐는지" 를 빠르게 파악할 수 있다.

팀에 미친 영향

개개의 변경은 작아 보이지만, 조합하면 배포 파이프라인의 안정성이 크게 올라간다. 특히:

  • 배포 예측 가능성: npm ci 덕분에 "내 환경에서는 되는데" 하는 말이 줄었다.
  • 배포 동시성 문제 제거: lock 파일이 없을 때와 달리 "배포가 중복으로 실행됐나?" 하는 의심이 없다.
  • 장애 대응 속도: 배포 실패 시 자동 재시작이 없으니, 로그를 보면 원인이 명확하다. 짐작성 복구 대신 근본 원인 해결로 넘어갈 수 있다.

특히 팀 규모가 커질수록, 배포 횟수가 많아질수록 이런 표준화의 가치가 느껴진다. 두세 명이 하는 배포는 "조심하면 되지" 라고 넘어가기 쉽지만, 10명이 다양한 시간에 배포하는 환경에서는 표준화가 필수다.

배운 점

이 작업을 하면서 깨달은 건, "배포 스크립트도 코드다" 는 것이다. 프로덕션 영향이 큰 코드일수록 더 신중하게 다뤄야 한다. npm install vs npm ci 같은 사소한 선택이 배포 안정성을 크게 흔들 수 있다.

또 팀 온보딩 관점에서도 생각해봤다. 신입이 들어왔을 때 "배포는 이 스크립트를 쓴다" 고 명확히 할 수 있으니, 배포 문화의 진입장벽이 낮아진다. 예전처럼 "어... npm install 한 다음에... 아 그다음은?" 이런 식의 막연함이 없다.


🛒 이 글과 어울리는 추천 상품

*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

댓글 0

첫 댓글 달아줘.