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

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
ca-skeleton
wiki/projects/ca-tmpl/runtime-container-health-migration
branch
ca-skeleton
migration
startup
2026-05-21 in-progress BR-CA-SKELETON-OPERATIONAL-CONTRACT-017 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-017
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MIGRATION-001@1
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-MIGRATION-001@1
1 8152c547cd16be05f06e75309eb74853aed3e8023945bd76ddb698ee7eec6463

branch: feature-migration-startup-contract

Layer: raw/branch-notes/ — migration과 startup validation 실패 기준을 정의합니다.

부모 (필수)

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) 의 결정/근거/금지 사항을 정제한다.

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 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 shapefeature-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 / terminationGracePeriodSecondsfeature-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):
  • 검토한 대안:
    • 대안 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.ymlspring.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 StructuredArgumentsstartup.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.ymlspring.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_BEANSmigrationStartupRunner 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.yamlMIGRATION_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.ymlspring.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_BEANSAPP_MULTI_INSTANCE_ENABLED=truemigrationStartupRunner 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.ymlspring.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 shapefeature-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.ymlspring.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 로만 기록

묶음

본 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 (이 작업에서 나온 글감)

관련 일일 노트

이 브랜치를 작업한 날짜들. 양방향 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 참조.