Files
llm-wiki/raw/blog-topics/spring-boot-startup-exit-code-propagation-2026-06-10.md
T

116 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 는 0255 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한다.