infra
Platform

모듈 맵

[Infra Ops] WAR/JAR/정적파일 배포와 배포 스크립트 작성

0 / 52 완료

펼치기
0 / 52 완료0%

Infra-ops · 37 / 52

[Infra Ops] WAR/JAR/정적파일 배포와 배포 스크립트 작성

WAR/JAR/정적파일 배포 방식, Nginx reload vs Tomcat restart, 배포 스크립트 작성까지 — 실수 없이 반복 가능한 배포 절차 구성

🚨INCIDENT ALERT
HIGH

야간 정기 배포 시간입니다. WAR 파일을 Tomcat에 올려야 하고, React 빌드 결과물은 Nginx로 서빙되는 서버에 올려야 합니다. 그리고 Spring Boot JAR로 동작하는 API 서버도 있습니다. 배포마다 순서를 헷갈리고, 실수로 백업 없이 WAR를 덮어쓴 뒤 기동에 실패한 적도 있습니다. 롤백하려 하니 이전 버전 파일이 없어서 빌드 서버에서 다시 받는 데 20분이 걸렸습니다.

이 모듈은 WAR/JAR/정적파일 배포를 실수 없이 반복 가능한 구조로 만드는 방법을 다룹니다. 배포 스크립트 작성, 헬스체크, 실패 시 롤백 절차까지 포함합니다.

이번 챕터에서 배울 것
  • 1WAR(Tomcat)/JAR(Spring Boot)/정적파일(Nginx) 배포 방식의 차이를 설명하고 각 절차를 실행할 수 있다
  • 2배포 스크립트의 구성 요소(백업/배포/기동/헬스체크/롤백)를 이해하고 기본 스크립트를 작성할 수 있다
  • 3Nginx reload와 Tomcat restart를 상황에 따라 선택하고 실행할 수 있다
  • 4배포 후 헬스체크(curl 응답코드/로그/프로세스)로 성공 여부를 확인할 수 있다
  • 5기동 실패 시 catalina.out에서 원인을 찾고 이전 버전으로 롤백하는 절차를 따를 수 있다

배포 유형 비교

💡개념

WAR / JAR / 정적파일 배포 방식 차이

배포할 산출물의 종류에 따라 절차가 달라집니다. 셋을 혼동하면 엉뚱한 서버를 재기동하거나 서비스 중단이 생깁니다.

WAR / JAR / 정적파일 배포 방식 차이

야간 배포를 진행하다가 React 빌드 결과물을 Tomcat webapps 디렉터리에 복사했습니다. Nginx를 재기동했는데 서비스가 안 뜨고, 알고 보니 JAR는 아직 구버전이 실행 중이었습니다. 산출물 종류를 구분하지 못하면 엉뚱한 서버를 재기동하거나 잘못된 위치에 파일을 올리는 실수가 반복됩니다. WAR/JAR/정적파일은 실행 환경이 다르고, 재기동 대상도 다르고, 배포 후 확인 방법도 다릅니다. 이 차이를 먼저 이해해야 배포 스크립트를 실수 없이 작성할 수 있습니다.

산출물실행 환경재기동 대상배포 후 확인
WARTomcat (webapps/)Tomcat 재기동catalina.out, HTTP 응답
JARJVM 직접 (systemd)서비스 재기동systemctl status, HTTP 응답
정적파일(dist/)Nginx (html/)Nginx reload브라우저 확인, curl

WAR 배포 절차

💡개념

Tomcat WAR 배포 — 표준 절차

WAR 파일을 Tomcat webapps/ 디렉터리에 복사하면 배포됩니다. 자동 배포(autoDeploy)를 꺼두고 수동으로 Tomcat을 재기동하는 것이 운영 표준입니다.

로컬 터미널
# ── 배포 전 준비 ─────────────────────────────────────
# 현재 버전 백업 (날짜와 시간 포함)
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/opt/backup/tomcat"
mkdir -p ${BACKUP_DIR}

cp /opt/tomcat/webapps/myapp.war ${BACKUP_DIR}/myapp_${TIMESTAMP}.war
echo "백업 완료: ${BACKUP_DIR}/myapp_${TIMESTAMP}.war"

# ── Tomcat 중지 ───────────────────────────────────────
systemctl stop tomcat
# 완전히 종료됐는지 확인
sleep 3
ps aux | grep catalina | grep -v grep
# 아무것도 안 나와야 함

# ── 배포 ─────────────────────────────────────────────
# 이전 배포 디렉터리 제거 (exploded WAR 캐시 정리)
rm -rf /opt/tomcat/webapps/myapp/

# 새 WAR 복사
cp /tmp/myapp.war /opt/tomcat/webapps/myapp.war
ls -lh /opt/tomcat/webapps/myapp.war

# ── Tomcat 기동 ───────────────────────────────────────
systemctl start tomcat

# ── 기동 확인 ─────────────────────────────────────────
# 로그 실시간 확인 (기동 완료까지 대기)
timeout 120 tail -f /opt/tomcat/logs/catalina.out | grep -m1 -E "Server startup|SEVERE|Exception"

JAR 배포 절차

💡개념

Spring Boot JAR 배포 — systemd 서비스

Spring Boot는 내장 Tomcat이 포함된 실행 가능한 JAR를 만듭니다. 별도 WAS 없이 java -jar myapp.jar로 실행합니다. 운영 환경에서는 systemd 서비스로 등록해 관리합니다.

로컬 터미널
# /etc/systemd/system/myapp.service (서비스 파일 예시)
# [Unit]
# Description=My Spring Boot Application
# After=network.target
#
# [Service]
# User=appuser
# WorkingDirectory=/opt/app
# ExecStart=/usr/bin/java -jar /opt/app/myapp.jar
# EnvironmentFile=/etc/myapp/env
# Restart=on-failure
# RestartSec=5
#
# [Install]
# WantedBy=multi-user.target

# ── JAR 배포 절차 ─────────────────────────────────────
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/opt/backup/app"
mkdir -p ${BACKUP_DIR}

# 현재 JAR 백업
cp /opt/app/myapp.jar ${BACKUP_DIR}/myapp_${TIMESTAMP}.jar

# 서비스 중지
systemctl stop myapp
sleep 2
systemctl is-active myapp  # inactive 여야 함

# 새 JAR 배포
cp /tmp/myapp-1.2.0.jar /opt/app/myapp.jar
ls -lh /opt/app/myapp.jar

# 서비스 시작
systemctl start myapp

# 기동 상태 확인
sleep 5
systemctl status myapp | head -20
journalctl -u myapp -n 30 --no-pager | grep -E "Started|ERROR|Exception"

배포 파이프라인 — 환경별 배포 방식 비교

정적파일 배포 절차

💡개념

React/Vue 정적파일 배포 — Nginx

프론트엔드 빌드 결과물(dist/ 또는 build/)을 Nginx root 디렉터리에 복사하고 reload합니다. 프로세스 재시작 없이 설정만 재로드하므로 연결 중단이 없습니다.

로컬 터미널
# ── 정적파일 배포 절차 ────────────────────────────────
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
WEBROOT="/var/www/myapp"
BACKUP_DIR="/opt/backup/frontend"
mkdir -p ${BACKUP_DIR}

# 현재 버전 백업
tar -czf ${BACKUP_DIR}/frontend_${TIMESTAMP}.tar.gz -C /var/www myapp
echo "백업: ${BACKUP_DIR}/frontend_${TIMESTAMP}.tar.gz"

# 배포 (rsync로 변경분만 전송)
rsync -avz --delete /tmp/dist/ ${WEBROOT}/
# --delete : 이전에 있던 파일 중 새 dist에 없는 파일 제거

# 또는 단순 복사
cp -r /tmp/dist/* ${WEBROOT}/

# 파일 권한 설정
chown -R nginx:nginx ${WEBROOT}
find ${WEBROOT} -type f -exec chmod 644 {} \;
find ${WEBROOT} -type d -exec chmod 755 {} \;

# Nginx 설정 검증 후 reload (연결 유지)
nginx -t && systemctl reload nginx

# 배포 확인
curl -s -o /dev/null -w "%{http_code}" http://localhost/

배포 스크립트 작성

1WAR 배포 스크립트 실행

배포 스크립트는 백업 → 배포 → 기동 → 헬스체크 → 실패 시 롤백 흐름을 자동화합니다. 아래는 기본 구조입니다.

로컬 터미널
#!/bin/bash
# /opt/scripts/deploy-war.sh

set -e   # 오류 발생 시 즉시 중단

WAR_FILE="$1"
APP_NAME="myapp"
TOMCAT_HOME="/opt/tomcat"
WEBAPPS="${TOMCAT_HOME}/webapps"
BACKUP_DIR="/opt/backup/tomcat"
HEALTH_URL="http://localhost:8080/myapp/health"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

if [ -z "$WAR_FILE" ]; then
  echo "Usage: $0 <war_file_path>"
  exit 1
fi

echo "=== 배포 시작: $(date) ==="
echo "배포 파일: $WAR_FILE"

# ① 백업
mkdir -p ${BACKUP_DIR}
if [ -f "${WEBAPPS}/${APP_NAME}.war" ]; then
  cp "${WEBAPPS}/${APP_NAME}.war" "${BACKUP_DIR}/${APP_NAME}_${TIMESTAMP}.war"
  echo "백업 완료: ${BACKUP_DIR}/${APP_NAME}_${TIMESTAMP}.war"
fi

# ② Tomcat 중지
echo "Tomcat 중지 중..."
systemctl stop tomcat
sleep 3

# ③ 배포
rm -rf "${WEBAPPS}/${APP_NAME}/"
cp "$WAR_FILE" "${WEBAPPS}/${APP_NAME}.war"
echo "WAR 복사 완료: $(ls -lh ${WEBAPPS}/${APP_NAME}.war)"

# ④ Tomcat 기동
echo "Tomcat 기동 중..."
systemctl start tomcat

# ⑤ 헬스체크 (최대 60초 대기)
echo "헬스체크 대기 중..."
for i in $(seq 1 12); do
  sleep 5
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${HEALTH_URL}" 2>/dev/null)
  if [ "$HTTP_CODE" = "200" ]; then
    echo "헬스체크 통과: HTTP ${HTTP_CODE}"
    echo "=== 배포 완료: $(date) ==="
    exit 0
  fi
  echo "대기 중... (${i}/12) HTTP ${HTTP_CODE}"
done

# ⑥ 헬스체크 실패 → 롤백
echo "ERROR: 헬스체크 실패. 롤백 시작..."
systemctl stop tomcat
LATEST_BACKUP=$(ls -t ${BACKUP_DIR}/${APP_NAME}_*.war 2>/dev/null | head -1)
if [ -n "$LATEST_BACKUP" ]; then
  cp "$LATEST_BACKUP" "${WEBAPPS}/${APP_NAME}.war"
  echo "롤백 파일: $LATEST_BACKUP"
  systemctl start tomcat
  echo "롤백 완료"
else
  echo "CRITICAL: 백업 파일 없음. 수동 조치 필요"
fi
exit 1
bash /opt/scripts/deploy-war.sh /tmp/myapp.war
🔍실행 후 확인할 것
  • 스크립트 실행 후 '헬스체크 통과: HTTP 200' 메시지가 출력됐는가
  • BACKUP_DIR 에 타임스탬프 포함 백업 파일이 생성됐는가 (ls -lh /opt/backup/tomcat/)
  • systemctl status tomcat 이 active (running) 상태인가
  • tail -f /opt/tomcat/logs/catalina.out 에서 SEVERE 또는 Exception 없이 'Server startup in' 메시지가 있는가
2배포 결과 확인 — 응답 코드/로그/프로세스

배포 후 세 가지 관점에서 확인합니다: HTTP 응답, 프로세스 상태, 로그 에러 여부.

로컬 터미널
# 1. HTTP 응답 확인
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/myapp/health)
echo "HTTP 상태코드: $HTTP_CODE"

# 2. 프로세스 상태 확인
systemctl status tomcat | grep -E "Active|Main PID"
ps aux | grep java | grep catalina | grep -v grep | awk '{print "PID:", $2, "메모리:", $6/1024"MB"}'

# 3. 최근 로그 오류 확인
grep -E "SEVERE|Exception|OutOfMemory|StackOverflow" /opt/tomcat/logs/catalina.out | tail -10

# 4. 배포된 버전 확인 (MANIFEST.MF 에 버전 정보가 있으면)
unzip -p /opt/tomcat/webapps/myapp.war META-INF/MANIFEST.MF 2>/dev/null | grep "Implementation-Version"

# 5. 정적파일 배포 후 확인 (Nginx)
curl -I http://localhost/ | grep -E "HTTP|Last-Modified|Content-Type"
OUTPUT
HTTP 상태코드: 200
Active: active (running) since Fri 2026-05-30 10:45:00 KST
PID: 12345 메모리: 512.00MB
curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/myapp/health
🔍실행 후 확인할 것
  • HTTP 상태코드가 200인가 (500이면 애플리케이션 오류, 502면 Tomcat 미기동)
  • Tomcat/앱 프로세스 메모리 사용량이 비정상적으로 높지 않은가 (xmx 설정 대비)
  • catalina.out 에 OutOfMemoryError 또는 ClassNotFoundException 없는가
  • 배포 후 첫 요청 응답 시간이 콜드 스타트 이후 정상화됐는가 (최초 요청은 느릴 수 있음)

트러블슈팅

원인: 이전 Tomcat 프로세스가 완전히 종료되지 않은 상태에서 새 Tomcat이 기동을 시도했습니다. 8080 포트를 이전 프로세스가 아직 점유하고 있습니다.

로컬 터미널
# 1. 8080 포트 점유 프로세스 확인
ss -tlnp | grep ':8080'
lsof -i:8080

# 2. 이전 Tomcat 프로세스 확인 및 종료
ps aux | grep catalina | grep -v grep
# PID 확인 후
kill -15 <PID>   # 정상 종료 시도
sleep 3
# 종료 안 되면 강제
kill -9 <PID>

# 3. Tomcat PID 파일 잔존 확인
ls -la /opt/tomcat/work/catalina.pid 2>/dev/null
rm -f /opt/tomcat/work/catalina.pid

# 4. 재기동
systemctl start tomcat
systemctl status tomcat

# 5. 재발 방지: 배포 스크립트에 종료 대기 로직 추가
# systemctl stop tomcat
# while pgrep -f catalina > /dev/null; do sleep 1; done
# echo "Tomcat 완전 종료 확인"

다른 원인 패턴 (catalina.out 에서 확인):

로컬 터미널
# 클래스 충돌: java.lang.ClassNotFoundException
# → 이전 exploded 디렉터리가 남아 있음 → rm -rf webapps/myapp/

# 설정 오류: java.io.FileNotFoundException: application-prod.yml
# → setenv.sh 경로 확인

grep -E "Exception|Error" /opt/tomcat/logs/catalina.out | tail -20

원인: 두 가지 캐시 레이어가 있습니다. ① 브라우저 캐시(로컬) ② CDN 엣지 캐시(원격). 파일을 서버에 올려도 이 캐시들이 남아 있으면 구버전이 계속 제공됩니다.

로컬 터미널
# 1. 서버에 새 파일이 올라갔는지 먼저 확인
ls -lh /var/www/myapp/static/js/
curl -I http://localhost/static/js/main.a3f9c21.js | grep "Last-Modified"

# 2. Nginx 캐시 확인 (proxy_cache를 쓰는 경우)
# Nginx 캐시 디렉터리 비우기
rm -rf /var/cache/nginx/*
nginx -s reload

# 3. 브라우저 캐시 우회 (개발자 도구에서 확인)
# Chrome: F12 → Network 탭 → "Disable cache" 체크 후 새로고침
# curl 로 캐시 없이 직접 확인
curl -H "Cache-Control: no-cache" http://your-domain.com/static/js/main.js -I

# 4. CDN 캐시 purge (CloudFront 예시)
aws cloudfront create-invalidation \
  --distribution-id ABCDEFG123 \
  --paths "/*"

# 5. 근본 해결: 파일명에 컨텐츠 해시 포함 (Create React App 기본 동작)
# main.a3f9c21.js  ← 파일 내용이 바뀌면 해시가 바뀜
# 이 방식이면 CDN 캐시 무효화 없이도 새 버전이 자동 적용됨
ls /var/www/myapp/static/js/
# main.abc12345.chunk.js  ← 배포마다 파일명 변경 확인

배포 체크리스트 산출물

💡개념

배포 전후 체크리스트

반복 가능한 배포를 위해 체크리스트를 표준화합니다. 야간 배포나 긴급 배포에서 실수를 줄이는 핵심 도구입니다.

배포 전 체크리스트
  □ 빌드 산출물 확인 (파일명, 크기, 버전)
  □ DB 마이그레이션 스크립트 준비 여부 확인
  □ setenv.sh 프로파일 설정 확인 (prod 여야 함)
  □ 이전 버전 백업 경로 확인
  □ 헬스체크 URL 확인
  □ 롤백 절차 숙지 (이전 WAR/JAR 경로)

배포 후 체크리스트
  □ 헬스체크 HTTP 200 확인
  □ systemctl status 프로세스 active 확인
  □ catalina.out 또는 app.log 에 ERROR 없음 확인
  □ 주요 기능 smoke test (로그인, 핵심 API 1~2개)
  □ 모니터링 대시보드 정상 확인 (CPU/메모리/응답시간)
  □ 배포 완료 팀 공유 (Slack/Teams 알림)
💼
실무 맥락
현업 패턴

실제 업무에서 이 지식이 쓰이는 상황:

인프라 엔지니어가 배포를 담당하는 세 가지 대표 상황입니다.

1. 정기 야간 배포:

로컬 터미널
# 표준 배포 실행 루틴
# 21:00 — 배포 준비
echo "배포 예정 파일:"
ls -lh /tmp/myapp-*.war

# DB 마이그레이션 먼저 (애플리케이션 배포 전)
mysql -h prod-db -u dba -p mydb < /deploy/V10__add_column.sql
mysql -h prod-db -u dba -p mydb -e "DESCRIBE users;" | grep phone

# 애플리케이션 배포
bash /opt/scripts/deploy-war.sh /tmp/myapp-1.2.0.war

# 배포 후 확인
curl -s http://localhost:8080/myapp/version
# {"version":"1.2.0","buildTime":"2026-05-30T18:30:00"}

2. 긴급 롤백 — 배포 후 문제 발생:

서버 터미널
# 장애 감지 즉시 실행
systemctl stop tomcat

# 직전 백업 파일 확인
ls -lt /opt/backup/tomcat/ | head -5

# 롤백
cp /opt/backup/tomcat/myapp_20260530_210015.war /opt/tomcat/webapps/myapp.war
rm -rf /opt/tomcat/webapps/myapp/
systemctl start tomcat

# DB 롤백이 필요하면 (UNDO SQL)
mysql -h prod-db -u dba -p mydb < /deploy/V10__undo_add_column.sql

# 복구 확인
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/myapp/health

3. 다중 서버 배포 (로드밸런서 뒤에 서버 2대):

로컬 터미널
# 1대씩 배포해 서비스 중단 없이 진행
# 서버 1 LB에서 제외 → 배포 → 헬스체크 통과 → LB 복귀
# 서버 2 동일 반복

# LB 제외/복귀는 LB 종류에 따라 다름
# HAProxy:
echo "disable server myapp/web01" | socat stdio /var/run/haproxy.sock
# 배포 완료 후
echo "enable server myapp/web01" | socat stdio /var/run/haproxy.sock

배포는 기술보다 절차가 중요합니다. 체크리스트와 스크립트가 있으면 어떤 팀원이 배포해도 결과가 일관됩니다. 다음 모듈에서는 컨테이너 배포와 WAS 배포를 비교하고 실무 전환 방법을 다룹니다.

지식 확인

퀴즈 — 4문제

Q1

WAR 파일 배포 후 Tomcat 자동 reload와 수동 restart 중 안전한 방식은?

Q2

Nginx reload가 restart보다 안전한 이유는?

Q3

정적파일 배포 후 CDN 캐시를 무효화해야 하는 이유는?

Q4

배포 스크립트에서 이전 버전 백업이 중요한 이유는?

0 / 4 답변

🧪 실습으로 확인하기

Nginx 설치 및 기동

초급

Linux 서버에 Nginx를 설치하고 systemd 서비스로 등록하여 80포트에서 응답하는 상태까지 만든다.

30📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

infra-ops중급 · 55
[Infra Ops] systemd 서비스 등록과 자동 재시작 설정
인프라 서비스 운영 트랙 계속
linux입문 · 30
[Linux] 개발자가 왜 리눅스 서버와 커맨드라인을 반드시 배워야 하는가
Linux 트랙 시작점