Files
llm-wiki/raw/branch-notes/feature-runtime-health-lifecycle-contract.md

506 lines
61 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: branch / feature-runtime-health-lifecycle-contract
source_type: branch-note
status: raw
branch: feature-runtime-health-lifecycle-contract
parent_branch:
related_projects: [ca-skeleton]
governing_docs: [wiki/projects/ca-tmpl/runtime-container-health-migration]
tags: [branch, ca-skeleton, runtime, health, lifecycle]
created: 2026-05-21
target_merge:
status_label: in-progress
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-013
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-013
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MIGRATION-001@1]
refines: []
overrides: []
depends_on: []
contract_packet: 1
contract_packet_sha256: 3924b8c0f447ff7dd65b9e109da6dadaac2055bb945f0db17844ea22b8e8e0fd
---
# branch: feature-runtime-health-lifecycle-contract
> Layer: `raw/branch-notes/` — runtime health와 application lifecycle 실패 계약을 정의합니다.
<!-- 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 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: startup·readiness·shutdown lifecycle test가 통과한다
<!-- 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]] |
<!-- 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 -->
## 목표
서비스는 요청 처리 중에만 실패하지 않습니다. startup, migration, readiness, graceful shutdown, scheduler, async executor, resource exhaustion 같은 lifecycle 표면도 skeleton 기본 기준에 포함되어야 합니다.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- actuator health/readiness/liveness 기준.
- graceful shutdown 기준.
- startup validation 기준.
- scheduled job 실패 기준.
- async executor/thread pool rejection 기준.
- resource exhaustion 분류.
- system clock/timezone 기준.
### 제외 범위
- Kubernetes manifest 작성.
- cloud provider specific health check.
- scheduler business job 구현.
## 근거 (필수, 최소 1개+)
> 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
| Source | 정당화하는 결정 |
|---|---|
| [[raw/official-docs/runtime-health-k8s-probes-official]] | K8s liveness/readiness/startup probe 공식 |
| [[raw/official-docs/runtime-health-spring-actuator-groups]] | Spring Boot Actuator Health Groups 공식 (ca-tmpl 결정과 정합 |
| [[raw/official-docs/runtime-health-istio-mesh-health-check]] | mTLS 환경 편의성 vs sidecar/app 살아있음 구분 불명확 |
| [[raw/company-tech-blogs/runtime-health-datadog-engineering-graceful-shutdown]] | Datadog preStop 5s + drain 20s + grace 35s 비율 보강 |
| [[raw/official-docs/k8s-configure-probes-task-page]] | D5 startup probe budget 산식 (`failureThreshold × periodSeconds`) verbatim + D11 startup validation scope (legacy / slow-starting 분리) 정당화 |
| [[raw/official-docs/k8s-pod-lifecycle-probes-concept]] | D7 timeoutSeconds vs periodSeconds 의미 구분 — 4가지 probe 메커니즘이 "단일 호출" 단위임을 정의 + probe outcome 정의 |
| [[raw/official-docs/rfc3339-datetime-utc]] | D12 UTC 강제의 IETF Standards Track 근거 ("Z" suffix 의미 + UTC interoperability 권고) |
| [[raw/official-docs/spring-smartlifecycle-reference]] | D4 graceful shutdown 의 phase ordering (ascending start / descending stop) + `stop(Runnable)` async + `DefaultLifecycleProcessor` phase-level timeout 메커니즘 |
## 외부 근거 / 대안 조사 (2026-05-22 — Group G-D: Runtime Health Lifecycle)
본 branch의 liveness/readiness/startup probe 3-endpoint 분리 + Required vs Optional Dependency Matrix + UTC + NTP drift >5s readiness fail 결정에 대한 외부 source.
- **채택 결정 (3-endpoint 분리 + Dependency Matrix)**:
- [[raw/official-docs/runtime-health-k8s-probes-official]] — K8s liveness/readiness/startup probe 공식
- [[raw/official-docs/runtime-health-spring-actuator-groups]] — Spring Boot Actuator Health Groups 공식 (ca-tmpl 결정과 정합)
- **검토한 대안**:
- **대안 1: Single /health endpoint (legacy)** — K8s 공식이 분리 권장
- **대안 2: Custom HealthIndicator beans** — Spring 기본, 단 default readiness는 외부 dependency 미포함이라 ca-tmpl이 명시적으로 readiness group에 DB/broker 묶음
- **대안 3: Service mesh-based health (Istio)** — [[raw/official-docs/runtime-health-istio-mesh-health-check]] (mTLS 환경 편의성 vs sidecar/app 살아있음 구분 불명확)
- **사례**: [[raw/company-tech-blogs/runtime-health-datadog-engineering-graceful-shutdown]] — Datadog preStop 5s + drain 20s + grace 35s 비율 보강
- **비교 핵심**: ca-tmpl 3-endpoint 분리 + 150s startup budget은 K8s 공식 + Spring Actuator Groups와 정합. Spring default readiness가 외부 dependency 미포함이라 ca-tmpl이 명시적 readiness group으로 보강. Istio mesh health는 sidecar 살아있음/app 살아있음 구분 어려움.
## TODO
> TODO drained 2026-05-22 — 결정은 아래 "결정 사항" / "Health Endpoint Contract" / "Required vs Optional Dependency Matrix" / "Startup Validation Scope" / "Decisionized Work Items" 참조. actuator endpoints/graceful shutdown/startup validation/scheduler/executor/resource exhaustion/timezone-clock 모두 표 또는 결정 라인으로 반영됨. 잔존 TODO 없음.
## 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` 승급 위치 |
## 진행 중 메모
- readiness 실패와 liveness 실패는 운영 의미가 다릅니다.
## 결정 사항
- 2026-05-21: runtime lifecycle도 non-business operational contract에 포함.
- 2026-05-22: endpoint shape owner는 이 branch. management actuator security branch는 exposure/auth policy만 소유.
- 2026-05-22: startup probe를 별도로 두고 migration/startup validation 중 readiness/liveness 오판을 막음.
- 2026-05-22: graceful shutdown timeout은 app runtime과 deployment manifest sync table에서 같은 값을 사용.
- 2026-05-22: startup probe timeout = `initialDelaySeconds=10`, `periodSeconds=5`, `failureThreshold=30` (최대 150s, migration 포함). 초과 시 K8s가 SIGKILL.
- 2026-05-22: graceful shutdown total budget = 35s (`terminationGracePeriodSeconds`). app shutdown timeout = 20s, preStop sleep = 5s, safety margin = 10s.
- 2026-05-22: startup probe single-call timeout 30s × failureThreshold 30 × periodSeconds 5s = **total budget 150s**. container-runtime의 single timeout 30s는 single probe call 한도. 150s는 startup 전체 한도(migration 포함). 두 수치는 다른 축.
- 2026-05-22: multi-instance claim parsing SSOT는 `feature-env-driven-runtime-configuration``APP_MULTI_INSTANCE_ENABLED` flag. 본 branch는 readiness probe 시 이 flag와 distributed lock contract test 결과의 일치 verify (consume only).
## 결정-근거 매핑
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다.
> `Decision ID` 는 이 branch-note 안에서 안정적으로 유지한다.
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|
| D1 | runtime lifecycle 도 non-business operational contract 에 포함 | UNSUPPORTED_DECISION (scope 결정은 내부 운영 정책) | N/A | branch scope 결정 — 외부 표준 인용 대상 아님 |
| D2 | endpoint shape owner = 본 branch, management actuator security branch 는 exposure/auth policy 만 소유 | UNSUPPORTED_DECISION (SSOT ownership 분할) | N/A | branch ownership 정책 |
| D3 | startup probe 별도 endpoint — migration/startup validation 중 readiness/liveness 오판 방지 | `raw/official-docs/runtime-health-k8s-probes-official.md#K8S-PROBE-C4`, `raw/official-docs/runtime-health-k8s-probes-official.md#K8S-PROBE-C5` (startup probe 가 성공할 때까지 liveness/readiness 실행 안 함 + startup 실패 시 kubelet kill) | `official-vendor-doc` (K8s 공식 — startup probe 가 느린 초기화 보호) | `K8S-PROBE-C4` Usage Boundary: startup probe 미설정 시 동작은 본 인용 범위 밖. Spring Boot 가 startup 전용 group 을 default 제공하는지는 `SB-HEALTH-C1` 에 명시 없음 (liveness + readiness 만) |
| D4 | graceful shutdown timeout = app runtime ↔ deployment manifest sync table 동일값 | **Mechanism SUPPORTED**: `raw/official-docs/spring-smartlifecycle-reference.md#SPRING-SMARTLC-C3` (startup ascending / shutdown descending phase 순서 — web server 보다 outbound 컴포넌트가 먼저 stop 되는 mechanism), `raw/official-docs/spring-smartlifecycle-reference.md#SPRING-SMARTLC-C7` (`stop(Runnable)` async + `DefaultLifecycleProcessor` 의 phase-level timeout 대기 mechanism). **Quantitative stays UNSUPPORTED**: app runtime ↔ deployment manifest 의 동일값 강제 + 35s/20s/5s/10s 조합 자체는 인용 자료에 직접 spec 없음. company-tech-blog `RH-DD-C1`~`C4``needs-confirmation` (verbatim 미확보) | `official-vendor-doc` (Spring Framework — mechanism only) + UNSUPPORTED (quantitative sync 값) | `SPRING-SMARTLC-C7` Does not prove: `DefaultLifecycleProcessor` 의 timeout default 값 (30s) 은 본 인용 범위 밖. company-tech-blog 자체가 `needs-confirmation` — official best practice 표현 금지. 35s/20s/5s/10s 가 "Datadog 권장 범위 내" 진술은 검증 실패. **CODE DRIFT**: ca-tmpl 실측값은 executor await 19s + server phase timeout 30s — §Audit `SHUTDOWN_BUDGET_DRIFT` 참조 |
| D5 | startup probe timeout = `initialDelaySeconds=10`, `periodSeconds=5`, `failureThreshold=30` (최대 150s, migration 포함) | **Mechanism SUPPORTED**: `raw/official-docs/k8s-configure-probes-task-page.md#K8S-PROBE-TASK-C2` (startup probe maximum budget = `failureThreshold × periodSeconds` 의 단일 문장 verbatim — "30 * 10 = 300s" 예시로 산식 직접 명시) + `raw/official-docs/runtime-health-k8s-probes-official.md#K8S-PROBE-C6` (periodSeconds default 10s — ca-tmpl 은 5s 로 override). **Quantitative stays UNSUPPORTED**: ca-tmpl 의 구체 값 `initialDelaySeconds=10` / `periodSeconds=5` / `failureThreshold=30` 자체는 내부 운영 가정 — 인용 자료의 예시는 30 × 10 = 300s 이며 ca-tmpl 의 30 × 5 = 150s 가 Spring Boot 콜드스타트를 cover 한다는 실측 부재 (`Claims To Verify` 참조) | `official-vendor-doc` (산식 mechanism) + UNSUPPORTED (정량 10/5/30) | `K8S-PROBE-TASK-C2` Does not prove: `initialDelaySeconds` 가 budget 에 포함되는지는 본 인용 단독으로 명시 안 됨 (C4 권고와 조합 필요). `failureThreshold` / `timeoutSeconds` / `initialDelaySeconds` default 값도 본 capture 에서 직접 증명 안 됨 — ca-tmpl 의 10/5/30 은 외부 표준이 아닌 ca-tmpl 운영 가정 |
| D6 | graceful shutdown total budget = 35s (terminationGracePeriodSeconds), app shutdown timeout = 20s, preStop sleep = 5s, safety margin = 10s | UNSUPPORTED_DECISION (인용 자료에 35s/20s/5s/10s 정량 spec 직접 근거 없음 — `RH-DD-C2` 의 "510s preStop + 1030s drain + 3060s grace" 도 verbatim 미확인, `needs-confirmation`) | `company-case-study` (Datadog blog — verbatim 미확인) | 정량 값은 ca-tmpl 운영 가정. company-tech-blog 의 "510s/1030s/3060s" 도 `needs-confirmation` — official 권장 아님. **CODE DRIFT**: 코드는 app shutdown 20s 가 아니라 executor await **19s** (`AsyncExecutorConfig:46` "container 20s budget 1s cleanup margin") + server phase timeout **30s** (`APP_SERVER_SHUTDOWN_TIMEOUT` default) — §Audit `SHUTDOWN_BUDGET_DRIFT` |
| D7 | startup probe single-call timeout 30s vs total budget 150s = 다른 축 명시 | **Mechanism SUPPORTED**: `raw/official-docs/k8s-pod-lifecycle-probes-concept.md#K8S-POD-LC-C4` (httpGet probe = 단일 HTTP GET 호출 — timeout 적용 단위), `K8S-POD-LC-C5` (exec probe = 단일 명령 실행), `K8S-POD-LC-C6` (tcpSocket probe = 단일 TCP 연결), `K8S-POD-LC-C7` (grpc probe = 단일 RPC 호출). 4가지 probe 메커니즘 모두 "단일 호출의 결과를 평가" 하므로 timeout 은 호출 단위, period 는 반복 주기라는 두 축 구분이 메커니즘 정의로부터 함의됨 + `raw/official-docs/runtime-health-k8s-probes-official.md#K8S-PROBE-C6` (periodSeconds default 10s — period 축 근거). **Quantitative stays UNSUPPORTED**: single-call timeout 30s 의 정량 값은 본 branch 인용 자료에 직접 verbatim 없음 — container-runtime spec 별도 필요 | `official-vendor-doc` (timeout vs period 축 구분 mechanism) + UNSUPPORTED (30s 단일 값) | `K8S-POD-LC-C4`~`C7` Does not prove: `periodSeconds` / `timeoutSeconds`**단일 문장 verbatim** 정의 — 본 capture 의 configuration fields 섹션이 truncate (concept 페이지 NOTE 참조). 의미 구분은 메커니즘 정의로부터 간접 정당화 |
| D8 | multi-instance claim parsing SSOT = `feature-env-driven-runtime-configuration` consume only | UNSUPPORTED_DECISION (SSOT 분할은 내부 운영 정책) | N/A | branch ownership 분할. **CODE 정합**: `StartupSafetyValidator:59` `validateMultiInstance()``APP_MULTI_INSTANCE_ENABLED=true` 시 5개 coordination bean (`distributedLockProvider` 등) 존재를 assert — owner 는 `feature-env-driven-runtime-configuration` + `feature-distributed-lock-contract` (§엣지·실패·의존) |
| D9 | liveness = JVM process can continue, readiness = traffic + required deps ready, startup = startup/migration validation 완료 | `raw/official-docs/runtime-health-spring-actuator-groups.md#SB-HEALTH-C1`, `raw/official-docs/runtime-health-spring-actuator-groups.md#SB-HEALTH-C2`, `raw/official-docs/runtime-health-spring-actuator-groups.md#SB-HEALTH-C3`, `raw/official-docs/runtime-health-spring-actuator-groups.md#SB-HEALTH-C6`, `raw/official-docs/runtime-health-k8s-probes-official.md#K8S-PROBE-C1`, `raw/official-docs/runtime-health-k8s-probes-official.md#K8S-PROBE-C3` | `official-vendor-doc` (Spring Actuator + K8s 공식) | `SB-HEALTH-C3` Does not prove: readiness 가 자동으로 외부 의존성 실패에 반응하는 것 아님 — application code 가 publish 해야 함. ca-tmpl 의 "readiness 에 외부 dependency 포함" 은 `SB-HEALTH-C8` (`needs-confirmation`) — default 모델과 어긋날 가능성 |
| D10 | Required vs Optional Dependency Matrix (primary DB required / primary cache conditional / message broker optional·fail-open / notification adapter optional) | `raw/official-docs/runtime-health-spring-actuator-groups.md#SB-HEALTH-C7` (health group 의 CompositeHealthContributor include/exclude 메커니즘 존재) | `official-vendor-doc` (부분) | `SB-HEALTH-C7` Does not prove: 외부 dependency 를 readiness 에 포함시키는 권장/비권장 정책은 본 인용 범위 밖. `SB-HEALTH-C8``needs-confirmation` — Spring 의 "default readiness 는 외부 의존성 미포함" verbatim 부재 |
| D11 | Startup validation = env var presence + DB schema migration history + required adapter bean — external endpoint reachability 는 startup-time 검사하지 않음 | **Mechanism SUPPORTED**: `raw/official-docs/k8s-configure-probes-task-page.md#K8S-PROBE-TASK-C1` (startup probe 의 일차 use case = legacy / slow-starting 워크로드 보호 — startup validation 의 외부 dependency 는 startup probe 가 cover 한다는 분리 정당화), `K8S-PROBE-TASK-C4` ("If your container usually starts in more than initialDelaySeconds + failureThreshold × periodSeconds, you should specify a startup probe that checks the same endpoint as the liveness probe" — runtime probe 로 외부 reachability 위임하는 메커니즘 정당화). **Scope decision stays partially UNSUPPORTED**: env var presence / DB migration history / adapter bean 의 각 항목이 startup validation 에 포함되어야 한다는 공식 spec 없음 — ca-tmpl 운영 가정 | `official-vendor-doc` (startup probe ↔ runtime probe 분리 mechanism) + UNSUPPORTED (validation 항목 구성) | `K8S-PROBE-TASK-C1` Does not prove: startup probe 가 모든 워크로드 default 라는 뜻 아님 — 본 인용은 "legacy applications" 한정. `K8S-PROBE-TASK-C4` 의 "should... the same endpoint" 는 권고 — startup probe endpoint 가 liveness 와 반드시 같아야 하거나 달라야 한다는 강제 아님. "startup-time 외부 endpoint 검사 anti-pattern" 의 공식 경고 자체는 본 capture 에 없음. **CODE 정합**: 실 구현은 sibling `feature-migration-startup-contract` (`RequiredEnvironmentValidator`/`MigrationStartupRunner`/`StartupSafetyValidator`, exit 78/70/71/72) — 본 branch 는 *scope policy* owner, 코드/에러코드는 위임 (§Audit `OWNERSHIP_DRIFT`) |
| D12 | JVM timezone UTC 강제 + NTP drift > 5초 시 readiness fail 검토 | **UTC part SUPPORTED**: `raw/official-docs/rfc3339-datetime-utc.md#RFC3339-C1` ("Z" suffix = UTC offset 00:00, ICAO "Zulu" 정의), `RFC3339-C2` ("true interoperability is best achieved by using Coordinated Universal Time (UTC)" — local timezone rule 의 daylight saving 복잡성으로 인한 IETF Standards Track 권고). **5s drift stays UNSUPPORTED**: NTP drift > 5초 threshold 의 정량 값은 RFC 3339 범위 밖 — NTP (RFC 5905) / NIST 별도 raw 필요. health endpoint timestamp 가 readiness 에 미치는 영향의 mechanism 도 본 RFC 범위 밖 | `official-standard` (UTC 권고 — IETF RFC 3339 Standards Track) + UNSUPPORTED (5s threshold + readiness 연동) | `RFC3339-C2` Does not prove: "UTC 만 허용" strict MUST 아님 — `best achieved by` 는 권고 (numeric offset 도 syntactically valid). NTP drift threshold 의 정량 spec 자체는 본 RFC 범위 밖 — `Claims To Verify` 의 NTP 5초 threshold 검증 항목 참조. **CODE 정합**: `Clock.systemUTC()``IdempotencyConfig:27` 에 실재 (UTC clock actually-implemented). JVM `-Duser.timezone=UTC` / `TZ=UTC` 는 container env (owner `feature-container-runtime-contract`). NTP-drift readiness check 는 코드 부재 = `planned` |
| D13 | Service mesh-based health (Istio) 대안 거부 | `raw/official-docs/runtime-health-istio-mesh-health-check.md#RH-IST-C1`, `raw/official-docs/runtime-health-istio-mesh-health-check.md#RH-IST-C2`, `raw/official-docs/runtime-health-istio-mesh-health-check.md#RH-IST-C3` (mTLS + httpGet probe 실패 / probe rewrite default 활성화 / sidecar 가 response body strip) | `official-vendor-doc` (Istio 공식) | `RH-IST-C2` Does not prove: probe rewrite 가 application 자체의 deadlock 을 감지한다는 뜻 아님 — sidecar→app HTTP probe 통과만 확인. ca-tmpl 의 "sidecar/app 살아있음 구분 불명확" 평가 와 정합 |
## Health Endpoint Contract
| endpoint | shape owner | default meaning | failure condition |
| --- | --- | --- | --- |
| `/actuator/health/liveness` | runtime-health | JVM process can continue | dependency outage alone fails liveness |
| `/actuator/health/readiness` | runtime-health | can receive traffic and required deps ready | migration/startup validation 중 healthy |
| `/actuator/health/startup` | runtime-health | startup/migration validation completed | absent startup gate in deployable profile |
> ⚠️ **구현 상태 = `planned`**: ca-tmpl 코드에는 현재 custom `GET /healthcheck` (`HealthcheckController:16`, `{"status":"UP"}`) 만 존재하며, 위 3개 actuator probe endpoint + Spring Boot Actuator Health Groups 설정은 미작성이다. 상세 + reconcile 권고는 §Audit `HEALTH_ENDPOINT_NOT_IMPLEMENTED`, 구현 절차는 §구현 가이드 1 참조.
## Required vs Optional Dependency Matrix
이 branch는 dependency taxonomy 표만 owns. 실제 dependency 분류는 `integration-adapter-templates`와 cross-link.
| dependency type | required | startup validation | readiness 영향 |
|-----------------|----------|--------------------|------------------|
| primary DB | yes | connection + migration history | unavailable → readiness fail |
| primary cache (Redis enabled 시) | conditional | ping | unavailable → degraded ready (cache-aside fallback) |
| message broker (Kafka, outbox publish) | no — fail-open | none (producer lazy) | unavailable → degrade (outbox 가 DB 보존 후 retry; **readiness 미반영**) |
| notification adapter (Slack/Email) | no | none | unavailable → degrade |
> dependency taxonomy 표의 owner는 본 branch. 실제 adapter별 분류(Kafka/Redis/Slack/Email 등)와 fail-open/closed 정책 SSOT는 integration-adapter-templates branch consume. 양방향 cross-link.
## Startup Validation Scope
- env var presence + type/range 검증.
- DB schema migration history 일치 확인.
- required adapter bean 등록 확인.
- external endpoint reachability는 startup-time에 검사하지 않음 (runtime probe로 대체).
- JVM timezone UTC 강제. NTP drift > 5초 시 readiness fail 검토 (테스트 계약 항목).
## Decisionized Work Items
| item | Decision | Allowed | Forbidden | Required test |
| --- | --- | --- | --- | --- |
| graceful shutdown | stop readiness first, drain inflight, then exit | force stop after timeout | accept new traffic while draining | lifecycle smoke |
| scheduler failure | structured error log + retry/DLQ owner mapping | fail-fast for critical jobs | swallow exception | job failure test |
| executor rejection | map to operational error/log with executor name | shed load with 503 | generic internal without context | rejection test |
| resource exhaustion | memory/disk/temp classified separately | platform alert first | raw OOM only | resource failure mapping |
## 구현 가이드
> *결정* 이 "*무엇*" 이라면 본 §는 "*어디에 어떻게*" 의 사전 명세. 본 branch 는 ca-tmpl 운영 계약의 **runtime health + lifecycle 표면**을 owns — 단, 구체 error-code / env-key / executor 설정 / 메트릭은 sibling branch 가 SSOT (registry `owner_branch` 기준). 따라서 아래 sub-section 은 본 branch 가 *정하는 것* (endpoint shape, dependency taxonomy, startup validation scope, shutdown ordering, clock readiness policy) 만 명세하고, sibling-owned 메커니즘은 **위임 포인터(R3)** 로 남긴다. ca-tmpl 코드 anchor 는 `/home/donghyeon/workspace/ca-tmpl/src` (read-only 대조 2026-06-14).
### 1. Health probe endpoint shape + readiness group membership
> **Trace**: D3 (`K8S-PROBE-C4`/`C5`) + D9 (`SB-HEALTH-C1`/`C2`/`C3`/`C6`, `K8S-PROBE-C1`/`C3`) + D10 (`SB-HEALTH-C7`). Health Endpoint Contract 표가 owner.
>
> - **UNSUPPORTED_IMPL_DECISION**: (a) readiness group `include` 멤버의 정확한 indicator 이름 집합 — `SB-HEALTH-C8` 이 `needs-confirmation` 이라 "Spring default readiness 가 외부 dependency 미포함"의 verbatim 미확보 → 어떤 indicator 를 명시 include 할지는 구현자 trade-off. (b) 기존 custom `/healthcheck` (`HealthcheckController:16`) 를 retire 할지 actuator 와 공존할지 — 두 endpoint 공존 시 운영 혼선 vs migration 비용 trade-off.
| 구현 항목 | 명세 | 상태 | Anchor |
|---|---|---|---|
| actuator probe 활성화 | `management.endpoint.health.probes.enabled=true` + `management.endpoint.health.group.{liveness,readiness,startup}.include=...` | `actually-implemented` | 2026-06-15 worktree `ca-tmpl-runtime-health-lifecycle`. `spring-boot-starter-actuator` 추가 + `application.yml` management 블록 |
| startup group/probe | startup gate 를 readiness 와 분리해 migration 중 readiness/liveness 오판 방지 | `actually-implemented` | `management.endpoint.health.group.startup.include=readinessState` |
| liveness 멤버 | `livenessState` 만 — 외부 dependency 미포함 (outage 시 restart loop 방지) | `actually-implemented` | `management.endpoint.health.group.liveness.include=livenessState` |
| readiness 멤버 | `readinessState` + `db` (primary DB — REQUIRED) — optional 의존성 제외 | `actually-implemented` | `management.endpoint.health.group.readiness.include=readinessState,db` |
| 기존 endpoint | custom `GET /healthcheck``{"status":"UP"}` (actuator 미사용) | `actually-implemented` | `adapter-web/.../HealthcheckController.java:16` |
| exposure/auth policy | **OUT_OF_BRANCH_SCOPE (R3)** — actuator 노출/인증은 [[raw/branch-notes/feature-management-actuator-security-contract]] (D2) | 위임 ⚠️ §Audit `PROBE_AUTH_BLOCKER` (현재 probe 401) | governing `security-baseline-jwt-actuator-secrets` |
### 2. Required-dependency → readiness wiring (taxonomy → group membership)
> **Trace**: D10 + §Required vs Optional Dependency Matrix. CompositeHealthContributor include/exclude 메커니즘 = `SB-HEALTH-C7`.
>
> - **UNSUPPORTED_IMPL_DECISION**: "degraded ready" (primary cache conditional) 를 Spring HealthStatus 로 어떻게 표현할지 (UP-with-detail vs custom status) — Spring status enum 매핑은 구현자 선택. 인용 자료에 spec 없음.
| dependency | readiness 멤버십 | 위임 owner (R3) |
|---|---|---|
| primary DB (required) | readiness group include → unavailable=DOWN | adapter 분류는 `feature-integration-adapter-templates` |
| message broker (Kafka, outbox publish) | **readiness 제외** — Kafka 기본 비활성(`DisabledMessagePublisher`) + publish 실패는 outbox retry, broker HealthIndicator 부재 (2026-06-15 런타임 확인: readiness body 에 broker component 없음) | mechanism `feature-domain-event-outbox-contract` + fail-open/closed `feature-integration-adapter-templates` |
| primary cache (conditional) | readiness 제외 → cache-aside fallback = degraded ready | `feature-cache-consistency-contract` |
| notification (Slack/Email, optional) | readiness 제외 → degrade only | `feature-integration-adapter-templates` (fail-open/closed) |
| multi-instance 일치 | readiness 시 `APP_MULTI_INSTANCE_ENABLED` flag ↔ distributed-lock contract test 결과 일치 verify (consume only) | D8 — flag SSOT `feature-env-driven-runtime-configuration`, lock `feature-distributed-lock-contract` |
### 3. Startup validation scope (policy owner here, 코드 위임)
> **Trace**: D11 (`K8S-PROBE-TASK-C1`/`C4`). 본 branch = startup validation 에 *무엇이 포함되는가* 의 scope policy owner. 코드 + exit-code 매핑은 sibling `feature-migration-startup-contract` 가 SSOT (§Audit `OWNERSHIP_DRIFT`).
>
> - **UNSUPPORTED_IMPL_DECISION**: env presence / migration history / adapter bean 3항목 구성 자체는 ca-tmpl 운영 가정 (D11 partially-unsupported) — 공식 spec 없음.
| validation 항목 (scope) | 위임 구현 (sibling) | exit code | Anchor |
|---|---|---|---|
| env var presence (datasource) | `RequiredEnvironmentValidator` | 78 `STARTUP_VALIDATION_FAILED` | `app-bootstrap/.../runtime/startup/RequiredEnvironmentValidator.java` |
| DB schema migration history | `MigrationStartupRunner` (readiness-gated) | 70 `MIGRATION_FAILED` | `.../runtime/startup/MigrationStartupRunner.java` |
| prod-forbidden flyway flags | `FlywayProdSafetyValidator` | 71 `PROFILE_MISMATCH` | `.../runtime/startup/FlywayProdSafetyValidator.java` |
| required adapter bean 등록 + prod-unsafe toggle | `StartupSafetyValidator` | 72 `REQUIRED_ADAPTER_DISABLED` | `.../runtime/StartupSafetyValidator.java:57-100` |
| StartupPhase 라벨 (구조화 로그) | `StartupPhase` enum: env-validation / migration / adapter-enablement / profile-check | — | `.../runtime/startup/StartupPhase.java` |
| external endpoint reachability | **금지** — startup-time 검사 안 함, runtime probe 로 위임 | — | D11 (`K8S-PROBE-TASK-C4`) |
### 4. Graceful shutdown ordering + budget sync
> **Trace**: D4 (`SPRING-SMARTLC-C3`/`C7` — descending stop phase + async `stop(Runnable)` + phase-level timeout). 본 branch = shutdown *ordering invariant* + *budget ≤ terminationGracePeriod sync 요구* owner. 정량 값은 sibling SSOT.
>
> - **UNSUPPORTED_IMPL_DECISION**: 35s/20s/5s/10s 조합은 ca-tmpl 운영 가정 (D6 UNSUPPORTED_DECISION). `RH-DD-C1`~`C4` 는 `needs-confirmation`.
> - **OUT_OF_BRANCH_SCOPE (R3)**: `terminationGracePeriodSeconds=35s` + `preStop sleep=5s` 는 K8s manifest 값 → §범위 Out of scope. `feature-container-runtime-contract` 가 owner.
| 항목 | 명세 | 위임/상태 | Anchor |
|---|---|---|---|
| ordering invariant | SIGTERM → readiness DOWN (신규 traffic 차단) → server inflight drain → outbound 컴포넌트 descending stop → exit | 본 branch owns (D4) | `SPRING-SMARTLC-C3` |
| spring 설정 | `server.shutdown=graceful` + `spring.lifecycle.timeout-per-shutdown-phase=${APP_SERVER_SHUTDOWN_TIMEOUT}` | `actually-implemented` (config) | `app-bootstrap/.../application.yml:203-205, 211` |
| server phase timeout 값 | `APP_SERVER_SHUTDOWN_TIMEOUT` default **30s** | 위임 `feature-env-driven-runtime-configuration` | `env-keys.yaml` (validation: `≤ k8s terminationGracePeriod`) |
| executor await | `setWaitForTasksToCompleteOnShutdown(true)` + `setAwaitTerminationSeconds(19)` ("20s budget 1s margin; 25s forbidden") | 위임 `feature-background-job-async-contract` | `app-bootstrap/.../async/AsyncExecutorConfig.java:46,72-73` |
| budget sync 요구 | app shutdown budget **≤** terminationGracePeriodSeconds — 초과 시 SIGKILL → inflight 유실 | 본 branch invariant + container-runtime 값 | §엣지·실패·의존 |
### 5. executor / resource) — taxonomy owns here, mechanism 위임
> **Trace**: §Decisionized Work Items. 본 branch = 실패 표면 *분류 policy* owner. 구체 error-code / executor 설정 / scheduler 코드 / 메트릭은 sibling SSOT (§Audit `OWNERSHIP_DRIFT`).
>
> - **UNSUPPORTED_IMPL_DECISION**: resource exhaustion (memory/disk/temp) 분류는 본 branch policy 지만 대응 registry error-code 가 **부재** (error-codes.yaml `NOT FOUND`) → `RESOURCE_*` 코드는 "신규 제안" / `planned`.
| 실패 표면 | policy (본 branch) | 위임 mechanism (sibling) | Anchor |
|---|---|---|---|
| scheduler failure | structured error log + retry/DLQ owner mapping; critical=fail-fast; swallow 금지 | `OutboxRelayScheduler.relay()` — 모든 Exception catch + ERROR 로그 + 다음 tick 재시도 (thread 생존) | `app-bootstrap/.../outbox/OutboxRelayScheduler.java:66-87` (`feature-domain-event-outbox-contract`) |
| executor rejection | executor name 포함 operational error/log + 503 shed; context 없는 generic internal 금지 | `LoggingAbortPolicy``OperationalError.JOB_EXECUTOR_REJECTED` (`TRANSIENT_DEPENDENCY` / 503 / retryable) + 메트릭 `executor.rejected.total`·`executor.saturation` | `AsyncExecutorConfig.java:71`, `shared-contract/.../OperationalError.java:148`, `error-codes.yaml` (`feature-background-job-async-contract`) |
| resource exhaustion | memory/disk/temp 별도 분류; platform alert first; raw OOM only 금지 | **planned** — 대응 `RESOURCE_*` error-code 미존재 (신규 제안 필요) | error-codes.yaml `NOT FOUND` |
### 6. Clock / timezone readiness
> **Trace**: D12 (`RFC3339-C1`/`C2` — UTC interoperability 권고).
>
> - **UNSUPPORTED_IMPL_DECISION**: NTP drift > 5초 threshold + readiness-gating 메커니즘은 무출처 (RFC 3339 범위 밖). **2026-06-14 자동조사 결론**: 어떤 공식 표준(RFC 5905/7519, NIST SC-45, K8s)도 app-readiness 의 NTP-drift 임계값을 정의하지 않으며, readiness 를 clock skew 로 gating 하면 동일 노드 모든 pod 의 동시 readiness fail(cascade) 위험 → **Alt 2(clock-agnostic readiness + 인프라 계층 모니터링 위임)** 권고. 본 sub-section 의 "NTP readiness" 행은 사용자 D12 개정 확정 전까지 `planned` 유지. 상세 §Audit `NTP_READINESS_ANTIPATTERN`.
> - **OUT_OF_BRANCH_SCOPE (R3)**: JVM `-Duser.timezone=UTC` / `TZ=UTC` 는 container env → `feature-container-runtime-contract` (governing doc: `TZ=UTC`, `LANG=C.UTF-8`).
| 항목 | 명세 | 상태 | Anchor |
|---|---|---|---|
| UTC clock | `Clock.systemUTC()` bean (timestamp 생성 UTC 고정) | `actually-implemented` | `app-bootstrap/.../idempotency/IdempotencyConfig.java:27` |
| JVM timezone | `TZ=UTC` container env 강제 (production) + `-Duser.timezone=UTC` test JVM arg (test pinning) | container env 위임 `feature-container-runtime-contract`; test arg `actually-implemented` 2026-06-15 (`app-bootstrap/build.gradle` `tasks.named('test')`) | `RuntimeHealthLifecycleContractTest#jvm_default_timezone_is_utc` 로 검증 |
| NTP drift readiness | drift > 5초 시 readiness fail | `planned` (무출처, 코드 부재) | D12 / Claims To Verify / §Audit 자동조사 |
## 엣지·실패·의존
> R4(깊이 게이트) 캡처용. 정상 경로 외 *구현 중 부딪힐* 실패/엣지/타 계약 의존.
- **실패·엣지 경로**:
- **readiness flip race**: migration 진행 중 startup probe 통과 전까지 readiness 는 DOWN 이어야 함 (D3). readiness 가 migration 완료 전 UP 되면 un-migrated 인스턴스로 traffic 유입.
- **liveness ≠ dependency outage**: DB outage → liveness 200 / readiness 503 (D9, `K8S-PROBE-C1`). liveness 가 외부 의존성 실패로 죽으면 cascading restart loop.
- **graceful shutdown race**: app shutdown budget > terminationGracePeriodSeconds → SIGKILL → inflight 유실 (D4/D6 budget sync invariant). executor await 19s + server phase 30s 가 grace 35s 안에 drain 완료해야 함.
- **executor rejection under load**: queue capacity 200 초과 → `LoggingAbortPolicy` → 503 (`TRANSIENT_DEPENDENCY`). executor-name context 없이 shed 하면 금지 (Decisionized Work Items).
- **scheduler 침묵 swallow**: `OutboxRelayScheduler` 가 모든 Exception catch + 생존 — business 실패가 조용히 삼켜지면 안 됨 (status 전이는 use case 에서 로깅).
- **clock skew 미감지**: NTP drift 미감지 시 JWT exp 검증 / distributed-lock TTL / idempotency timestamp 왜곡 (ca-tmpl audit report 의 "clock drift 노드가 readiness UP 유지" 격리 갭).
- **startup-time 외부 reachability 미검사**: 필수 외부 의존성이 boot 시 down 이어도 인스턴스는 ready 가 됨 (D11) → runtime readiness probe 가 잡아야 함.
- **다른 계약 의존**:
- [[raw/branch-notes/feature-env-driven-runtime-configuration]] `D8``APP_MULTI_INSTANCE_ENABLED` / `APP_SERVER_SHUTDOWN` / `APP_SERVER_SHUTDOWN_TIMEOUT` (consume; readiness 가 flag↔lock-test 일치 verify).
- [[raw/branch-notes/feature-container-runtime-contract]] — `terminationGracePeriodSeconds=35s` / `preStop=5s` / `TZ=UTC` / JVM ergonomics (K8s manifest + container env; 본 branch budget 은 ≤ grace 로 sync).
- [[raw/branch-notes/feature-migration-startup-contract]] — startup validators + exit code 78/70/71/72 + `StartupErrorCode`/`StartupPhase` (본 branch 가 scope 정의, 해당 branch 가 코드 구현).
- [[raw/branch-notes/feature-background-job-async-contract]] — `applicationTaskExecutor` + `LoggingAbortPolicy` + `JOB_EXECUTOR_REJECTED` + executor 메트릭 + awaitTermination 19s (본 branch 가 rejection policy 의도, 해당 branch 가 구현).
- [[raw/branch-notes/feature-domain-event-outbox-contract]] — `OutboxRelayScheduler` (scheduler 실패 mechanism).
- [[raw/branch-notes/feature-distributed-lock-contract]] `D1`/`D3``distributedLockProvider` bean; multi-instance readiness 일관성.
- [[raw/branch-notes/feature-integration-adapter-templates]] — adapter→dependency 분류 + fail-open/closed SSOT.
- ⚠️ **BLOCKER** [[raw/branch-notes/feature-management-actuator-security-contract]] `D2` — actuator endpoint exposure/auth. **2026-06-15 런타임 검증**: probe shape 는 정확하나 `SECURITY_PUBLIC_PATHS=/api/healthcheck` 만 public + 코드상 management `SecurityFilterChain` 부재 → `/actuator/health/{liveness,readiness,startup}` 가 JWT 인증 뒤 → kubelet(토큰 없음) **401** → liveness=restart loop / readiness=never-ready / startup=kill. 이 sibling 이 probe 경로를 unauthenticated 허용(또는 별도 management port)하기 전까지 probe end-to-end **비동작****2026-06-15 `src/.env` interim 으로 로컬/런타임 해소**(probe 200 / 집계 401). 정식 owner 는 sibling. (D2 위임 — probe shape 는 본 branch, exposure 는 interim 후 sibling 이관. 상세 §Audit `PROBE_AUTH_BLOCKER`.)
## Audit & Findings (ca-tmpl ground-truth 대조 2026-06-14)
> `/branch-spec` §2 — branch-note 의 명칭/매핑이 registry/코드 enum 과 어긋날 때 surface. 사용자 작성 결정 영역은 auto-rewrite 하지 않고 *정합 권고*만 기록.
- **`HEALTH_ENDPOINT_NOT_IMPLEMENTED`** ~~(정합 권고)~~**2026-06-15 해소**: `feature-runtime-health-lifecycle-contract` worktree 에서 `spring-boot-starter-actuator` 추가 + `management.endpoint.health.probes.enabled=true` + 3개 group include 설정 완료 (`actually-implemented`). custom `GET /healthcheck` (`HealthcheckController:16`) 는 **공존** — task 명세가 retire 금지를 명시함. actuator probe 는 별도 경로(`/actuator/health/{liveness,readiness,startup}`)로 추가됨. exposure/auth policy 는 parallel `feature-management-actuator-security-contract` 소유 (unchanged).
- **`SHUTDOWN_BUDGET_DRIFT`** (정합 권고): D6/§결정사항 의 "app shutdown timeout = 20s" 가 코드와 어긋남 — 코드 실측은 (a) executor await `setAwaitTerminationSeconds(19)` (`AsyncExecutorConfig:46`, "container 20s budget 1s cleanup margin; 25s forbidden"), (b) server phase timeout `APP_SERVER_SHUTDOWN_TIMEOUT` default **30s**. 노트가 executor-await(19s)와 server-phase-timeout(30s) 두 축을 "20s" 하나로 뭉갬. → 권고: D6 를 *executor await 19s / server phase 30s / terminationGracePeriod 35s(manifest)* 세 축으로 분리. (사용자 결정 영역 — auto-rewrite 안 함.)
- **`OWNERSHIP_DRIFT`** (정합 — 위임 확인): 본 노트가 표로 다루는 일부 계약값의 registry `owner_branch` 는 sibling 임 (D2/D8 의 "shape/scope/policy 만 owns" 와 정합):
- `JOB_EXECUTOR_REJECTED` (`TRANSIENT_DEPENDENCY`/503/retryable) → `feature-background-job-async-contract` (`error-codes.yaml`, `OperationalError.java:148`).
- `STARTUP_VALIDATION_FAILED(78)`/`MIGRATION_FAILED(70)`/`PROFILE_MISMATCH(71)`/`REQUIRED_ADAPTER_DISABLED(72)``feature-migration-startup-contract`.
- `executor.saturation`/`executor.rejected.total``feature-background-job-async-contract` (`metrics.yaml`).
- `APP_SERVER_SHUTDOWN`/`APP_SERVER_SHUTDOWN_TIMEOUT`/`APP_MULTI_INSTANCE_ENABLED``feature-env-driven-runtime-configuration` (`env-keys.yaml`).
- `StartupSafetyValidator:35``distributedLockProvider` multi-instance 주석은 [[raw/branch-notes/feature-distributed-lock-contract]] (D1/D3) 로 reassign 됨.
→ 조치: 이 코드/키들을 본 branch 가 *소유*한다고 주장하지 않음. §구현 가이드 의 위임 포인터(R3) 유지.
- **`GOVERNING_DOC_STALE`** (Advisory): governing `wiki/projects/ca-tmpl/runtime-container-health-migration.md` (last_reviewed 2026-05-22) 은 "C2 미진입 / 코드 없음" 으로 기술하나, startup validators + async executor + outbox scheduler 는 현재 코드 존재 (sibling-owned). Health endpoint 슬라이스는 여전히 `planned` (정합). → 본 branch health 슬라이스 착수 시 governing doc refresh 권고. 비차단.
- **`RESOURCE_CODE_ABSENT`** (planned): resource-exhaustion 분류(memory/disk/temp)에 대응하는 registry error-code 가 `error-codes.yaml`**없음**. 별도 operational code 가 필요하면 owner_branch=본 branch 로 "신규 제안" row 등록 (§구현 가이드 5).
- **`NTP_READINESS_ANTIPATTERN`** (정합 권고 — 자동조사 2026-06-14): D12 의 "NTP drift > 5초 시 readiness fail" 은 `wiki-decision-researcher` 조사 결과 **어떤 공식 표준에도 근거 없음** — RFC 5905(STEPT 125ms / PANICT 1000s, app readiness 임계값 아님)·RFC 7519(JWT leeway "a few minutes", 숫자 없음)·NIST SP 800-53 SC-45(org-defined 위임)·K8s 공식(클럭을 readiness 사유로 미정의). "5초" 는 무출처 운영 가정으로 확정. 또한 readiness 를 clock-skew 로 gating 하면 동일 노드의 모든 pod 이 동시에 readiness fail → cascade failure 위험(AWS EKS prescriptive guidance). 조사 권고 = **Alt 2**: readiness 는 clock-agnostic, clock-skew 모니터링은 인프라 계층(Prometheus `node_timex_offset_seconds` + K8s NodeProblemDetector `NTPProblem` NodeCondition)에 위임. → **권고(사용자 결정 영역 — auto-rewrite 안 함)**: D12 의 readiness-gating 부분을 제거하고 (a) UTC 강제(유지, `RFC3339-C1`/`C2` + `Clock.systemUTC()`), (b) clock-skew = 인프라 위임으로 분리. 채택 시 raw 4건 archive(RFC 5905 / RFC 7519 / K8s NPD / node-exporter mixin) 후 §Sources·§Decision Evidence Map 갱신. 미채택(Alt 3 startup-only sanity check) 선택지도 조사에 포함 — 결정 전 확인 필요: ca-tmpl 의 실제 JWT leeway / 분산락 TTL(허용 드리프트 역산), NPD·node-exporter 배포 여부.
- **`PROBE_AUTH_BLOCKER`** (⚠️ 차단 의존 — 2026-06-15 런타임 검증; 2026-06-15 sentinel BLOCKED): worktree 부팅 후 unauthenticated curl 결과 `/actuator/health` + `/actuator/health/{liveness,readiness,startup}` 전부 **HTTP 401 `AUTH_TOKEN_MISSING`** (`/api/healthcheck` 만 200). 원인: `SECURITY_PUBLIC_PATHS=/api/healthcheck` + 코드에 management/actuator `SecurityFilterChain` 부재(`EndpointRequest`/`toAnyEndpoint` 검색 0건). K8s kubelet 은 JWT 없이 probe 를 호출하므로 liveness 401=restart loop / readiness 401=never-ready / startup 401=kill → probe **end-to-end 비동작**. → **조치(sibling 코드)**: `feature-management-actuator-security-contract``/actuator/health/liveness`·`/actuator/health/readiness` 를 unauthenticated 허용(`EndpointRequest.to("health")` permitAll 또는 별도 `management.server.port`). **본 branch 코드 변경 아님**(D2 exposure/auth 위임). → **2026-06-15 interim 시도 후 revert**: `src/.env``SECURITY_PUBLIC_PATHS` 에 3개 sub-path 를 interim 추가했으나 `ca-architect-sentinel`**not-ready(blocking:1)** 판정 — `verifyPublicPathSnapshot` 스냅샷 미갱신 + 이 branch scope 밖(actuator 인증/노출은 `feature-management-actuator-security-contract` + 별도 `management.server.port=9001` 에서 처리되므로 8080 `SECURITY_PUBLIC_PATHS` 에 추가하는 것이 의미상 잘못됨). → **revert 완료(2026-06-15)**: `SECURITY_PUBLIC_PATHS=/api/healthcheck` 단일값으로 복원. `verifyPublicPathSnapshot` PASS. `src/.env` = HEAD~1 identical. **현재 상태**: probe shape `actually-implemented`, probe auth = **여전히 sibling BLOCKER**`feature-management-actuator-security-contract` 정식 구현(별도 `management.server.port=9001` 또는 `EndpointRequest.to("health").permitAll()`) 전까지 kubelet probe 401 은 expected in this branch.
- **`BROKER_READINESS_DRIFT`** (정합 — 2026-06-15 코드 대조 후 노트 정정 완료): §Dependency Matrix(D10) 가 broker 를 "required(publish) → readiness fail" 로 기술했으나 **구현은 broker 를 readiness 에서 제외**(`readiness.include=readinessState,db`). 코드 ground truth: Kafka 기본 비활성(`DisabledMessagePublisher`) + fail-open(publish 실패는 outbox 흡수) + broker HealthIndicator 부재. transactional outbox 설계상 broker 가용성이 readiness 를 gating 하면 안 됨 → **코드가 옳음, 노트가 stale**. → 본 세션에서 D10 + §Matrix + §구현 가이드 2 를 broker=optional·fail-open 으로 정정. **코드 변경 불필요.**
- **`ACTUATOR_METERREGISTRY_SIDEEFFECT`** (확인 필요 — 2026-06-15): 본 branch 가 `spring-boot-starter-actuator` 를 classpath 에 추가 → 여태 "no Actuator → no-op" 이던 `MeterRegistry` 가 actuator autoconfiguration 으로 **활성화**(tracing/metrics/outbox/lock 의 `ObjectProvider<MeterRegistry>` no-op fallback 이 실제 등록으로 전환). 부팅 로그에 `SimpleMeterRegistry — A MeterFilter is being configured after a Meter has been registered` WARN 2건(cardinality filter ordering — 일부 early meter 에 미적용 가능). → **확인(metrics 브랜치)**: (a) metrics dormant→active 가 의도된 통합 시점인지, (b) `MetricsCardinalityMeterFilter`/`MetricsContractConfig` filter 설치를 meter 등록 *이전* 으로 당겨 WARN 해소. `feature-metrics-alerting-contract` 소유 — 본 branch 코드 변경 아님(actuator 의존은 health probe 에 필수).
## 테스트 계약
- required dependency가 unavailable이면 readiness가 실패해야 함.
- graceful shutdown 중 신규 요청 처리 정책이 명시되어야 함.
- scheduler failure가 조용히 삼켜지면 실패.
- async executor rejection이 INTERNAL without context로 뭉개지면 실패.
- startup probe 없이 migration/readiness race가 가능하면 실패.
- JVM timezone이 UTC가 아니면 실패.
- NTP drift > 5초 상태에서 readiness가 ready를 유지하면 실패 (검토 대상).
## 검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| Spring Boot 의 default readiness group 이 외부 의존성 (DB/Kafka) 을 포함하지 않음 → ca-tmpl 이 명시적 `management.endpoint.health.group.readiness.include` 필요 | `SB-HEALTH-C8``needs-confirmation` — verbatim 미확보 | Spring Boot reference 의 `actuator.endpoints.health.groups` 페이지 별도 fetch + `application.yml` config 검증 | `needs-confirmation` |
| startup probe total budget = `failureThreshold × periodSeconds` 산식의 K8s 공식 verbatim | `K8S-PROBE-C7``needs-confirmation` — task 페이지 truncate | task 페이지 `#define-startup-probes` sub-URL 직접 fetch | `needs-confirmation` |
| ca-tmpl 의 startup 30 × 5s = 150s 가 Spring Boot 콜드스타트 + JVM warmup + 외부 의존성 wiring 시간 cover | 실측 부재 | k8s deployment 실측 (startup 시간 분포 + p99) | `planned` |
| readiness fail → EndpointSlice 제거 → drain → preStop sleep → SIGTERM → shutdown timeout 의 e2e timing 이 ca-tmpl 의 PreStop 5s + grace 35s 와 정합 | `K8S-PROBE-C3` Does not prove: EndpointSlice 제거 propagation delay 본 인용 범위 밖 | chaos test — readiness fail 시 inflight request loss rate 측정 | `planned` |
| liveness probe 가 dependency outage 로 인해 실패하지 않음 (cascading restart 방지) | `K8S-PROBE-C1` Usage Boundary: liveness 가 모든 hang 검출하지 않음. ca-tmpl 의 "JVM process can continue" 정의 와 정합 검증 필요 | contract test: DB outage fixture → liveness 200 / readiness 503 | `planned` |
| Spring Boot graceful shutdown 시 readiness 자동 DOWN 전환 메커니즘 | 본 branch 인용 자료에 verbatim 부재 (`SB-HEALTH` Usage Boundary) | Spring Boot `features/graceful-shutdown.html` 별도 fetch | `needs-confirmation` |
| Datadog 의 preStop 5s + drain 20s + grace 35s 비율이 실제 Datadog 공식 권장 | `RH-DD-C1`~`C4` 모두 `needs-confirmation` — verbatim 미확보 | Datadog Engineering blog 원본 URL 재 fetch 또는 ca-tmpl 정책으로만 표현 | `needs-confirmation` |
| Istio probe rewrite 환경에서도 ca-tmpl 의 3-endpoint 분리 가 동작 | `RH-IST-C2` 는 probe rewrite 가 sidecar→app HTTP 만 — group 별 endpoint 가 sidecar 에서 어떻게 보이는지 별도 | Istio sandbox 환경 통합 test | `planned` |
| NTP drift > 5초 readiness fail 의 정량 threshold (5초) 출처 + readiness-gating 이 anti-pattern 인지 | `UNSUPPORTED_DECISION` — 외부 spec 인용 없음 | **조사 완료 (2026-06-14 `wiki-decision-researcher`)**: RFC 5905(STEPT 125ms/PANICT 1000s)·RFC 7519(JWT leeway "a few minutes")·NIST SP 800-53 SC-45(org-defined)·K8s 공식 어디에도 *app readiness 의 NTP-drift 임계값* 정의 없음 → "5초" 는 무출처 운영 가정 확정. readiness-gating 은 cascade-failure 위험(AWS EKS guidance) — **Alt 2 권고**: readiness 는 clock-agnostic, clock-skew 는 인프라 계층(Prometheus `node_timex_offset_seconds` + K8s NodeProblemDetector NTPProblem)에 위임. 채택 시 별도 raw 4건(RFC 5905·RFC 7519·K8s NPD·node-exporter mixin) archive. §Audit `NTP_READINESS_ANTIPATTERN` | `resolved (no authoritative standard)` — D12 readiness-gating 부분은 사용자 확정 후 Alt 2 로 개정 권고 |
| ca-tmpl 실 코드의 graceful shutdown 정량값 (executor await 19s + server phase 30s) 이 terminationGracePeriod 35s 안에서 inflight drain 완료 | `AsyncGracefulShutdownBehaviorTest` 는 behaviour test (19s-vs-25s 정확한 수치는 증명 안 함) | k8s 실측 또는 통합 lifecycle test 로 drain 완료 시간 측정 | `planned` |
| 3-endpoint actuator config (`management.endpoint.health.probes.enabled` + group include) 가 실제로 `/actuator/health/{liveness,readiness,startup}` 노출 | 2026-06-15 `actually-implemented``application.yml` management 블록 추가 + `spring-boot-starter-actuator` 의존성. HTTP-level endpoint 노출 검증은 `feature-management-actuator-security-contract` 가 exposure/security config 완료 후 통합 테스트 가능 | `locally-verified` (group config shape 레벨) |
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
> `/coverage` 가 채우는 **생성물** — 손으로 유지하지 않는다. governing 문서(frontmatter `governing_docs`)가 요구하는 관심사를 이 브랜치가 빠짐없이 덮는지의 결과. 기준: `rules/coverage-gate.md`. governing_docs: `wiki/projects/ca-tmpl/runtime-container-health-migration` (§Health + §Graceful Shutdown 슬라이스).
> 마지막 감사: 2026-06-14 (coverage-auditor) → **Covered** (Blocking 0 / Should-fix 2 = 위임 링크 보강으로 해소 / Advisory 1). Container 슬라이스(base image·JVM ergonomics·locale)와 Migration 슬라이스(Flyway·exit code)의 4개 관심사는 본 슬라이스 범위 밖 — 각각 `feature-container-runtime-contract` / `feature-migration-startup-contract` 소유(dropped, governing doc §Container·§Migration).
| 관심사 | 상태 | owner | 심각도 | 근거 |
|--------|------|-------|--------|------|
| liveness/readiness/startup 3-probe 분리 | covered-here | — | — | D3, D9 |
| Spring Boot Actuator Health Groups | covered-here | — | — | D9, D10 |
| Required vs Optional Dependency Matrix | covered-here | — | — | D10 + §Dependency Matrix |
| graceful shutdown ordering | covered-here | — | — | D4 (§구현 가이드 4) |
| graceful shutdown 정량값 (`APP_SERVER_SHUTDOWN*` / executor await) | delegated | [[raw/branch-notes/feature-env-driven-runtime-configuration]], [[raw/branch-notes/feature-background-job-async-contract]], [[raw/branch-notes/feature-container-runtime-contract]] (grace=35s manifest) | OK | §Audit `OWNERSHIP_DRIFT` + §엣지·실패·의존 |
| startup validation scope | covered-here | — | — | D11 |
| startup validators 코드 + exit code 78/70/71/72 | delegated | [[raw/branch-notes/feature-migration-startup-contract]] | OK | §구현 가이드 3 + §Audit `OWNERSHIP_DRIFT` |
| scheduled job 실패 정책 | covered-here | — | — | §Decisionized Work Items |
| scheduler 실 mechanism (`OutboxRelayScheduler`) | delegated | [[raw/branch-notes/feature-domain-event-outbox-contract]] | OK | §구현 가이드 5 |
| async executor rejection 정책 | covered-here | — | — | §Decisionized Work Items |
| executor 설정·`JOB_EXECUTOR_REJECTED`·메트릭 | delegated | [[raw/branch-notes/feature-background-job-async-contract]] | OK | §구현 가이드 5 + §Audit `OWNERSHIP_DRIFT` |
| resource exhaustion 분류 | covered-here | — | ⚪ Advisory | §Decisionized Work Items — `RESOURCE_*` code 부재(`planned`, §Audit `RESOURCE_CODE_ABSENT`) |
| system clock/timezone (UTC) | covered-here | — | — | D12 (`Clock.systemUTC()`) |
| JVM timezone `TZ=UTC` (container env) | delegated | [[raw/branch-notes/feature-container-runtime-contract]] | OK | §구현 가이드 6 |
| NTP drift readiness | covered-here | — | 🟡 Should-fix→해소 | D12 `planned` — 2026-06-14 조사: 무출처, Alt 2(clock-agnostic readiness + 인프라 모니터링 위임) 권고. §Audit `NTP_READINESS_ANTIPATTERN` |
| actuator endpoint exposure/auth | delegated | [[raw/branch-notes/feature-management-actuator-security-contract]] | 🟡 Should-fix (probe 401 — §Audit `PROBE_AUTH_BLOCKER`) | D2 + §구현 가이드 1 |
| multi-instance readiness 일관성 | delegated | [[raw/branch-notes/feature-env-driven-runtime-configuration]] (D8), [[raw/branch-notes/feature-distributed-lock-contract]] (D1/D3) | OK | §구현 가이드 2 |
## 구현 진행 기록 (2026-06-15 — CA Implementer)
> 작업 트리: `ca-tmpl-runtime-health-lifecycle` worktree (develop 에서 fork된 격리 환경).
> 구현된 범위: **health probe SHAPE** (liveness/readiness/startup 3-group split + readiness dependency taxonomy + JVM UTC timezone pinning). management port/exposure/security 는 parallel worktree 소유.
### 구현 사실 (actually-implemented, locally-verified 2026-06-15)
| 구현 항목 | 파일 | 상태 | 비고 |
|---|---|---|---|
| `spring-boot-starter-actuator` 의존성 추가 | `src/app-bootstrap/build.gradle` | `actually-implemented` | `implementation` 스코프 |
| `-Duser.timezone=UTC` test JVM arg | `src/app-bootstrap/build.gradle` (`tasks.named('test')` 블록) | `actually-implemented` | RuntimeHealthLifecycleContractTest 의 JVM TZ 어설션 핀 |
| `management.endpoint.health.probes.enabled=true` | `src/app-bootstrap/src/main/resources/application.yml` | `actually-implemented` | `management:` 블록 신규 추가 |
| `management.endpoint.health.group.liveness.include=livenessState` | 동상 | `actually-implemented` | |
| `management.endpoint.health.group.readiness.include=readinessState,db` | 동상 | `actually-implemented` | primary DB = REQUIRED 분류 |
| `management.endpoint.health.group.startup.include=readinessState` | 동상 | `actually-implemented` | startup gate |
| `RuntimeHealthLifecycleContractTest` (6개 테스트) | `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/runtime/RuntimeHealthLifecycleContractTest.java` | `locally-verified` | `ApplicationContextRunner` 기반 — HTTP 없음, SecurityFilterChain 없음 |
### ca-quality-reviewer 수정 (2026-06-15 — test quality + comment accuracy)
> 행동 변경 없음. 테스트 품질 + 주석 정확성 수정만.
| 수정 항목 | 파일 | 상태 | 비고 |
|---|---|---|---|
| `health_probes_enabled_is_bound``startup_group_includes_readiness_state` (메서드 리네임 + 어설션 교체) | `RuntimeHealthLifecycleContractTest.java` | `locally-verified` | 중복 어설션(liveness/readiness isNotNull 재확인) 제거 → startup 그룹 멤버십(`isMember("readinessState")`) 어설션으로 교체. startup 그룹을 liveness/readiness 수준의 커버리지 동등성으로 맞춤 |
| `jvm_default_timezone_is_utc` 주석 정정 — "aligns with production" 제거 | `RuntimeHealthLifecycleContractTest.java` | `locally-verified` | 이 테스트는 UTC timezone POLICY 핀 + test-JVM 결정론 보장만. 프로덕션 UTC 강제는 `feature-container-runtime-contract` (`TZ=UTC` Dockerfile) 소유임을 명시 |
| `tasks.named('test')` 블록 주석 정정 — "aligns with production" / "logging timezone default" 과장 제거 | `src/app-bootstrap/build.gradle` | `locally-verified` | `-Duser.timezone=UTC` 는 TEST JVM 전용(결정론적 타임스탬프 산술). 프로덕션 UTC 는 container-runtime-contract 위임 |
| `java.util.Set` / `java.util.TimeZone` FQN → import + 단순명 | `RuntimeHealthLifecycleContractTest.java` | `locally-verified` | 기존 파일 나머지 코드와 일관성 맞춤 |
#### 검증 명령 및 결과
- `./gradlew :app-bootstrap:test --tests '*RuntimeHealthLifecycleContractTest'`**BUILD SUCCESSFUL** (7/7 pass — `startup_group_includes_readiness_state` 포함)
- `./gradlew :app-bootstrap:test`**BUILD SUCCESSFUL** (전체 모듈 테스트 — 회귀 없음)
### 검증 명령 및 결과
- `./gradlew :app-bootstrap:test --tests '*RuntimeHealthLifecycleContractTest'`**BUILD SUCCESSFUL** (6/6 pass)
- `./gradlew :app-bootstrap:test`**BUILD SUCCESSFUL** (전체 모듈 테스트 — 회귀 없음)
- `./gradlew verifyCleanArchitectureDependencies`**BUILD SUCCESSFUL**
- `./gradlew verifyEnvKeys`**BUILD SUCCESSFUL** (97 env keys — application.yml 에 새 env placeholder 없음)
### 사후 revert (2026-06-15 sentinel BLOCKED → 수정)
- `ca-architect-sentinel` 판정: **not-ready, blocking:1**`src/.env``SECURITY_PUBLIC_PATHS` 3개 actuator 경로 추가가 스냅샷 미갱신 + 이 branch scope 밖.
- 조치: `SECURITY_PUBLIC_PATHS=/api/healthcheck` 로 revert (HEAD~1 identical).
- `./gradlew verifyPublicPathSnapshot`**BUILD SUCCESSFUL** ("1 public path(s) unchanged").
- `./gradlew :app-bootstrap:test --tests '*RuntimeHealthLifecycleContractTest'`**BUILD SUCCESSFUL** (revert 후에도 6/6 pass — test 는 public paths 에 의존하지 않음).
- `src/.env` 현재 = HEAD~1 (working tree 미스테이지).
### 구현 중 마주친 기술적 문제
1. **`AvailabilityHealthContributorAutoConfiguration` 조건 오인**: `livenessState`/`readinessState` 기여자는 K8s 환경 감지 조건(`@ConditionalOnBooleanProperty("management.health.livenessstate.enabled")`) 뒤에 있음. `ApplicationContextRunner` 에서 이 속성을 명시적으로 `true` 로 설정해야 하고 `ApplicationAvailabilityAutoConfiguration` 도 함께 등록해야 함.
2. **`HealthEndpointGroupMembershipValidator`**: 그룹 `include` 에 명시된 기여자가 컨텍스트에 없으면 startup fail. `db` 기여자를 `DownDbContributorConfig` @Bean 으로 등록해 해소.
3. **Package 오인**: 자동 완성 없이 `org.springframework.boot.autoconfigure.actuate.health` (잘못됨) → `org.springframework.boot.actuate.autoconfigure.health` (올바름) 로 수정.
## 마주친 문제
- Spring Boot 3.5.x 에서 `AvailabilityHealthContributorAutoConfiguration` 의 조건 구조 (Kubernetes 환경 감지 + 속성 explicit enable) 가 `ApplicationContextRunner` 슬라이스와 상호작용하는 방식을 확인해야 했음. 해결책: `management.health.livenessstate.enabled=true` / `management.health.readinessstate.enabled=true` 속성 명시적 추가.
## 묶음
<!-- GENERATED: sources:start -->
- [[raw/company-tech-blogs/runtime-health-datadog-engineering-graceful-shutdown]]
- [[raw/official-docs/adapter-spring-boot-autoconfig-custom-starter]]
- [[raw/official-docs/config-spring-cloud-kubernetes-configmap-reload]]
- [[raw/official-docs/k8s-configure-probes-task-page]]
- [[raw/official-docs/k8s-pod-lifecycle-probes-concept]]
- [[raw/official-docs/migration-flyway-official-concepts-and-repair]]
- [[raw/official-docs/migration-k8s-init-container-job-pattern]]
- [[raw/official-docs/rfc3339-datetime-utc]]
- [[raw/official-docs/runtime-health-istio-mesh-health-check]]
- [[raw/official-docs/runtime-health-k8s-probes-official]]
- [[raw/official-docs/runtime-health-spring-actuator-groups]]
- [[raw/official-docs/spring-smartlifecycle-reference]]
<!-- GENERATED: sources:end -->
<!-- GENERATED: blog-topics:start -->
- [[raw/blog-topics/spring-actuator-health-probe-group-split-2026-07-02]]
<!-- GENERATED: blog-topics:end -->
> 본 feature branch 는 leaf — Phase C2 실 코드 작성 완료 (health probe SHAPE 슬라이스).
### 오류 기록 (본 feature 작업 중 발생)
- **Spring actuator autoconfig package 오인**: `org.springframework.boot.autoconfigure.actuate.health.*` 는 존재하지 않음. 올바른 패키지는 `org.springframework.boot.actuate.autoconfigure.health.*` (actuate 와 autoconfigure 순서 반전). `ApplicationContextRunner` 사용 시 jar tf 로 확인 필요.
- **AvailabilityHealthContributor 조건 gap**: K8s 자동감지 없는 `ApplicationContextRunner` 에서 `livenessState`/`readinessState` 기여자는 비활성. `management.health.livenessstate.enabled=true` + `management.health.readinessstate.enabled=true` + `ApplicationAvailabilityAutoConfiguration` 등록으로 해소.
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- Spring Boot Actuator health probe group split (liveness/readiness/startup) — 각각의 의미와 K8s 연동.
- `StatusAggregator.getDefault()` — DOWN 하나가 포함되면 전체 DOWN 이 되는 이유.
- `ApplicationContextRunner` vs `@SpringBootTest` 차이 — actuator health 테스트에서 왜 runner 를 선택했는가.
- `HealthEndpointGroupMembershipValidator` 가 startup 에 실패하는 조건과 해결 패턴.
## 관련 일일 노트
> 이 브랜치를 작업한 날짜들. 양방향 nav 유지.
- (없음 — Phase E 운영 계약 설계 단계. C2 실 구현 착수 시 daily-note 연결)
## 완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목:
- `locally-verified` 항목:
- `prod-verified` 항목:
- **추출하지 않을 항목** (planned / documented-only / abandoned):