--- title: blog-topic / spring-boot-startup-exit-code-propagation-2026-06-10 source_type: blog-topic status: raw related_branches: [feature-migration-startup-contract] related_projects: [ca-tmpl] tags: [blog-topic, ca-tmpl, spring-boot, exit-code, startup, kubernetes, flyway, sysexits] created: 2026-06-10 status_label: ready-for-canonical target_audience: backend-engineer inspiration_url: archive_url: --- # blog-topic: spring-boot-startup-exit-code-propagation-2026-06-10 > Layer: `raw/blog-topics/` — 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며, `wiki/blog/` 직접 생성 근거가 아니다. ## Parent / 부모 - [[raw/branch-notes/feature-migration-startup-contract]] ## 글감 한 줄 서버가 뜨기 전에 죽는 실패(env 누락 / migration 실패 / profile mismatch / required adapter disabled)에서 **원인별 JVM exit code** 를 안전하게 전파하는 Spring Boot 메커니즘과, 흔히 처방되는 `System.exit(SpringApplication.exit(run(...)))` 패턴이 장기 실행 서버에서는 오히려 버그인 이유. ## 핵심 포인트 (draft 후보) 1. **두 가지 메커니즘과 동작 시점** - `ExitCodeExceptionMapper` (bean) — context 가 active 일 때만 동작. context refresh 실패(env/profile/adapter 검증이 `SmartInitializingSingleton` 에서 throw)는 `context.isActive()==false` 라 mapper 가 호출되지 않음. - `ExitCodeGenerator` (예외가 직접 구현) — `SpringApplication.run()` 이 실패를 re-throw 하면, 부팅 스레드에 설치된 `SpringBootExceptionHandler`(uncaught exception handler)가 실패 예외 체인에서 `getExitCode()` 를 읽어 `System.exit(code)` 호출. **main() 을 건드리지 않아도** custom exit code 가 전파된다. 2. **`System.exit(SpringApplication.exit(run(...)))` 의 함정** - 많은 글이 "custom exit code 를 쓰려면 main 을 이렇게 감싸라"고 처방한다. - 그러나 `SpringApplication.exit(context, ...)` 의 구현은 `finally { close(context); }` — context 를 닫고, 정상 부팅이면 `ExitCodeGenerator` bean 이 없으니 **0 을 반환**한다. - 결과: web 서버처럼 계속 떠 있어야 하는 프로세스를 **부팅 직후 종료**시킨다. 이 패턴은 batch/CLI(러너 완료 후 종료)용이지 long-running server 용이 아니다. - 교훈: "startup 실패 exit code" 와 "정상 종료 exit code" 는 다른 문제다. 전자는 예외 + `ExitCodeGenerator` 로 충분. 3. **exit code 숫자 선택 — sysexits(3) 정합/불일치** - `78 EX_CONFIG`(env 누락/malformed), `70 EX_SOFTWARE`(migration 실패) 는 BSD sysexits 의미와 정합. - `71 EX_OSERR`("cannot fork/pipe"), `72 EX_OSFILE`("system file missing") 는 profile mismatch / adapter disabled 와 의미가 어긋남 → 외부 표준으로 방어 불가, **조직 internal convention** 으로만 성립. 글에서 "POSIX 표준" 이라 과장하지 말 것. - k8s 는 0–255 exit code 를 `lastState.terminated.exitCode` 에 보존하지만 숫자별 자동 분기는 없음 → 실질 discriminator 는 structured log(`startup.phase`/`error.code`). 4. **migration 을 readiness 이전에 — `FlywayMigrationStrategy` vs `ApplicationRunner`** - `FlywayMigrationStrategy` 는 context refresh 단계(Flyway bean 초기화)에 실행 → readiness(=ApplicationReadyEvent 이후 UP) **이전**에 완료/실패. 반쯤 migrate 된 schema 가 트래픽을 받지 못한다. - 같은 일을 `ApplicationRunner` 로 하면 ready 이후 실행되어 순서 보장이 깨진다. ## 왜 글로 쓸 만한가 - "startup exit code" 검색 시 나오는 다수 처방이 long-running 서버에 부적합하다는 점은 실제로 코드를 까봐야 드러난다 (`SpringApplication.exit` 의 `finally close`). - sysexits 를 빌려 쓰되 71/72 처럼 의미가 안 맞는 코드를 "표준" 이라 부르지 않는 정직한 컨벤션 설계 사례. ## 검증 상태 - `locally-verified`: 예외별 `getExitCode()` = 78/70/71/72 단위 테스트, structured log 필드 단위 테스트, refresh-time 전략 구조 테스트 (app-bootstrap, 전체 `check` green). - `planned`: 실제 k8s pod `lastState.terminated.exitCode` e2e 단언, testcontainers 기반 migration 실패 로그 단언. ## 트리거 / Trigger - 트리거 유형: `branch-work` - 트리거 날짜: 2026-06-10 - 트리거 연결 노트: [[raw/branch-notes/feature-migration-startup-contract]] ## 글감 / Topic seed - 한 문장 요지: startup failure exit code는 long-running server를 `SpringApplication.exit(run(...))`로 감싸는 문제가 아니라 실패 예외와 Boot exit-code propagation 경로를 이해하는 문제다. - 예상 제목 후보: - Spring Boot startup 실패 exit code를 안전하게 전파하기 - `SpringApplication.exit(run(...))`가 서버에서 위험한 이유 ## 핵심 주장 후보 / Claim candidates - 사실 후보: - `ExitCodeExceptionMapper`와 `ExitCodeGenerator`는 동작 시점이 다르다. - sysexits 숫자는 POSIX 표준이 아니라 BSD 관례/조직 convention으로 다뤄야 한다. - 의견/해석 후보: - startup failure exit code와 정상 종료 exit code는 다른 문제다. ## Outline seed 1. startup failure의 exit code 전파 경로를 구분한다. 2. `SpringApplication.exit(run(...))` 패턴이 long-running server를 닫는 함정을 설명한다. 3. sysexits 관례와 ca-tmpl 내부 convention의 경계를 분리한다. ## Canonical 전환 후보 / Canonical extraction candidates - `wiki/projects/ca-tmpl/runtime-container-health-migration.md` 후보: - startup failure exit code propagation 글감. - 필요한 추가 검증: - 현재 ca-tmpl 코드의 exit code exception/test 존재 여부. ## Sources / 근거 후보 - [[raw/branch-notes/feature-migration-startup-contract]] - [[raw/official-docs/spring-boot-exit-code-generator-startup-failure]] - [[raw/official-docs/sysexits-bsd-exit-code-convention]] - [[raw/official-docs/kubernetes-exit-code-observability-termination]] ## 미해결 / Unknown - 아직 확인해야 할 사실: k8s pod termination exit code e2e 검증 여부. - 과장하면 안 되는 부분: sysexits를 POSIX 표준이라고 쓰지 않는다. ## Related - [[raw/official-docs/spring-boot-exit-code-generator-startup-failure]] - [[raw/official-docs/sysexits-bsd-exit-code-convention]] - [[raw/official-docs/kubernetes-exit-code-observability-termination]] - [[raw/blog-topics/hikaricp-inter-knob-constraints-startup-guard-2026-06-09]] — 같은 `SmartInitializingSingleton` fail-fast startup-guard 패턴. ## Decision / 처리 결정 - 액션: `promote-to-canonical` - 이유: `wiki/projects/ca-tmpl/runtime-container-health-migration.md` 에 startup failure exit code propagation 글감으로 반영했다. - 다음 단계: target canonical이 아직 `draft` 이므로 `blogify` 전 sysexits 관례와 ca-tmpl 내부 convention 경계를 분리해 review한다.