44 KiB
title, source_type, status, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, contract_packet_sha256
| title | source_type | status | branch | parent_branch | related_projects | governing_docs | tags | created | target_merge | status_label | id | kind | project | work_item | inherits | refines | overrides | depends_on | contract_packet | contract_packet_sha256 | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| branch / feature-migration-startup-contract | branch-note | raw | feature-migration-startup-contract |
|
|
|
2026-05-21 | in-progress | BR-CA-SKELETON-OPERATIONAL-CONTRACT-017 | project-work-item | ca-skeleton-operational-contract | WI-CA-SKELETON-OPERATIONAL-CONTRACT-017 |
|
1 | 8152c547cd16be05f06e75309eb74853aed3e8023945bd76ddb698ee7eec6463 |
branch: feature-migration-startup-contract
Layer:
raw/branch-notes/— migration과 startup validation 실패 기준을 정의합니다.
부모 (필수)
- 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 runnerrow · Multi-Instance Guardrailmigration runnerrow) 의 결정/근거/금지 사항을 정제한다.
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
1 - 패킷 스키마:
contract_packet: 1 - 완료 조건: Flyway 실패·진행 중 readiness가 healthy가 아님을 검증한다
상속한 프로젝트 결정
| 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 |
브랜치 지역 결정
기존 branch-local 결정은 아래
## Decision Evidence Map / 결정-근거 매핑의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|
선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|
목표
서버가 뜨기 전에도 실패는 발생합니다. env 누락, migration 실패, profile mismatch, required bean/adapter disabled 같은 startup 계열 실패는 요청/응답 handler로 처리되지 않으므로 별도 계약이 필요합니다.
- 이슈:
- PR:
범위
포함 범위
- 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.categoryenum 정의 →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 명시)
- 대안 1: Liquibase (XML/YAML) — raw/official-docs/migration-liquibase-official-changelog-xml-yaml (DB-agnostic + rollback이지만 XML/YAML verbose + rollback 안전 보장 없음 —
- 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.
- 메커니즘: Spring Boot
- 비교 핵심: 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. ArchUnitCleanArchitectureTest+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-tmplmain()이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 직전 logstashStructuredArguments로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.gradleL11-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.sql1개, 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 상태를 명시 연결하는 정확한 메커니즘(customHealthIndicatorvsFlywayMigrationStrategy지연)은 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 실패 시점이라
ExitCodeExceptionMapperbean 이 미동작(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 전 → ExitCodeGeneratorimplements 필수STARTUP_VALIDATION_FAILED migration 실패 (Flyway FlywayExceptionwrap)70 ✔ ApplicationRunner 단계 → mapper 가능 MIGRATION_FAILED profile mismatch ProfileMismatchException(신규)71 ✘ → ExitCodeGeneratorimplements 필수PROFILE_MISMATCH required adapter disabled RequiredAdapterDisabledException(신규)72 ✘ → ExitCodeGeneratorimplements 필수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()는 plainIllegalStateException(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시migrationStartupRunnerbean 의 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) 가 의도된 시나리오에서 실제 반환 | 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 문서(frontmattergoverning_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 로만 기록 |
묶음
- 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
본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
오류 기록 (본 feature 작업 중 발생)
- 코드 레벨 에러/빌드 실패 없음 — 1차 구현이 전부 green (592 tests). 단 spec 권고와 정확성이 충돌한 F2 건은 §마주친 문제 에 기록.
면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- "Spring Boot 에서 startup 실패 시 custom JVM exit code 를 어떻게 전파하나?
ExitCodeGeneratorvsExitCodeExceptionMapper차이, 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.exitcontext-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.flywayblock) +FlywayProdSafetyValidator, D7 4개ExitCodeGenerator예외 + 78/70/71/72, D8StartupFailuresstructured loglocally-verified항목 (2026-06-10, app-bootstrap 단위/슬라이스 테스트, 전체check592 green): exit code 78/70/71/72 =getExitCode()단언; structured logstartup.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항목: (없음 — k8slastState.terminated.exitCodee2e + testcontainers migration 실패 로그는planned)
- 추출하지 않을 항목 (planned / documented-only / abandoned): D5 actuator readiness probe shape(위임),
startup.phasemdc-keys 등록(foundation 위임), k8s manifestterminationMessagePolicy/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 참조.