운영 장애를 줄이기 위해 배포 전에 확인하는 체크리스트
실서비스 배포에서 자주 놓치던 항목들을 체크리스트로 묶고 나서 장애가 어떻게 줄었는지, 실제로 어떤 순서로 확인하는지 정리했습니다.

실서비스에서 몇 번 배포를 해보고 나면, 장애는 대단한 기술 이슈보다 사소한 확인 누락에서 시작되는 경우가 더 많다는 걸 느끼게 됩니다.
코드 자체는 맞았는데 환경 변수가 빠져 있거나, 배치 시간대를 생각하지 않고 배포했거나, DB 마이그레이션 순서를 잘못 잡아서 장애가 나는 식입니다.
저도 한동안은 "이번 변경은 작으니까 괜찮겠지"라는 생각으로 배포했다가 몇 번 크게 데인 적이 있었습니다.
그 뒤로는 배포 전에 보는 항목을 체크리스트로 묶어두고, 규모가 작은 배포라도 같은 순서로 다시 확인하고 있습니다.
순서 실수 자체는 GitHub Actions로 배포 절차를 자동화하면 많이 줄지만, 자동화가 검증해 주지 못하는 판단 항목은 결국 이렇게 사람이 체크리스트로 확인해야 했습니다.
이 글에서 예로 드는 명령들은 제가 운영하던 환경(Spring Boot jar를 EC2 한 대에 올리고, 앞단에 Nginx를 두는 구성) 기준입니다. 스택이 다르면 명령은 바뀌지만, "무엇을 확인하느냐"는 거의 그대로 가져갈 수 있었습니다.
체크리스트를 두기 전에는 왜 자꾸 놓쳤을까
배포 전에 확인해야 할 항목은 머리로는 다 알고 있습니다.
- 스키마 변경이 있는지
- 롤백이 가능한지
- 로그로 바로 확인할 포인트가 있는지
- 프론트나 운영팀에 공유할 내용이 있는지
문제는 이걸 "알고 있는 것"과 "항상 빠뜨리지 않는 것"이 전혀 다르다는 점이었습니다.
회의가 길어지거나 일정이 밀리면 제일 먼저 사라지는 게 이런 확인 과정이었습니다.
그래서 지금은 체크리스트를 기술 문서라기보다, 실수 방지 장치로 보고 있습니다.
1. 이번 배포가 어떤 종류인지 먼저 나눈다
가장 먼저 보는 건 변경의 성격입니다.
- 단순 코드 수정인지
- 스키마 변경이 포함되는지
- 외부 연동 포인트가 있는지
- 트래픽 피크 시간에 민감한 기능인지
이걸 먼저 나누면 배포 난이도를 대충 감으로 때려 맞추지 않게 됩니다.
저는 머리로 분류하기 전에, 직전 배포 태그와 비교해서 무엇이 바뀌었는지 눈으로 먼저 봅니다.
# 직전 릴리스 태그를 기준으로 변경된 파일 범위를 본다
git diff --stat $(git describe --tags --abbrev=0)..HEAD
# 마이그레이션/SQL이 섞여 있는지 따로 확인한다
git diff --name-only $(git describe --tags --abbrev=0)..HEAD \
| grep -Ei 'migration|flyway|liquibase|\.sql$' || echo "스키마 변경 없음"예를 들어 API 응답 필드 하나 추가하는 배포와, 주문 테이블 스키마를 바꾸는 배포는 같은 "배포"라도 준비 방식이 달라야 합니다.
위 명령에서 .sql이 잡히면, 그때부터는 코드보다 DB를 먼저 봅니다.
2. DB 변경은 코드보다 먼저 본다
운영 장애를 만들기 쉬운 배포는 대부분 DB가 끼어 있었습니다.
특히 아래 항목은 꼭 따로 봅니다.
- nullable -> not null 변경이 들어가는지
- 대용량 테이블에 인덱스를 추가하는지
- 애플리케이션 코드와 스키마가 동시에 바뀌어야 하는지
- 이전 버전 애플리케이션과 잠시 공존해도 괜찮은지
마이그레이션이 운영에 어디까지 반영됐는지는 감으로 두지 않고 직접 조회합니다.
# 빌드 시점에 적용 여부를 보거나
./gradlew flywayInfo
# 운영 DB에 직접 최근 적용 이력을 본다
psql "$DATABASE_URL" -c \
"select version, description, success, installed_on
from flyway_schema_history
order by installed_rank desc limit 5;"실무에서는 애플리케이션은 롤백해도, 이미 반영된 스키마는 쉽게 되돌리기 어려운 경우가 많았습니다.
그래서 위험한 변경은 한 번에 바꾸지 않고, "컬럼 추가 → 코드 배포 → 백필 → 이전 컬럼 제거"처럼 단계를 나눠서 이전 버전과 잠시 공존할 수 있게 만듭니다.
3. 기능 플래그나 토글로 나눌 수 있는지 본다
한 번에 크게 여는 기능일수록 배포 순간의 리스크가 커졌습니다.
그래서 가능하면 아래처럼 쪼갭니다.
- 코드 먼저 배포
- 기능은 닫아둠
- 모니터링 확인
- 이후 플래그 오픈
코드 쪽에서는 새 흐름을 플래그 뒤에 숨겨 둡니다.
@GetMapping("/api/orders/new-flow")
public ResponseEntity<?> newFlow() {
// 플래그가 꺼져 있으면 기존 동작과 동일하게 막아 둔다
if (!featureFlags.isEnabled("order.new-flow")) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
return ResponseEntity.ok(orderService.newFlow());
}플래그 자체는 재배포 없이 열 수 있게 해두면, 문제가 생겨도 코드를 되돌리지 않고 닫기만 하면 됩니다.
# application.yml: feature.order-new-flow: ${ORDER_NEW_FLOW:false}
# 코드 배포는 이미 끝났고, 환경 값만 바꿔 재기동한다
echo "ORDER_NEW_FLOW=true" >> /home/ubuntu/app/deploy.env
sudo systemctl restart my-service이렇게 하면 장애가 나더라도 원인을 좁히기 쉽습니다.
예전에는 "배포와 기능 오픈"을 한 번에 끝내려고 했는데, 그 방식은 문제가 났을 때 어디서 흔들린 건지 찾기가 어려웠습니다.
배포 순간 자체의 다운타임이 걱정될 때는 Nginx로 무중단 배포를 구성해, 새 버전이 정상인지 확인한 뒤 트래픽을 넘기는 방식도 함께 고려했습니다.
4. 배포 직후 볼 로그와 대시보드를 미리 정해둔다
배포 후에 무엇을 볼지 정하지 않고 배포하면, 막상 이상 징후가 생겨도 어디를 먼저 확인해야 할지 헷갈립니다.
그래서 배포가 끝나면 가장 먼저 헬스체크와 핵심 API 스모크 테스트부터 돌립니다.
# 1) 애플리케이션이 떴는지 헬스체크 (실패하면 비정상 종료)
curl -f http://localhost:8080/actuator/health || echo "HEALTH CHECK FAILED"
# 2) 이번 배포에서 가장 위험한 엔드포인트의 상태 코드와 응답 시간
curl -s -o /dev/null -w "status=%{http_code} time=%{time_total}s\n" \
http://localhost:8080/api/orders그다음에는 배포 후 10분 동안 로그에서 정해둔 키워드만 따라봅니다.
tail -f /home/ubuntu/app/app.log \
| grep -iE 'error|exception|timeout|5[0-9]{2}'이렇게 "이번 배포에서 가장 위험한 API", "그 API의 에러율과 응답 시간", "확인할 로그 키워드"를 미리 적어두면, 배포가 끝난 직후 대시보드 앞에서 멍하니 그래프만 보는 일이 줄어듭니다.
5. 롤백 기준을 미리 숫자로 적어둔다
롤백은 "이상하면 하자" 수준으로 잡아두면 항상 늦어졌습니다.
그래서 지금은 최대한 기준을 숫자로 적습니다.
- 5xx 비율이 평소 대비 얼마나 오르면 롤백할지
- 핵심 API 응답 시간이 몇 배 이상 튀면 멈출지
- 특정 예외가 몇 건 이상 반복되면 배포를 되돌릴지
이 기준이 있으면 배포 중에 분위기에 휩쓸리지 않고 판단할 수 있습니다.
실무에서는 기술적 판단보다도 "조금만 더 보자"라는 심리 때문에 대응이 늦어지는 경우가 많았습니다.
6. 롤백 절차는 명령까지 적어둔다
롤백 기준만 정해두고 정작 "어떻게 되돌리는지"는 그 순간에 떠올리려 하면 또 늦어졌습니다.
그래서 지금은 환경별로 롤백 명령을 미리 적어두고, 배포 전에 이전 버전이 실제로 남아 있는지부터 확인합니다.
cd /home/ubuntu/app
# 1) 현재 프로세스 종료
pkill -f 'my-service' || true
# 2) 직전 정상 빌드로 교체 (배포 시 releases/에 버전별로 보관해 둔 경우)
ln -sfn /home/ubuntu/app/releases/my-service-<previous>.jar current.jar
# 3) 재기동
nohup java -jar current.jar > app.log 2>&1 &
# 4) 헬스체크로 복구 확인
sleep 5 && curl -f http://localhost:8080/actuator/health컨테이너로 운영한다면 직전에 돌던 이미지 태그로 되돌립니다.
# 직전 정상 커밋 SHA(또는 태그)로 이미지를 되돌린다
docker pull registry.example.com/my-service:<previous-sha>
docker stop my-service && docker rm my-service
docker run -d --name my-service -p 8080:8080 \
registry.example.com/my-service:<previous-sha>
docker ps --filter name=my-service배포 자체를 파이프라인으로 돌린다면, 직전 성공 릴리스를 다시 흘려보내는 게 가장 깔끔했습니다.
# 직전 성공한 워크플로우 실행을 그대로 다시 돌린다
gh run rerun <last-successful-run-id>
# 또는 직전 정상 릴리스 태그를 배포 브랜치로 되감는다
git push origin <previous-tag>:refs/heads/deploy --force실제로 쓰는 배포 전 체크리스트
지금은 배포 직전에 아래 표를 한 번 훑습니다. "확인 방법"에 명령을 같이 적어두니, 바쁠 때도 떠올리지 않고 그대로 실행만 하면 됐습니다.
| 확인 항목 | 확인 방법(명령) | 놓치면 생기는 일 |
|---|---|---|
| 변경 범위 | git diff --stat <last-tag>..HEAD | 영향 범위를 모른 채 배포해 회귀를 늦게 발견 |
| 마이그레이션 포함 | git diff --name-only <last-tag>..HEAD 에서 .sql 확인 | 스키마와 코드 반영 순서가 꼬여 부팅 실패 |
| 환경 변수 | 배포 셸의 printenv 결과에 필수 키가 있는지 | 값 누락으로 NPE 또는 외부 연동 실패 |
| 빌드 산출물 | ls -al build/libs/*.jar 로 시각/크기 확인 | 옛 jar를 그대로 다시 올림 |
| 헬스체크 | curl -f http://localhost:8080/actuator/health | 죽은 인스턴스에 트래픽이 들어감 |
| 핵심 API 스모크 | curl -s -o /dev/null -w "%{http_code}" .../api/orders | 500을 사용자보다 늦게 발견 |
| 롤백 경로 | ls /home/ubuntu/app/releases/ 로 이전 jar 확인 | 되돌릴 게 없어 장애가 길어짐 |
항목 자체는 거창하지 않지만, 이걸 적어도 한 번 눈으로 보고 배포하는 것만으로 사고가 꽤 줄었습니다.
마무리
배포 체크리스트는 배포를 느리게 만드는 문서가 아니라, 배포를 덜 불안하게 만드는 장치에 가깝습니다.
실서비스에서는 기술력이 높은 사람보다, 배포 전에 무엇을 확인해야 하는지 습관적으로 알고 있는 사람이 사고를 더 적게 냅니다.
저도 결국 배포를 잘하는 방법은 특별한 도구보다, 반복 가능한 확인 순서를 만드는 데 있다는 쪽으로 생각이 바뀌었습니다.