Files
llm-wiki/raw/branch-notes/feature-migration-startup-contract.md
T

375 lines
44 KiB
Markdown

---
title: branch / feature-migration-startup-contract
source_type: branch-note
status: raw
branch: feature-migration-startup-contract
parent_branch:
related_projects: [ca-skeleton]
governing_docs: [wiki/projects/ca-tmpl/runtime-container-health-migration]
tags: [branch, ca-skeleton, migration, startup]
created: 2026-05-21
target_merge:
status_label: in-progress
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-017
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-017
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MIGRATION-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-MIGRATION-001@1]
refines: []
overrides: []
depends_on: []
contract_packet: 1
contract_packet_sha256: 8152c547cd16be05f06e75309eb74853aed3e8023945bd76ddb698ee7eec6463
---
# branch: feature-migration-startup-contract
> Layer: `raw/branch-notes/` — migration과 startup validation 실패 기준을 정의합니다.
<!-- section-id: branch-parent -->
## 부모 (필수)
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§15 Runtime/Lifecycle · §25 Default Decisions `migration runner` row · Multi-Instance Guardrail `migration runner` row) 의 결정/근거/금지 사항을 정제한다.
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: Flyway 실패·진행 중 readiness가 healthy가 아님을 검증한다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MIGRATION-001@1` | Flyway는 startup에서 실행하고 migration 완료 후에만 readiness를 healthy로 전환한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-MIGRATION-001@1` | schema migration tool은 Spring Boot transitive Flyway다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
서버가 뜨기 전에도 실패는 발생합니다. env 누락, migration 실패, profile mismatch, required bean/adapter disabled 같은 startup 계열 실패는 요청/응답 handler로 처리되지 않으므로 별도 계약이 필요합니다.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- Flyway/Liquibase 선택 기준.
- migration failure log 기준.
- startup env validation.
- required adapter enablement validation.
- profile mismatch detection.
- startup failure exit/log 기준.
### 제외 범위
- migration script 작성 규칙 전체.
- zero-downtime migration 전략.
- database branching strategy.
- actuator readiness/liveness/startup **probe endpoint shape**`feature-runtime-health-lifecycle-contract` (본 branch 는 "readiness 가 migration gate 됨" 정책만 소유, probe 모양은 위임).
- error envelope schema / `error.category` enum 정의 → `feature-operational-error-observability-foundation` (본 branch 는 registry 의 기존 code 를 *소비*).
- container base image / JVM ergonomics / `terminationGracePeriodSeconds``feature-container-runtime-contract`.
## 근거 (필수, 최소 1개+)
> 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
| Source | 정당화하는 결정 |
|---|---|
| [[raw/official-docs/migration-flyway-official-concepts-and-repair]] | D1~D5: Flyway 공식이 prod repair/baseline_on_migrate/out_of_order 위험성 직접 명시 (ca-tmpl forbidden의 직접 근거) + schema history 메커니즘 |
| [[raw/official-docs/migration-liquibase-official-changelog-xml-yaml]] | D1 대안: DB-agnostic + rollback이지만 XML/YAML verbose + rollback 안전 보장 없음 (LIQUIBASE-C6) |
| [[raw/official-docs/migration-atlas-schema-as-code]] | D1 대안: declarative + integrity hash 강점 vs Java/Spring 생태계 성숙도 부족 (ATLAS-C3 ORM list 에 JPA/Hibernate 미명시) |
| [[raw/official-docs/migration-k8s-init-container-job-pattern]] | D6: multi-replica race 회피에 Job이 init container보다 구조적 우월 (K8S-INIT-C4 / K8S-JOB-C1~C2 — 단 "공식 권장"은 아님, 운영 해석) |
| [[raw/official-docs/spring-boot-exit-code-generator-startup-failure]] | D7/D8: Spring Boot exit code 메커니즘 (ExitCodeGenerator / ExitCodeExceptionMapper) + `context.isActive()` 조건 (SB-EXIT-C2/C3) + 기본 exit code = 1 (SB-EXIT-C6) |
| [[raw/official-docs/sysexits-bsd-exit-code-convention]] | D7: 78/70/71/72 의 BSD sysexits(3) 근거 — 78(EX_CONFIG)·70(EX_SOFTWARE) 정합(SYSEXIT-C1/C2), 71(EX_OSERR)·72(EX_OSFILE) **의미 불일치**(SYSEXIT-C3/C4) + OpenBSD "do not use"(SYSEXIT-C5) |
| [[raw/official-docs/kubernetes-exit-code-observability-termination]] | D7/D8: k8s 가 0-255 exit code 를 `lastState.terminated.exitCode` 에 보존하나(K8S-EXIT-C1) 숫자별 자동 분기는 없음(K8S-EXIT-C3) + structured log 는 `terminationMessagePolicy: FallbackToLogsOnError`(K8S-EXIT-C4) |
## 외부 근거 / 대안 조사 (2026-05-22 — Group G-D: Migration Startup · 2026-06-09 D7 보강)
본 branch의 Flyway + readiness gated by migration + exit codes 78/70/71/72 + prod Flyway repair forbidden 결정에 대한 외부 source.
- **채택 결정 (Flyway baseline)**:
- [[raw/official-docs/migration-flyway-official-concepts-and-repair]] — Flyway 공식이 prod repair/baseline_on_migrate/out_of_order 위험성 직접 명시 (ca-tmpl forbidden의 직접 근거)
- **검토한 대안**:
- **대안 1: Liquibase (XML/YAML)** — [[raw/official-docs/migration-liquibase-official-changelog-xml-yaml]] (DB-agnostic + rollback이지만 XML/YAML verbose + rollback 안전 보장 없음 — `LIQUIBASE-C6`: "Rollback in production is not guaranteed to be safe")
- **대안 2: Atlas (schema-as-code)** — [[raw/official-docs/migration-atlas-schema-as-code]] (declarative + integrity hash 강점 vs Java/Spring 성숙도 부족 — `ATLAS-C3`: ORM provider list 에 GORM/Drizzle/Django/SQLAlchemy 만 명시, JPA/Hibernate 미포함)
- **대안 3: K8s Init Container / Job 패턴** — [[raw/official-docs/migration-k8s-init-container-job-pattern]] (multi-replica race 회피에 Job이 init container보다 구조적 우월 — `K8S-INIT-C4`: init 은 pod 단위 실행 → replica 수만큼 migration 실행 가능 / `K8S-JOB-C1~C2`: Job 은 completion 까지 단일 실행. **단 "migration 에 Job 을 쓰라"는 공식 권고 인용은 미확보 — 운영 해석**)
- **대안 4: Hibernate hbm2ddl** — 공식 anti-pattern으로 ca-tmpl이 명시적 거부 (governing doc `runtime-container-health-migration.md` §61 명시)
- **D7 exit-code 표준 (2026-06-09 보강 — `wiki-decision-researcher`)**:
- **메커니즘**: Spring Boot `ExitCodeExceptionMapper` 는 context refresh 실패 시 호출되지 않음 (`SB-EXIT-C3`: `if (context == null || !context.isActive()) return 0`). 따라서 env/profile/adapter 실패에서 custom exit code 를 반환하려면 각 예외 클래스가 `ExitCodeGenerator` 를 implements 해야 함 (`SB-EXIT-C2`).
- **숫자 정합성**: 78(EX_CONFIG="misconfigured state")·70(EX_SOFTWARE="internal software error") 은 sysexits 와 정합. **71(EX_OSERR="cannot fork/pipe")·72(EX_OSFILE="system file missing") 은 profile mismatch / required adapter disabled 와 의미 불일치** → 외부 표준 방어 불가, ca-tmpl internal convention 으로만 성립.
- **k8s 현실**: exit code 는 보존되나(`K8S-EXIT-C1`) k8s 가 78/70 에 다른 동작을 취하지 않음(`K8S-EXIT-C3`). per-cause 코드의 가치는 수동 triage 또는 외부 alert rule 에서만 실현. D8 structured log 가 더 풍부한 discriminator.
- **비교 핵심**: Flyway 공식이 ca-tmpl forbidden 결정(prod repair/baseline_on_migrate/out_of_order)의 직접 근거. Liquibase는 verbose + rollback 보장 없음. Atlas는 declarative 강점이나 Java/Spring 성숙도 부족. multi-instance에서는 Init Container보다 Job 또는 migration lock이 race 회피에 우월. exit code 는 D8 structured log 의 coarse-grained 보조 신호.
## TODO
> TODO drained — 결정은 아래 표/결정 사항 참조.
## 진행 중 메모
- startup failure는 API response가 없으므로 log와 exit behavior가 계약입니다.
- 2026-06-10 C2 구현: 모든 신규 코드는 `app-bootstrap` (composition root) 한 모듈에 위치. 신규 패키지 `dev.caskeleton.bootstrap.runtime.startup`. ArchUnit `CleanArchitectureTest` + `verifyCleanArchitectureDependencies` + 전체 `check`(592 tests) green. startup 검증은 전부 `SmartInitializingSingleton`(refresh 단계) 으로 배선 — refresh 실패가 곧 부팅 실패이므로 exit code/log 가 전파됨.
## 결정 사항
- 2026-05-21: migration/startup 실패를 runtime lifecycle에서 분리해 별도 branch로 관리.
- 2026-05-22: migration runner 기본값은 Flyway app startup runner. Liquibase는 조직 표준일 때만 허용.
- 2026-05-22: readiness는 migration 완료와 startup validation 성공 전까지 unhealthy.
- 2026-05-22: multi-instance에서는 app startup runner를 그대로 확장하지 않고 platform one-shot job 또는 migration lock 검증이 필요.
- 2026-05-22: prod에서 Flyway repair는 forbidden. partial schema 회복은 manual recovery runbook(`runbook://migration/manual-recovery`) 경로만.
- 2026-05-22: non-prod(dev/staging)에서만 Flyway repair 허용. 실행 시 audit log 필수.
- 2026-05-22: startup exit code 표준 = env 누락/malformed=78, migration 실패=70, profile mismatch=71, required adapter disabled=72.
- 2026-05-22: Flyway `baseline_on_migrate`, `out_of_order` 기본 false. 활성화는 명시적 결정 사항으로만.
- 2026-06-09: (정정) exit code 78/70 만 sysexits 외부 정합. 71/72 는 의미 불일치 → `UNSUPPORTED_IMPL_DECISION` (ca-tmpl internal convention). 또한 ca-tmpl `main()``System.exit(SpringApplication.exit(...))` 미배선 → D7 은 현재 *구현 불가* 상태(`planned`, main() 변경 선행 필요).
- 2026-06-10: **C2 구현 완료 (`actually-implemented` / `locally-verified`)**. app-bootstrap 에 startup 계약 코드 작성. D7 exit code 배선은 **F2 권고(main() rewrite)를 의도적으로 기각**`SpringApplication.exit(context)``finally` 에서 context 를 close 하고 정상 부팅 시 0 을 반환 → 장기 실행 web 서버를 부팅 직후 종료시킴. 대신 4개 startup 예외가 `ExitCodeGenerator` 를 구현하면 `SpringApplication.run()` 실패 시 `SpringBootExceptionHandler`(부팅 스레드 uncaught handler)가 `System.exit(getExitCode())` 를 호출 → **main() 변경 없이** 78/70/71/72 전파. (자세한 근거는 derived note 참조)
- 2026-06-10: D2/D4 enforcement = `application.yml``spring.flyway.{baseline-on-migrate,out-of-order}=false` + `clean-disabled=true` *명시 pin* + `FlywayProdSafetyValidator`(prod 에서 forbidden 옵션 재활성 시 exit 71) runtime fail-fast. §Audit F1 의 default 의존 DRIFT 해소.
- 2026-06-10: D8 structured log = `StartupFailures` 가 throw 직전 logstash `StructuredArguments``startup.phase`/`error.code`/`error.category`(=INTERNAL) emit (§4 권고 (a) 채택). `startup.phase` 의 mdc-keys.yaml 등록은 여전히 foundation 위임(§F4) — MDC 가 아니라 structured argument 라 등록 없이도 JSON 필드로 출력됨.
## 결정-근거 매핑
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시.
| Decision ID | Decision (요약) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|
| D1 | migration runner default = Flyway app startup runner. Liquibase 는 조직 표준일 때만 허용 | `raw/official-docs/migration-flyway-official-concepts-and-repair.md#FLYWAY-C1` (schema history table audit-trail), `#FLYWAY-C2` (applied vs available 비교). **코드 정합 (actually-implemented)**: `ca-tmpl/src/adapter-persistence/build.gradle` L11-12 `flyway-core` + `flyway-database-postgresql` + `V1__idempotency_record.sql` 존재 → Spring Boot autoconfig default 로 app startup runner 동작 | `official-vendor-doc` + `actually-implemented` (도구 선택·의존성) | Liquibase 거부 근거 = `migration-liquibase-official-changelog-xml-yaml.md#LIQUIBASE-C6` (prod rollback not guaranteed safe). Atlas 거부 = `migration-atlas-schema-as-code.md#ATLAS-C3` (ORM list 에 JPA/Hibernate 미명시 — Spring 성숙도 gap). 두 alt 모두 claim ID 매핑 완료 (이전 needs-confirmation 해소) |
| D2 | prod 에서 Flyway repair forbidden. partial schema 회복은 manual recovery runbook 경로만 | `raw/official-docs/migration-flyway-official-concepts-and-repair.md#FLYWAY-C3` (repair 의 3가지 동작: 실패 migration 제거 + checksum 재정렬 + missing as deleted), `#FLYWAY-C4` (repair 는 migrate 와 동일 locations 필수) | `official-vendor-doc` (동작 명시) + `internal-policy` (prod 금지) | "prod 에서 절대 쓰면 안 된다" 직접 금지 문구는 공식에 **없음** (`#FLYWAY-C3` Does not prove). prod-forbidden 은 audit trail tampering 우려 기반 운영 정책. **enforcement 메커니즘 미구현**`application.yml``spring.flyway:` 블록 자체가 없음(§Audit F1), prod 가드는 `planned` |
| D3 | non-prod(dev/staging) 에서만 Flyway repair 허용. 실행 시 audit log 필수 | `raw/official-docs/migration-flyway-official-concepts-and-repair.md#FLYWAY-C3`, `#FLYWAY-C4` (repair 동작 정의) | `official-vendor-doc` (동작만) + `internal-policy` (audit log 요구) | audit log 요구는 cited raw 범위 밖 — ca-tmpl 내부 결정. mdc-keys.yaml 에 startup/migration audit key 미등록(§Audit F4) |
| D4 | Flyway `baseline_on_migrate`, `out_of_order` 기본 false. 활성화는 명시적 결정 사항으로만 | `raw/official-docs/migration-flyway-official-concepts-and-repair.md#FLYWAY-C5` (outOfOrder default false + 동작), `#FLYWAY-C6` (baselineOnMigrate "safety net 제거" 경고) | `official-vendor-doc` (default false 보장) | **DRIFT**: 본 branch 의 Claims To Verify 는 "`application-prod.yml` 에 명시" 라 가정하나 **ca-tmpl 에 `application-prod.yml` 파일이 없고 `spring.flyway:` 블록도 없음**(§Audit F1). 현재는 Flyway/Spring Boot *default* 에만 의존 (명시적 pin 아님) → `documented-only`. `#FLYWAY-C5/C6` Does not prove: prescriptive 금지는 운영 해석 |
| D5 | readiness 는 migration 완료와 startup validation 성공 전까지 unhealthy | `raw/official-docs/migration-flyway-official-concepts-and-repair.md#FLYWAY-C1` (schema history 가 applied 추적), `#FLYWAY-C2` (applied vs available 비교) | `official-vendor-doc` (메커니즘) + `internal-policy` (readiness gating) | Actuator readiness probe **shape** 는 본 branch 범위 밖 → `feature-runtime-health-lifecycle-contract` 위임(§Edge). 본 branch 는 "migration 완료 전 readiness=false" *정책*만 소유. 구현 시 `FlywayMigrationStrategy` + readiness group 연결 `planned` |
| D6 | multi-instance 에서는 app startup runner 를 그대로 확장하지 않고 platform one-shot job 또는 migration lock 검증 필요 | `raw/official-docs/migration-k8s-init-container-job-pattern.md#K8S-INIT-C4` (init 은 pod 단위 → replica 수만큼 migration 실행 가능), `#K8S-JOB-C1`/`#K8S-JOB-C2` (Job 은 completion 까지 단일 실행). **코드 anchor**: `ca-tmpl StartupSafetyValidator.REQUIRED_MULTI_INSTANCE_BEANS``migrationStartupRunner` bean 을 `APP_MULTI_INSTANCE_ENABLED=true` 시 필수로 요구 | `official-vendor-doc` (k8s 메커니즘) + `internal-policy` (Job 채택은 운영 해석) | `K8S-INIT-C4`/`K8S-JOB-C1` Does not prove: "migration 에 Job 을 쓰라"는 **공식 권고 인용 미확보** — 운영 해석. `migrationStartupRunner` bean 의 **실제 구현체는 없음** (validator 는 presence 만 검사) → `planned`. Flyway lock 의 timeout/deadlock 시맨틱은 별도 Flyway 문서 raw 필요 |
| D7 | startup exit code = env=78, migration=70, profile=71, adapter=72 | **78/70 정합**: `raw/official-docs/sysexits-bsd-exit-code-convention.md#SYSEXIT-C1` (EX_CONFIG=78 "misconfigured state"), `#SYSEXIT-C2` (EX_SOFTWARE=70 "internal software error"). **메커니즘**: `raw/official-docs/spring-boot-exit-code-generator-startup-failure.md#SB-EXIT-C3` (mapper 는 context 비활성 시 미동작), `#SB-EXIT-C2` (custom 예외가 `ExitCodeGenerator` implements 필요). **71/72 = `UNSUPPORTED_IMPL_DECISION`**: `#SYSEXIT-C3` (EX_OSERR=71 "cannot fork/pipe" — profile mismatch 와 불일치), `#SYSEXIT-C4` (EX_OSFILE=72 — adapter disabled 와 불일치) | `official-standard` (78/70) + `UNSUPPORTED_IMPL_DECISION` (71/72 — ca-tmpl internal convention) | **CRITICAL DRIFT**: ca-tmpl `CaSkeletonApplication.main()``SpringApplication.run(...)` 만 호출 — `System.exit(SpringApplication.exit(...))` 미배선(§Audit F2) → 현재 어떤 custom exit code 도 반환 불가, JVM default 1 로 종료. D7 은 `planned` + main() 변경 선행 필수. k8s 는 코드 보존하나 자동 분기 없음(`#K8S-EXIT-C3`) → D8 log 가 실질 discriminator |
| D8 | structured startup failure log with `startup.phase`, `error.code`, `error.category` (generic log 금지) | **error.code/error.category = registry-backed (신규 invent 아님)**: `ca-tmpl/docs/registries/error-codes.yaml``MIGRATION_FAILED`/`STARTUP_VALIDATION_FAILED`/`REQUIRED_ADAPTER_DISABLED`/`PROFILE_MISMATCH` (모두 `category: INTERNAL`, `owner_branch: feature-migration-startup-contract`). **surfacing**: `raw/official-docs/kubernetes-exit-code-observability-termination.md#K8S-EXIT-C4` (`terminationMessagePolicy: FallbackToLogsOnError`) | `internal-policy` + `registry-backed` (4개 error code) + `official-vendor-doc` (k8s log surfacing) | `startup.phase` field 는 registry/mdc-keys 미등록(§Audit F4) — 신규 제안. **현재 구현**: `StartupSafetyValidator` 는 plain `IllegalStateException(message)` throw — structured field 없음 → structured log 는 `planned`. FallbackToLogsOnError 는 2048B/80L truncate 한계(`#K8S-EXIT-C4`) |
## Decisionized Work Items
| item | Decision | Allowed | Forbidden | Required test | Failure condition |
| --- | --- | --- | --- | --- | --- |
| migration tool | Flyway default | Liquibase with org standard | branch마다 runner 혼재 | startup smoke | tool 미정 |
| readiness gating | migration success before readiness healthy | local no-db profile only | migration 중 healthy | readiness test | failed migration reports ready |
| concurrent startup | single-instance default | platform job or migration lock | multi-replica blind startup | migration lock test | concurrent migration race |
| startup failure log | structured log with `startup.phase`, `error.code`, `error.category` | provider details in internal diagnostic only | generic log without cause | log assertion | 원인 없는 startup failure |
| rollback | no in-place rollback | forward-only migration with feature flag | production Flyway repair | prod profile에서 `flyway.repair` 호출 경로가 enabled이면 fail | prod에서 repair 활성화 |
## 구현 가이드
> CLAUDE.md §15.5 3-rule 적용. 각 항목은 본 branch 의 Decision ID + Supporting Claim 을 Trace 한다. 근거 없는 detail 은 `UNSUPPORTED_IMPL_DECISION` + trade-off 한 줄. 본 branch 범위 밖 detail 은 §엣지·실패·의존 에 위임 링크로만 남긴다.
### §1. Migration runner 배선 (Trace: D1 · FLYWAY-C1/C2)
- **위치/메커니즘 (actually-implemented)**: `ca-tmpl/src/adapter-persistence/build.gradle` L11-12 의 `org.flywaydb:flyway-core` + `flyway-database-postgresql` → Spring Boot autoconfig 가 context refresh 중 자동 실행 (별도 runner bean 불필요).
- **migration script 경로 (actually-implemented)**: `src/adapter-persistence/src/main/resources/db/migration/V{n}__{description}.sql` (현재 `V1__idempotency_record.sql` 1개, owner=`feature-rate-limit-idempotency-contract`). 본 branch 는 *naming/위치 계약*만 소유, 개별 script 내용은 owner branch.
- **readiness gate (Trace: D5, `planned`)**: migration 완료 전 `/actuator/health/readiness` = `OUT_OF_SERVICE`. `UNSUPPORTED_IMPL_DECISION` — Spring Boot autoconfig 는 migration 을 readiness 전에 실행하나, readiness group 에 Flyway 상태를 명시 연결하는 정확한 메커니즘(custom `HealthIndicator` vs `FlywayMigrationStrategy` 지연)은 cited raw 가 권고 안 함. trade-off: probe shape owner(`feature-runtime-health-lifecycle-contract`)와 합의 후 확정.
### §2. Flyway prod-forbidden 옵션 가드 (Trace: D2 · D4 · FLYWAY-C3~C6)
- **현재 상태 (DRIFT — §Audit F1)**: `application.yml``spring.flyway:` 블록 자체가 없음 + `application-prod.yml` 부재. repair/baselineOnMigrate/outOfOrder 는 Flyway/Spring Boot *default*(repair=수동 명령, baseline=false, outOfOrder=false)에만 의존.
- **`planned` 명세**: prod profile 에서 `spring.flyway.baseline-on-migrate=false`, `spring.flyway.out-of-order=false`*명시 pin* + `repair` 호출 경로(Spring bean / CLI / Actuator) disabled 단언.
- **enforcement 메커니즘 (`UNSUPPORTED_IMPL_DECISION`)**: prod 가드를 (a) `StartupSafetyValidator` 류 fail-fast 검사로 둘지 (b) ArchUnit/contract test 로만 둘지 cited raw 가 권고 안 함. trade-off: `StartupSafetyValidator` 패턴(=같은 repo 의 prod-safety 검사 선례)과 정합시키면 runtime 가드, contract test 면 build 가드. 선례 정합상 **runtime fail-fast 권고**(env-driven branch 의 `validateProdSafety()` 와 동형)이나 미결.
### §3. Startup exit code 배선 (Trace: D7 · SB-EXIT-C2/C3 · SYSEXIT-C1~C4)
- **선행 조건 (CRITICAL — §Audit F2, `planned`)**: `CaSkeletonApplication.main()``System.exit(SpringApplication.exit(SpringApplication.run(...), ...))` 로 변경해야 custom exit code 가 JVM 종료 코드로 전파됨. 현재 `main()``run(...)` 결과를 버림 → 모든 startup 실패가 exit 1.
- **mechanism 명세 (Trace: SB-EXIT-C3)**: env 누락 / profile mismatch / required adapter disabled 는 context refresh 실패 시점이라 `ExitCodeExceptionMapper` bean 이 **미동작**(`context.isActive()==false`). 따라서 각 cause 의 custom 예외가 `ExitCodeGenerator` 를 implements 해야 함(SB-EXIT-C2). migration 실패(ApplicationRunner 단계)만 mapper 로 처리 가능.
- **cause → 예외 클래스 → exit code 매핑** (클래스명은 모두 `UNSUPPORTED_IMPL_DECISION` — cited raw 가 명명 미권고, ca-tmpl convention. C2 진입 시 `ca-tmpl/src` 의 실제 throw 예외 체계와 정합 확인 필요):
| cause | 예외 클래스 (제안) | exit code | mapper 동작? (SB-EXIT-C3) | registry error.code |
|---|---|---|---|---|
| env 누락/malformed | `StartupValidationException` (신규) | 78 | ✘ context refresh 전 → `ExitCodeGenerator` implements 필수 | STARTUP_VALIDATION_FAILED |
| migration 실패 | (Flyway `FlywayException` wrap) | 70 | ✔ ApplicationRunner 단계 → mapper 가능 | MIGRATION_FAILED |
| profile mismatch | `ProfileMismatchException` (신규) | 71 | ✘ → `ExitCodeGenerator` implements 필수 | PROFILE_MISMATCH |
| required adapter disabled | `RequiredAdapterDisabledException` (신규) | 72 | ✘ → `ExitCodeGenerator` implements 필수 | REQUIRED_ADAPTER_DISABLED |
- **숫자 (Trace: SYSEXIT-C1/C2 정합 / C3/C4 불일치)**: 78=env(EX_CONFIG ✔), 70=migration(EX_SOFTWARE ✔). **`UNSUPPORTED_IMPL_DECISION`**: 71=profile, 72=adapter — sysexits 원래 의미와 불일치. trade-off: 외부 표준 방어를 포기하고 ca-tmpl internal convention 으로 lookup table 문서화하거나, 71/72 를 78/1 로 통합. governing doc 이 이미 "POSIX 강제 표준 아님 — 조직 enum 명시 필요"로 overclaim 가드 보유.
- **대안 평가 (SYSEXIT-C7)**: adapter disabled 에 72(EX_OSFILE="system file missing") 보다 **69(EX_UNAVAILABLE="service unavailable")** 가 더 가깝다는 후보 존재. 단 69 는 *runtime* service 불가 의미가 강해 *startup* 단계 검증과 의미가 어긋남 → 72 유지하되 internal convention 임을 명시. (최종 71/72 vs 78/1 통합 결정은 Claims To Verify 참조)
### §4. Structured startup failure log (Trace: D8 · registry error-codes · K8S-EXIT-C4)
- **field 명세**: `startup.phase`(예: `env-validation`|`migration`|`adapter-enablement`|`profile-check`) + `error.code`(registry SSOT: `STARTUP_VALIDATION_FAILED`|`MIGRATION_FAILED`|`REQUIRED_ADAPTER_DISABLED`|`PROFILE_MISMATCH`) + `error.category`(=`INTERNAL`, registry 고정).
- **현재 구현 (`planned`)**: `StartupSafetyValidator.afterSingletonsInstantiated()` 는 plain `IllegalStateException(message)` throw — structured field 없음.
- **logger 호출 위치 (`UNSUPPORTED_IMPL_DECISION`)**: structured log 를 (a) `throw` 직전 각 validator 가 직접 logger 호출 + field map 채움 vs (b) 공용 startup-failure handler 에 위임(예외 → field 변환). trade-off: (a)는 phase 별 정확한 field 보장하나 호출 분산, (b)는 일관성 높으나 context refresh 실패 예외를 잡을 handler 등록 위치가 까다로움. 선례(`StartupSafetyValidator` 가 직접 throw)와 정합상 **(a) throw 직전 직접 호출** 권고.
- **k8s surfacing**: deployment manifest 에 `terminationMessagePolicy: FallbackToLogsOnError` 설정 시 `kubectl describe` 로 startup log 확인(K8S-EXIT-C4, 단 2048B/80L truncate).
- **`UNSUPPORTED_IMPL_DECISION`**: `startup.phase` 는 mdc-keys.yaml 미등록 신규 키. trace/request key 의미 SSOT 는 foundation branch → mdc-keys 등록은 foundation registry 경유 권고(§Audit F4).
### §5. Multi-instance migration runner (Trace: D6 · K8S-INIT-C4 / K8S-JOB-C1~C2)
- **anchor (actually-implemented, cross-branch)**: `StartupSafetyValidator.REQUIRED_MULTI_INSTANCE_BEANS``APP_MULTI_INSTANCE_ENABLED=true``migrationStartupRunner` bean 의 **presence** 를 단언 (없으면 startup fail). 이 validator 자체는 `feature-env-driven-runtime-configuration` 소유 — 본 branch 는 그 list 의 `migrationStartupRunner` 항목 owner.
- **`planned`**: `migrationStartupRunner`**실제 구현체 없음**. multi-instance 활성 시 (a) platform one-shot Job 으로 app 내 Flyway 실행을 비활성화하거나 (b) Flyway lock 으로 단일 실행 보장. `UNSUPPORTED_IMPL_DECISION`: Job vs lock 중 default 미결 — K8S-JOB 은 메커니즘만 보장, "migration=Job" 공식 권고는 미확보(운영 해석).
- **선택 기준 (조건부, 임의 trade-off)**: 클러스터에 Job 생성 권한 + CI/CD 가 migration 을 deploy step 으로 분리 가능하면 **Job 우선**(app 부팅과 migration 분리 → readiness race 원천 제거). 그렇지 못하면 **Flyway lock**(app 내 실행 유지, lock 으로 단일화). lock 전략은 `feature-background-job-async-contract`(scheduler/outbox lock SSOT — DB advisory lock 기본값)와 정합시켜 상속 권고.
- **`UNSUPPORTED_DECISION` (raw 부재)**: Flyway lock 의 `lockRetryCount` / lock wait timeout / deadlock 해소 동작은 cited raw 에 verbatim 없음 → lock 경로 선택 시 `raw/official-docs/migration-flyway-lock-*.md` 추가 조사 필요(별도 `wiki-decision-researcher` 옵트인). 현재 lock 옵션 근거는 L0(존재) 수준.
## 엣지·실패·의존
### 다른 branch 에 위임 (OUT_OF_BRANCH_SCOPE)
| 관심사 | owner branch | 본 branch 와의 접점 |
|---|---|---|
| Actuator readiness/liveness/startup **probe endpoint shape** | [[raw/branch-notes/feature-runtime-health-lifecycle-contract]] | D5 는 "migration gate 됨" 정책만, probe 모양은 위임 |
| error envelope schema / `error.category` enum | [[raw/branch-notes/feature-operational-error-observability-foundation]] | D8 은 registry 의 기존 INTERNAL code 4개를 *소비* |
| `StartupSafetyValidator` (prod-safety + multi-instance bean presence) | [[raw/branch-notes/feature-env-driven-runtime-configuration]] (D8) | D6 의 `migrationStartupRunner` 항목 owner = 본 branch, validator host = env-driven |
| MDC/log key standard (`startup.phase` 등록) | [[raw/branch-notes/feature-operational-error-observability-foundation]] | D8 신규 key 는 foundation registry 경유 |
| container base image / `terminationGracePeriodSeconds` / JVM ergonomics | [[raw/branch-notes/feature-container-runtime-contract]] | D7/D8 의 k8s manifest(`terminationMessagePolicy`)는 container branch 와 manifest 공유 |
| `flyway.repair` 등 secret/config source | `feature-secrets-config-source-contract` | repair 비활성은 본 branch, secret 분류는 위임 |
### 실패 모드
- migration 실패 시 readiness 가 healthy 로 남으면 트래픽이 깨진 schema 로 유입 → D5 contract test 로 차단.
- multi-replica 동시 startup 시 migration race → D6 (Job/lock).
- `main()` 미배선으로 모든 startup 실패가 exit 1 → cause 구분 불가(현재 상태, D7 §Audit F2).
- structured log 미적용 시 generic stacktrace 만 → cause triage 불가(D8 현재 상태).
## Work Item Contract
각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 `기준 작성`으로 남아 있으면 이 branch는 완료로 보지 않습니다.
| field | required | rule |
| --- | --- | --- |
| Decision | yes | 구현자가 선택해야 하는 기본값 |
| Allowed | yes | 허용되는 예외와 조건 |
| Forbidden | yes | 절대 금지되는 구현/문서 상태 |
| Required registry update | conditional | error/env/header/log/metric/capability 변경 시 필수 |
| Required contract test | yes | 계약 위반 시 실패해야 하는 테스트 |
| Failure condition | yes | review/build에서 실패로 판정할 상태 |
| Canonical extraction target | yes | `wiki/projects` 승급 위치 |
## 검증해야 할 주장
> 공식 문서 인용이 결정의 근거이지만, ca-tmpl 자체 구현에서의 동작은 별도 검증이 필요한 주장.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| prod profile 에서 `flyway.repair` 호출 경로가 disabled (D2 의 contract test) | `#FLYWAY-C3`/`#FLYWAY-C4` 는 repair 동작만 보장 — prod 금지는 운영 해석. 현재 가드 메커니즘 미선택 | contract test: `spring.profiles.active=prod` 시 Flyway `repair` invocation 경로(Spring bean / CLI / Actuator)가 모두 disabled 임을 ArchUnit + runtime probe 로 단언 | `planned` |
| `spring.flyway.baseline-on-migrate=false`, `out-of-order=false` 가 ca-tmpl 에 *명시 pin* | **DRIFT**: `application-prod.yml` 부재 + `spring.flyway:` 블록 부재 → 현재 default 의존(§Audit F1) | (1) `application.yml``spring.flyway:` 블록 추가(완료, `clean-disabled=true` 포함) → (2) `FlywayProdSafetyValidator` 가 prod 에서 재활성 시 exit 71 로 fail-fast (단위 테스트 완료) | `locally-verified` (2026-06-10 — `application-prod.yml` 대신 단일 yml pin + runtime prod guard) |
| `baselineOnMigrate=true` 활성화 시 ca-tmpl 의 audit log 가 schema drift 를 detect | `#FLYWAY-C6` 의 "safety net 제거" 경고는 일반 wrong-database 시나리오 — schema drift detection 메커니즘은 별도 | dev profile 에서 의도적 schema drift 생성 후 startup log + audit trail 단언 | `planned` |
| migration 완료 전 readiness 가 healthy 가 아님 (D5 의 contract test) | Actuator readiness probe + Flyway 통합 시맨틱 검증 필요. probe shape 는 위임 branch 소유 | integration test: Flyway migration 실행 중 `/actuator/health/readiness` 가 OUT_OF_SERVICE 단언, 완료 후 UP 단언 | `planned` |
| multi-instance (replicas > 1) 에서 두 pod 동시 startup 시 migration race 회피 (D6) | `migrationStartupRunner` 구현체 부재 + Flyway lock 시맨틱 미확보. "migration=Job" 공식 권고 미확보 | k8s e2e test: replicas=3 deploy 시 migration 1회만 실행 + 다른 pod 는 lock wait 또는 Job 전용 분리 검증 | `needs-confirmation` |
| startup exit code (D7: 78/70) 가 의도된 시나리오에서 실제 반환 | ~~선행 차단: main() 미배선~~**정정(2026-06-10)**: main() 변경 불필요. `ExitCodeGenerator` 예외 + `SpringBootExceptionHandler`(uncaught handler)가 `System.exit(getExitCode())` 호출. F2 의 main() rewrite 는 `SpringApplication.exit` 의 context-close 때문에 web 서버에 유해하여 기각 | (1) 4개 예외 `ExitCodeGenerator` 구현(완료) → (2) `getExitCode()`=78/70/71/72 단위 단언(완료) → (3) k8s pod `lastState.terminated.exitCode` e2e 단언(미완) | `locally-verified` (단위) / `planned` (k8s e2e) |
| startup exit code 71/72 (profile/adapter) | `UNSUPPORTED_IMPL_DECISION` — sysexits 의미 불일치(SYSEXIT-C3/C4). 외부 표준 방어 불가 | (선택) 71/72 유지 시 internal convention lookup table 문서화 단언, 또는 78/1 통합 결정 | `needs-confirmation` |
| structured startup failure log schema (D8: `startup.phase` + registry error.code/category) 적용 | error.code/category 는 registry-backed 이나 `startup.phase` 신규 + ~~현재 `StartupSafetyValidator` 는 plain throw~~ | `StartupFailures` 가 logstash `StructuredArguments` 로 emit, `ListAppender` 단위 단언으로 3개 field 확인(완료). `startup.phase` mdc-keys 등록은 structured argument 라 불요(foundation 위임 유지). testcontainers e2e 는 미완 | `locally-verified` (단위) / `planned` (testcontainers) |
| Liquibase / Atlas / hbm2ddl 거부 근거가 각 alternative raw claim ID 와 일치 | (해소) — LIQUIBASE-C6 (rollback not prod-safe), ATLAS-C3 (ORM list 에 JPA 미명시) 로 매핑 완료 | (완료) 본 branch §Sources / §외부 근거 에 claim ID 반영됨 | `verified` (매핑 완료, 도입 결정은 documented-only) |
## 테스트 계약
- required env 누락 시 startup이 성공하면 실패.
- migration failure가 원인 없이 generic log로만 남으면 실패.
- disabled required adapter로 app이 뜨면 실패.
- prod profile에서 local-only 설정이 켜지면 실패.
- migration 완료 전 readiness가 healthy이면 실패.
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
> `/coverage` 가 채우는 **생성물** — 손으로 유지하지 않는다. governing 문서(frontmatter `governing_docs`)가 요구하는 관심사를 이 브랜치가 빠짐없이 덮는지의 결과. 기준: `rules/coverage-gate.md`.
> 상태: `covered-here`(이 브랜치 결정) / `delegated`(다른 owner 브랜치) / `missing`(아무도 안 맡음 → Blocking).
> 마지막 감사: 2026-06-09 (branch-spec 인라인 pre-fill — coverage-auditor 정식 감사 대기). governing_doc: `runtime-container-health-migration` (Migration + Startup 영역; Health/Container/Graceful Shutdown 은 sibling branch 소유).
| 관심사 | 상태 | owner | 심각도 | 근거 |
|--------|------|-------|--------|------|
| Migration runner 선택 (Flyway forward-only) | covered-here | — | — | D1 (actually-implemented: flyway-core dep + V1 script) |
| prod Flyway repair/baselineOnMigrate/outOfOrder forbidden | covered-here | — | — | D2/D4 (정책 covered, enforcement `planned` — §Audit F1) |
| non-prod repair + audit log | covered-here | — | — | D3 |
| readiness gated by migration 완료 | covered-here | — | — | D5 (정책). probe **shape** 는 위임 ↓ |
| Actuator readiness/liveness/startup probe endpoint shape | delegated | [[raw/branch-notes/feature-runtime-health-lifecycle-contract]] (D2) | OK | §Edge 위임. governing doc §Health. probe shape owner = sibling D2 |
| multi-instance migration concurrent startup (Job/lock) | covered-here | — | — | D6 (`migrationStartupRunner` bean 항목 owner, 구현 `planned`) |
| startup exit code 표준 | covered-here | — | — | D7 (78/70 정합, 71/72 internal convention, main() 배선 `planned`) |
| structured startup failure log (phase/code/category) | covered-here | — | — | D8 (error.code/category registry-backed, `startup.phase` 신규) |
| startup env validation (required env 누락 fail-fast) | covered-here | — | — | §테스트 계약 + StartupSafetyValidator 선례. STARTUP_VALIDATION_FAILED registry |
| required adapter enablement validation | covered-here | — | — | REQUIRED_ADAPTER_DISABLED registry (owner=본 branch). runtime invoke 변종 ADAPTER_DISABLED 는 feature-integration-adapter-templates |
| profile mismatch detection | covered-here | — | — | PROFILE_MISMATCH registry. StartupSafetyValidator.validateProdSafety 선례 |
| error envelope schema / category enum | delegated | [[raw/branch-notes/feature-operational-error-observability-foundation]] (D6) | OK | §Edge. registry 의 기존 INTERNAL code 소비. envelope/category SSOT = foundation D6 |
| container base image / JVM ergonomics / graceful shutdown | delegated | [[raw/branch-notes/feature-container-runtime-contract]] · [[raw/branch-notes/feature-runtime-health-lifecycle-contract]] | OK | §Edge. governing doc §Container/§Graceful Shutdown |
| zero-downtime migration 전략 | missing | (없음) | ⚪ Advisory | §Out of scope 명시. skeleton 범위 밖 프로젝트 레벨 gap(비-Blocking) |
## 마주친 문제
- **F2 권고 ↔ 정확성 충돌 (2026-06-10)**: branch-spec §Audit F2 는 `CaSkeletonApplication.main()``System.exit(SpringApplication.exit(SpringApplication.run(...)))` 로 rewrite 하라고 *CRITICAL prerequisite* 로 명시했으나, `SpringApplication.exit(context)` 는 내부에서 `finally { close(context); }` 로 context 를 닫고 정상 부팅 시 exit code 0 을 반환한다 → 장기 실행 web 서버를 부팅 직후 종료시키는 버그. 따라서 main() 은 미변경 유지하고, exit code 전파는 `ExitCodeGenerator`(예외) + `SpringBootExceptionHandler`(부팅 스레드 uncaught handler) 경로로 구현. ca-spec-reviewer 가 이 deviation 을 "technically sound, D7 intent 충족" 으로 승인. → derived blog-topic note 로 추출.
- **§테스트 계약 5 (readiness gating) 의 owned 범위 (2026-06-10)**: probe **endpoint shape**`feature-runtime-health-lifecycle-contract` 위임이라 여기서 actuator readiness 를 구현하지 않음. 대신 본 branch 가 소유한 "migration 이 ready 이전에 실행" *순서 보장* 을 구조적으로 검증 — `MigrationStartupRunner` 가 refresh 단계 `FlywayMigrationStrategy` 이며 post-ready 훅(`ApplicationRunner`/`CommandLineRunner`/`SmartLifecycle`/ready-event listener)이 *아님* 을 단언하는 테스트 추가.
## 감사 이력
> 2026-06-09 branch-spec 의 ca-tmpl ground-truth 대조에서 발견한 drift. 사용자 작성 결정 영역은 자동 rewrite 하지 않고 정합 권고만 기록.
| ID | 유형 | 발견 | 권고 |
|---|---|---|---|
| F1 | CONFIG_DRIFT | `application-prod.yml` 부재 + `application.yml``spring.flyway:` 블록 자체가 없음. D4 의 baseline/outOfOrder=false 는 *명시 pin* 이 아니라 Flyway/Spring Boot **default 의존** | C2 구현 시 `spring.flyway:` 블록을 명시 pin (Claims To Verify 2번). 현재 Claims 가 "application-prod.yml 명시"라 가정한 부분을 default 의존으로 정정함 |
| F2 | IMPL_GAP (CRITICAL) | `CaSkeletonApplication.main()``SpringApplication.run(...)` 만 호출 — `System.exit(SpringApplication.exit(...))` 미배선 → custom exit code 전파 불가, 모든 startup 실패가 exit 1 | D7 구현 선행 작업으로 main() 변경 필요. §구현 가이드 §3 에 반영 |
| F3 | NUMBER_MISMATCH | exit code 71(EX_OSERR)·72(EX_OSFILE) 가 sysexits 원래 의미(OS error / system file)와 profile mismatch·adapter disabled 의미 불일치(SYSEXIT-C3/C4) | 71/72 를 `UNSUPPORTED_IMPL_DECISION` 으로 라벨. internal convention lookup table 문서화 또는 통합 결정. governing doc 이 이미 overclaim 가드 보유 |
| F4 | REGISTRY_GAP | `startup.phase` (D8 신규 field) 가 mdc-keys.yaml 미등록. registry `runbook://migration/failed` 등 4개 link 의 backing `docs/runbooks/` 파일 부재 | `startup.phase` 는 foundation MDC registry 경유 등록(§Edge). runbook 파일은 C2 운영 단계에서 작성 |
| F5 | CROSS_BRANCH (정합 OK) | `StartupSafetyValidator` (D6 의 `migrationStartupRunner` presence 검사 host) 는 `feature-env-driven-runtime-configuration` 소유 — 본 branch 가 invent 한 것 아님 | 정합. 본 branch 는 REQUIRED_MULTI_INSTANCE_BEANS list 의 migration 항목 owner 로만 기록 |
## 묶음
<!-- GENERATED: sources:start -->
- [[raw/official-docs/kubernetes-exit-code-observability-termination]]
- [[raw/official-docs/migration-atlas-schema-as-code]]
- [[raw/official-docs/migration-flyway-official-concepts-and-repair]]
- [[raw/official-docs/migration-k8s-init-container-job-pattern]]
- [[raw/official-docs/migration-liquibase-official-changelog-xml-yaml]]
- [[raw/official-docs/spring-boot-exit-code-generator-startup-failure]]
- [[raw/official-docs/sysexits-bsd-exit-code-convention]]
<!-- GENERATED: sources:end -->
<!-- GENERATED: errors:start -->
- [[raw/errors/jpa-repository-scan-miss-multimodule-2026-06-10]]
<!-- GENERATED: errors:end -->
<!-- GENERATED: blog-topics:start -->
- [[raw/blog-topics/spring-boot-startup-exit-code-propagation-2026-06-10]]
<!-- GENERATED: blog-topics:end -->
> 본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
### 오류 기록 (본 feature 작업 중 발생)
- 코드 레벨 에러/빌드 실패 없음 — 1차 구현이 전부 green (592 tests). 단 spec 권고와 정확성이 충돌한 F2 건은 §마주친 문제 에 기록.
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- "Spring Boot 에서 startup 실패 시 custom JVM exit code 를 어떻게 전파하나? `ExitCodeGenerator` vs `ExitCodeExceptionMapper` 차이, context refresh 실패 시 mapper 가 동작하지 않는 이유(`context.isActive()==false`)는?"
- "`System.exit(SpringApplication.exit(run(...)))` 패턴을 web 서버에 쓰면 왜 위험한가?" → `SpringApplication.exit` 가 context 를 close 하고 0 을 반환.
- "Flyway 를 readiness-gated 로 만들려면 왜 `FlywayMigrationStrategy`(refresh) 가 `ApplicationRunner`(post-ready) 보다 적합한가?"
### Blog topics (이 작업에서 나온 글감)
- [[raw/blog-topics/spring-boot-startup-exit-code-propagation-2026-06-10]] — startup 실패 exit code 전파 메커니즘 + `SpringApplication.exit` context-close 함정 + sysexits 78/70 정합 / 71·72 internal convention.
## 관련 일일 노트
> 이 브랜치를 작업한 날짜들. 양방향 nav 유지.
- (없음 — documented-only 단계, C2 미진입)
## 완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목: D1 (Flyway dep + V1 migration), D6 anchor (StartupSafetyValidator multi-instance bean presence — host branch 소유) + `migrationStartupRunner` 실 bean(FlywayMigrationStrategy), D2/D4 pin(`spring.flyway` block) + `FlywayProdSafetyValidator`, D7 4개 `ExitCodeGenerator` 예외 + 78/70/71/72, D8 `StartupFailures` structured log
- `locally-verified` 항목 (2026-06-10, app-bootstrap 단위/슬라이스 테스트, 전체 `check` 592 green): exit code 78/70/71/72 = `getExitCode()` 단언; structured log `startup.phase`/`error.code`/`error.category` 단언; FlywayProdSafety prod-forbidden 옵션 fail-fast(71); required datasource env 누락 fail-fast(78); migration 실패 → exit 70 + 구조화 로그; D5 순서 보장(refresh-time strategy) 구조 단언
- `prod-verified` 항목: (없음 — k8s `lastState.terminated.exitCode` e2e + testcontainers migration 실패 로그는 `planned`)
- **추출하지 않을 항목** (planned / documented-only / abandoned): D5 actuator readiness **probe shape**(위임), `startup.phase` mdc-keys 등록(foundation 위임), k8s manifest `terminationMessagePolicy`/e2e(`planned`), non-prod repair audit log(D3 — skeleton 에 repair 호출 경로 없어 `documented-only`). exit code 71/72 숫자는 구현됐으나 `UNSUPPORTED_IMPL_DECISION`(internal convention)으로 유지.
- **F2 deviation**: main() 미변경(정확성). [[raw/blog-topics/spring-boot-startup-exit-code-propagation-2026-06-10]] 참조.