ClOr

ClOr

백엔드 실무 트러블슈팅과 AI 에이전트 구조 분석을 기록합니다.

Claude Code 해부학 (완결)

51만 줄 소스코드를 19편에 걸쳐 분석한 완결 시리즈

전체 시리즈 보기 →

백엔드 트러블슈팅

실무에서 겪은 장애와 해결 과정 기록

전체 시리즈 보기 →

최신 글

article thumbnail

들어가며

사이드 프로젝트 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 서비스 관리)

Blue-Green 배포 기본 구조

                    ┌─ :8080 (Blue)  ← 현재 활성
사용자 → Caddy ────┤
                    └─ :8081 (Green) ← 대기
  1. Green(8081)에 새 버전 배포
  2. 헬스체크 통과하면 Caddy가 Green으로 트래픽 전환
  3. Blue(8080)가 대기 상태로 전환
  4. 다음 배포 시 역할 교대

프론트엔드도 동일하게 3000↔3001 포트 로테이션.

만난 문제들과 해결

1. NSSM "marked for deletion" 상태

증상: 서비스 제거 후 바로 재등록하면 "서비스가 삭제 대기 중"이라며 실패.

원인: Windows SCM(서비스 컨트롤 매니저)이 서비스 핸들을 완전히 해제하기 전에 재등록을 시도.

해결: 서비스 제거 후 상태 폴링을 추가.

nssm stop $serviceName
nssm remove $serviceName confirm
# 서비스가 완전히 제거될 때까지 대기
while (Get-Service $serviceName -ErrorAction SilentlyContinue) {
    Start-Sleep -Seconds 1
}
nssm install $serviceName ...

2. SERVICE_PAUSED 과도 상태

증상: 서비스가 시작도 중지도 아닌 "일시정지" 상태에 빠짐.

해결: 배포 스크립트에 상태 체크 로직 추가. PAUSED 상태면 강제로 continue → stop → start 순서로 복구.

3. GitHub Actions stderr terminating error

증상: PowerShell에서 stderr 출력이 있으면 GitHub Actions가 step을 실패로 처리.

해결: $ErrorActionPreference = "Continue"로 설정하여 stderr가 step 실패를 트리거하지 않도록 변경.

4. Workspace 파일 락

증상: 빌드 후 git checkout 시 jar 파일이 락 걸려서 실패.

원인: 이전 배포의 Java 프로세스가 jar를 물고 있음.

해결: 배포 시작 전 이전 프로세스를 확실히 종료하고, shallow clone으로 incremental fetch 전환.

git fetch --depth=1 origin $branch
git reset --hard FETCH_HEAD

5. Gradle 데몬 OOM

증상: 빌드가 반복되면서 Gradle 데몬이 메모리를 점점 먹다가 OOM.

해결: JVM 힙을 2G로 증가하고, 빌드 전 데몬을 정리.

# gradle.properties
org.gradle.jvmargs=-Xmx2g

# 빌드 전 데몬 정리
./gradlew --stop

6. Node.js max-old-space-size

증상: SvelteKit 빌드 시 메모리 부족으로 크래시.

해결: NODE_OPTIONS 환경 변수 설정.

$env:NODE_OPTIONS = "--max-old-space-size=4096"
npm run build

실서비스 이슈 대응

배포 인프라 외에도 런칭 후 실사용자 대면 이슈가 연이어 발생했다.

인앱 브라우저 Google OAuth 차단

카카오톡/인스타그램에서 링크를 열면 인앱 브라우저로 열리는데, Google이 인앱 브라우저에서의 OAuth를 차단한다. 안내 UI → iframe → location.href 등 여러 시도 끝에, 인앱 브라우저 감지 후 외부 브라우저로 자동 유도하는 것이 가장 깔끔한 해결책이었다.

인메모리 랭킹 데이터 유실

Hot Ranking을 인메모리로 관리했더니, 배포/재시작 시 랭킹이 사라지는 치명적 버그가 발생. DB에 주기적으로 스냅샷을 저장하는 영속화 로직(HotRankingSnapshotHelper)으로 근본 해결했다.

최종 배포 흐름

# GitHub Actions workflow (간략화)
jobs:
  deploy:
    runs-on: self-hosted
    steps:
      - name: Detect active port
        run: # Caddy 설정에서 현재 활성 포트 확인

      - name: Build
        run: ./gradlew build

      - name: Deploy to inactive port
        run: # NSSM으로 비활성 포트에 배포

      - name: Health check
        run: # 30초간 헬스체크 폴링

      - name: Switch traffic
        run: # Caddy 설정 변경으로 트래픽 전환

결과

  • 다운타임: 수십 초 → 제로
  • 50+ 커밋에 걸친 점진적 안정화
  • 롤백: 트래픽을 이전 포트로 되돌리면 즉시 완료

배운 점

  1. 무중단 배포는 "구현"이 아니라 "안정화"가 진짜 작업이다 — 기본 구조는 하루면 만들지만, 엣지 케이스 해결에 50커밋이 들었다
  2. Windows 환경의 서비스 관리는 Linux보다 훨씬 까다롭다 — NSSM의 상태 머신을 이해하지 않으면 디버깅이 불가능하다
  3. "만드는 것"과 "운영하는 것"은 완전히 다른 영역이다 — 실사용자가 있는 서비스를 운영하면서 비로소 체감했다
profile

ClOr

@ClOr

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

ClOr · 백엔드 트러블슈팅과 AI 에이전트 구조 분석을 기록합니다.