📑 목차
들어가며
사이드 프로젝트 Shift(shift-dev.kr)를 운영하면서, 배포할 때마다 서비스가 수십 초간 중단되는 문제가 있었다. 실사용자 500명이 쓰는 서비스에서 이건 용납할 수 없었다.
Windows self-hosted runner + NSSM + Caddy 조합으로 Blue-Green 무중단 배포를 구축했는데, 기본 구조는 하루면 만들었지만 안정화에 50개 이상의 커밋이 들었다.
환경
- Windows 10 + GitHub Actions self-hosted runner
- Java 25 + Spring Boot 4 (백엔드)
- SvelteKit + Svelte 5 (프론트엔드)
- Caddy (리버스 프록시) + NSSM (Windows 서비스 관리)

기본 구조
┌─ :8080 (Blue) ← 현재 활성
사용자 → Caddy ────┤
└─ :8081 (Green) ← 대기
- Green(8081)에 새 버전 배포
- 헬스체크 통과 → Caddy가 Green으로 트래픽 전환
- Blue(8080)가 대기 상태로 전환
- 다음 배포 시 역할 교대
50커밋의 엣지 케이스들
| # | 문제 | 원인 | 해결 |
|---|---|---|---|
| 1 | NSSM "marked for deletion" | SCM이 핸들 해제 전 재등록 시도 | 서비스 완전 삭제까지 폴링 대기 |
| 2 | SERVICE_PAUSED 상태 | 시작도 중지도 안 되는 과도 상태 | continue → stop → start 강제 복구 |
| 3 | stderr → step 실패 | PowerShell stderr를 Actions가 실패로 처리 | $ErrorActionPreference = "Continue" |
| 4 | Workspace 파일 락 | 이전 Java 프로세스가 jar를 물고 있음 | shallow clone + 프로세스 확실 종료 |
| 5 | Gradle 데몬 OOM | 빌드 반복으로 메모리 누적 | ./gradlew --stop + Xmx 설정 |
| 6 | SvelteKit 빌드 메모리 | Node 힙 부족 | NODE_OPTIONS=--max-old-space-size=4096 |
문제 1: NSSM 서비스 삭제 대기
nssm stop $serviceName
nssm remove $serviceName confirm
while (Get-Service $serviceName -ErrorAction SilentlyContinue) {
Start-Sleep -Seconds 1 # ← 완전히 사라질 때까지 대기
}
nssm install $serviceName ...
문제 4: Workspace 파일 락
git fetch --depth=1 origin $branch
git reset --hard FETCH_HEAD # ← shallow clone으로 전환
런칭 후 실서비스 이슈
배포 인프라 외에도 실사용자가 생기면서 새로운 이슈가 연이어 터졌다.
인앱 브라우저 Google OAuth 차단
카카오톡/인스타그램에서 링크를 열면 인앱 브라우저에서 열리는데, Google이 OAuth를 차단한다. 안내 UI → iframe → location.href 등 5개 커밋에 걸쳐 시행착오 끝에 인앱 브라우저 감지 → 외부 브라우저 자동 유도로 해결.
인메모리 랭킹 데이터 유실
"배포하면 인기글이 사라져요"
Hot Ranking을 인메모리로 관리했더니, Blue-Green 배포(= 프로세스 교체)할 때마다 데이터가 날아갔다. DB에 주기적으로 스냅샷을 저장하는 HotRankingSnapshotHelper를 구현하여 해결.
최종 배포 흐름
- 비활성 슬롯 준비 (기존 서비스 제거, 산출물 복사)
- NSSM 서비스 기동 (Backend + Frontend 등록, 시작)
- 헬스체크 (HTTP 200 확인, 3초 간격)
- Caddy 트래픽 전환 (
caddy reload) - 구 슬롯 정리 (이전 서비스 중지, 포트 갱신)
- 실패 시 → 신규 서비스 중지, Caddyfile 백업 복원 (자동 롤백)
결과
| 지표 | Before | After |
|---|---|---|
| 다운타임 | 수십 초 | 제로 |
| 안정화 커밋 | — | 50+ |
| 롤백 | 수동 | Caddy 설정 되돌리면 즉시 |
배운 점
- 무중단 배포는 "구현"이 아니라 "안정화"가 진짜 작업이다 — 기본 구조는 하루, 엣지 케이스 해결에 50커밋
- Windows 환경의 서비스 관리는 Linux보다 훨씬 까다롭다 — NSSM의 상태 머신을 이해하지 않으면 디버깅이 불가능하다
- "만드는 것"과 "운영하는 것"은 완전히 다른 영역이다 — 실사용자가 있는 서비스를 운영하면서 비로소 체감했다
'백엔드 트러블슈팅' 카테고리의 다른 글
| Tauri + Python 프로세스 간 통신 삽질기 — stdout 파이프가 영원히 안 끝날 때 (0) | 2026.03.29 |
|---|---|
| WebClient .block() → Kotlin Coroutine 전환기: suspend · @Transactional 충돌 해결 (0) | 2026.03.29 |
| Reactor 파이프라인 설계기: External Task 200건 동시 처리 · ConnectableFlux 삽질 (0) | 2026.03.29 |
| Schema-per-Tenant 멀티테넌시 구현: JPA 대신 Exposed를 선택한 이유와 실전 삽질 (0) | 2026.03.29 |
| Kafka 동기화 삽질기 4편: 멀티테넌시 + @Async에서 ThreadLocal이 사라지는 문제 (0) | 2026.03.29 |
