116 lines
6.8 KiB
Markdown
116 lines
6.8 KiB
Markdown
---
|
||
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한다.
|