443 lines
53 KiB
Markdown
443 lines
53 KiB
Markdown
---
|
|
title: branch / feature-env-driven-runtime-configuration
|
|
source_type: branch-note
|
|
status: raw
|
|
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-004
|
|
kind: project-work-item
|
|
project: ca-skeleton-operational-contract
|
|
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-004
|
|
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1]
|
|
refines: []
|
|
overrides: []
|
|
depends_on: []
|
|
contract_packet: 1
|
|
branch: feature-env-driven-runtime-configuration
|
|
parent_branch:
|
|
related_projects: [ca-skeleton]
|
|
governing_docs: [wiki/projects/ca-tmpl/config-and-adapter-templates.md]
|
|
tags: [branch, ca-skeleton, env, configuration, runtime]
|
|
created: 2026-05-21
|
|
target_merge:
|
|
status_label: in-progress
|
|
contract_packet_sha256: e68380e05a6baa55af05d9d692b7986fc91202315b02276cecfd7bb83c0098ca
|
|
---
|
|
|
|
# branch: feature-env-driven-runtime-configuration
|
|
|
|
> Layer: `raw/branch-notes/` — 서버별 운영 전환을 env로 가능하게 하는 설정 계약을 정의합니다.
|
|
|
|
> [!important] 주도권 이전 고지 (2026-07-28)
|
|
> 아래 관심사의 **owner 가 신규 branch 로 이동**했다. 근거·절차: `docs/superpowers/specs/2026-07-28-ca-skeleton-production-capability-feature-decomposition-design.md` §5.
|
|
>
|
|
> | 이전 ID | 대상 D-row | 이전한 관심사 | 신규 owner |
|
|
> |---|---|---|---|
|
|
> | H8 | D8 의 **집행 메커니즘** 부분 | multi-instance 활성화 판정 — bean 이름 presence 검사(`SmartInitializingSingleton` + `getBeanProvider`) → typed capability descriptor | [[raw/branch-notes/feature-capability-provider-selection-contract]] |
|
|
>
|
|
> **이전 범위는 판정 메커니즘뿐이다.** `APP_MULTI_INSTANCE_ENABLED` flag 자체, `env-keys.yaml` row, `APP_` prefix 통일(D2), env drift 검증(D7)은 **본 branch 가 계속 소유**한다.
|
|
>
|
|
> **본문은 아직 제거하지 않았다.** 신규 branch 는 스캐폴딩 상태이므로 D8 은 재판정 전까지 잠정 근거로 유효하며, 그 시점에 `docs/superpowers/specs/2026-07-28-ca-skeleton-production-capability-feature-decomposition-design.md` §5.2 6단계에 따라 포인터로 치환한다. D8 의 집행 메커니즘은 이미 `UNSUPPORTED_IMPL_DECISION` 라벨이 붙어 있어 재판정 대상임이 노트 자체에 기록되어 있다.
|
|
|
|
<!-- 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`
|
|
- **완료 조건**: env configuration 6필드 contract와 invalid-config test가 통과한다
|
|
|
|
<!-- section-id: inherited-project-decisions -->
|
|
### 상속한 프로젝트 결정
|
|
|
|
| Decision Ref | Project Summary | Branch Application | Source |
|
|
|---|---|---|---|
|
|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1` | framework는 Spring Boot 3.5.14다 | Spring Boot env binding과 startup validation에 적용 | [[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 -->
|
|
|
|
## 묶음
|
|
|
|
<!-- GENERATED: sources:start -->
|
|
- [[raw/company-tech-blogs/adapter-togglz-ff4j-feature-toggle-library]]
|
|
- [[raw/company-tech-blogs/config-launchdarkly-feature-flag-best-practice]]
|
|
- [[raw/official-docs/config-12-factor-app-config]]
|
|
- [[raw/official-docs/config-aws-appconfig-feature-flag-deployment]]
|
|
- [[raw/official-docs/config-spring-boot-externalized-configuration]]
|
|
- [[raw/official-docs/config-spring-cloud-config-server-official]]
|
|
- [[raw/official-docs/config-spring-cloud-kubernetes-configmap-reload]]
|
|
<!-- GENERATED: sources:end -->
|
|
|
|
<!-- GENERATED: interviews:start -->
|
|
- [[raw/interviews/startup-fail-fast-config-validation-2026-06-06]]
|
|
<!-- GENERATED: interviews:end -->
|
|
|
|
<!-- GENERATED: errors:start -->
|
|
- [[raw/errors/global-sed-env-rename-pitfalls-2026-06-06]]
|
|
<!-- GENERATED: errors:end -->
|
|
|
|
<!-- GENERATED: blog-topics:start -->
|
|
- [[raw/blog-topics/env-example-drift-gate-gradle-2026-06-06]]
|
|
<!-- GENERATED: blog-topics:end -->
|
|
|
|
> 본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
|
|
|
|
### 근거 자료
|
|
|
|
- [[raw/official-docs/config-12-factor-app-config]] — D1 근거 (12-factor §III Config)
|
|
- [[raw/official-docs/config-spring-cloud-config-server-official]] — D3 대안 (Spring Cloud Config Server)
|
|
- [[raw/official-docs/config-spring-cloud-kubernetes-configmap-reload]] — D3 대안 (k8s ConfigMap reload)
|
|
- [[raw/official-docs/config-aws-appconfig-feature-flag-deployment]] — D3/D9 대안 (AWS AppConfig)
|
|
- [[raw/company-tech-blogs/config-launchdarkly-feature-flag-best-practice]] — D9 대안 (LaunchDarkly)
|
|
- [[raw/official-docs/config-spring-boot-externalized-configuration]] — D4 (Duration/DataSize binding 포맷), D6 (SPRING_PROFILES_ACTIVE relaxed binding 메커니즘), D10 (@ConfigurationProperties + @Validated startup validation)
|
|
|
|
### 오류 기록 (본 feature 작업 중 발생)
|
|
|
|
- [[raw/errors/global-sed-env-rename-pitfalls-2026-06-06]] — M1 일괄 rename 중 (1) zsh unquoted 변수 무분할로 sed no-op, (2) `s/LOG_/APP_LOG_/g` substring 충돌로 `SPRING_MAIN_LOG_STARTUP_INFO` 훼손. 둘 다 resolved.
|
|
|
|
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
|
|
|
- [[raw/interviews/startup-fail-fast-config-validation-2026-06-06]] — `SmartInitializingSingleton` vs `EnvironmentPostProcessor` vs `ApplicationReadyEvent`, 계층형 `@Validated`+JSR-303 / compact-constructor throw, prod 가드의 case-sensitive profile 매칭 트레이드오프, name 기반 bean presence 검사.
|
|
|
|
### Blog topics (이 작업에서 나온 글감)
|
|
|
|
- [[raw/blog-topics/env-example-drift-gate-gradle-2026-06-06]] — env drift gate 설계 여정(글감). ⚠ 이 노트는 1차 설계(surface=정답, registry 미강제)를 담고 있으나 **2026-06-08 B 결정으로 registry=SSOT(check C)로 전환** — surface→registry SSOT 전환 자체가 더 좋은 글감(블로그 갱신 시 반영).
|
|
|
|
<!-- section-id: branch-goal -->
|
|
## 목표
|
|
|
|
local/dev/staging/prod 서버별 동작이 코드 수정 없이 env로 전환되어야 합니다. error exposure, logging, tracing, adapter enablement, timeout/retry/security/datasource 설정을 env contract로 고정합니다.
|
|
|
|
- 이슈:
|
|
- PR:
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
- env key naming 기준.
|
|
- server profile matrix.
|
|
- error detail exposure toggle.
|
|
- logging/tracing toggle.
|
|
- datasource/pool env.
|
|
- outbound timeout/retry/circuit breaker env.
|
|
- optional adapter enablement env.
|
|
- security/CORS env.
|
|
- invalid env fail-fast 기준.
|
|
|
|
### 제외 범위
|
|
|
|
- secret manager 연동.
|
|
- Kubernetes/Helm chart 작성.
|
|
- 실제 production deployment 구성.
|
|
|
|
## TODO
|
|
|
|
> TODO drained 2026-05-22 — env prefix/naming, local/dev/staging/prod matrix, error exposure, logging/tracing, datasource/pool, outbound timeout/retry/circuit breaker, adapter enablement, invalid env fail-fast 모두 "결정 사항" / "판정 기준" / "Feature Flag / Reload Defaults" / "테스트 계약"에 반영됨. 잔존 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` 승급 위치 |
|
|
|
|
## 진행 중 메모
|
|
|
|
- env는 secret만이 아니라 운영 모드 전환 장치입니다.
|
|
|
|
## 결정 사항 (decisions)
|
|
|
|
- 2026-05-21: 운영 계약 전체를 env로 제어하는 방향.
|
|
- 2026-05-22: application-owned env는 `APP_` prefix를 사용.
|
|
- **2026-06-05 (확정)**: env naming SSOT = `env-keys.yaml` registry 의 `APP_*`. `APP_` **전면 통일**(datasource/server 등 Spring-native 매핑 키도 예외 없이 `APP_`). 현행 코드의 `DB_*`/`LOG_*`/`CORS_*`/`OIDC_*`/`SERVER_*` → `APP_*` rename 은 후속 코드 마이그레이션(§Audit `ENV_PREFIX_DRIFT`).
|
|
- 2026-05-22: local/dev/staging/prod matrix를 문서와 테스트 양쪽에 둠.
|
|
- 2026-05-22: prod profile에서 body logging과 internal error detail exposure는 기본 금지.
|
|
- 2026-05-22: feature flag 기본값은 env-startup flag. runtime/canary flag는 optional이며 registry row, owner, rollout/rollback rule 없이는 허용하지 않음.
|
|
- 2026-05-22: reload policy 기본값은 no runtime reload. secret/config reload가 필요하면 secrets branch와 startup validation test를 연결.
|
|
- 2026-05-22: 모든 env 바인딩은 `@ConfigurationProperties + @Validated` 강제. validation 미적용 bean 등록 시 fail.
|
|
- **2026-06-05 (확정)**: validation = **계층형**. 단순 제약(필수·범위·정규식)은 `@Validated`+JSR-303 선언 기본, JSR-303 로 표현 불가한 조건부/교차필드만 compact constructor 에서 처리하되 invalid 면 `throw`(fail-fast). lenient default 금지(현행 `CorsSettings` 음수 maxAge default 는 throw 로 수정 후속).
|
|
- **2026-06-06 (확정)**: env 조합 기반 fail-fast 집행 컴포넌트 = `SmartInitializingSingleton` validator bean(context refresh 완료 전 1회 검사 → invalid 시 `throw`) + contract test 이중. `EnvironmentPostProcessor`(bean presence 검사 불가)·`ApplicationReadyEvent`(늦음) 대비 선택. D8 multi-instance 5종 강제 + prod-unsafe toggle 모두 이 컴포넌트가 집행.
|
|
- 2026-05-22: Spring Duration unit 표기 = `30s` 1택. ISO-8601 `PT30S` 형식은 forbidden (가독성/일관성). `@DurationUnit`을 통한 정수만 받는 형식은 허용 (예: int 30 + @DurationUnit(SECONDS)). byte는 `DataSize` (`10MB`).
|
|
- 2026-05-22: boolean 표기 = `true/false` only (`1/0`/`on/off` forbidden).
|
|
- 2026-05-22: APP_PROFILE 우선순위 = SPRING_PROFILES_ACTIVE > APP_PROFILE (Spring native 표준 우선). 두 값 불일치 시 startup fail.
|
|
- **2026-06-06 (확정, 위 항목 대체)**: `APP_PROFILE` 도입 포기. profile = `SPRING_PROFILES_ACTIVE` **단독**(런타임 환경 선택자는 Spring native 영역). 우선순위/mismatch-fail 로직 미구현. `SPRING_PROFILES_ACTIVE` unset → startup fail 유지.
|
|
- 2026-05-22: .env.example drift 검증 도구 = custom Gradle task `verifyEnvExample` (registry의 env-registry 표 vs .env.example 비교). ci-quality-gates의 .env.example drift gate가 이를 실행.
|
|
- **2026-06-08 (확정, 위 항목 대체 — B)**: `.env.example` 두지 않음(`src/.env` git-tracked 단일 소스). drift 도구 = `verifyEnvKeys` (registry ↔ application.yml ↔ `.env` **3-way**). **registry(`env-keys.yaml`) = enforced SSOT**: check C 가 live 모든 `APP_` 키의 registry 행 존재를 build-time 강제. registry 를 as-built 55키와 전면 정렬(39행 추가 + `APP_LOG_LEVEL` 5분할 + `APP_SHUTDOWN_TIMEOUT`→`APP_SERVER_SHUTDOWN_TIMEOUT`).
|
|
- 2026-05-22: multi-instance claim parsing 메커니즘 = env property `APP_MULTI_INSTANCE_ENABLED` boolean (default false). true로 설정 시 다음이 모두 강제: (a) ShedLock/distributed lock bean 등록, (b) Redisson `RLock` based cache stampede protection, (c) outbox publisher leader election (SKIP LOCKED), (d) distributed rate limiter (Redis counter), (e) migration runner platform job. flag true인데 위 5종 contract test 1개라도 없으면 startup fail-fast. `feature-runtime-health-lifecycle-contract`, `feature-background-job-async-contract`, `feature-cache-consistency-contract`, `feature-domain-event-outbox-contract`, `feature-rate-limit-idempotency-contract`, `feature-migration-startup-contract`가 모두 본 flag를 consume. `APP_MULTI_INSTANCE_ENABLED` row를 `ca-tmpl/docs/registries/env-keys.yaml`에 추가 (Phase D1 후속, 또는 별도 PR).
|
|
|
|
## 근거 (필수, 최소 1개+)
|
|
|
|
> 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
|
|
|
|
| Source | 정당화하는 결정 |
|
|
|---|---|
|
|
| [[raw/official-docs/config-12-factor-app-config]] | 12-factor §III |
|
|
| [[raw/official-docs/config-spring-cloud-config-server-official]] | 중앙 git-backed + `@RefreshScope`; 인프라 SPOF + bootstrap 의존 |
|
|
| [[raw/official-docs/config-spring-cloud-kubernetes-configmap-reload]] | 3-level reload: `refresh`/`restart_context`/`shutdown`; partial-state 디버깅 + k8s lock-in |
|
|
| [[raw/official-docs/config-aws-appconfig-feature-flag-deployment]] | managed validator + CloudWatch auto-rollback; AWS lock-in + per-call billing |
|
|
| [[raw/company-tech-blogs/config-launchdarkly-feature-flag-best-practice]] | SaaS A/B/canary + user-targeting; 외부 의존 + flag debt + cost |
|
|
| [[raw/official-docs/config-spring-boot-externalized-configuration]] | D4 Duration/DataSize binding 포맷, D6 `SPRING_PROFILES_ACTIVE` relaxed binding, D10 `@ConfigurationProperties + @Validated` |
|
|
|
|
## 외부 근거 / 대안 조사 (2026-05-22 — Group G-I: Env-driven Runtime Configuration)
|
|
|
|
본 branch의 `APP_` prefix + Duration `30s` 1택 + boolean `true/false` only + no-runtime-reload + `.env.example` drift verify + `APP_MULTI_INSTANCE_ENABLED` claim parsing 결정에 대한 외부 source.
|
|
|
|
- **채택 결정 (12-factor config + Spring `@ConfigurationProperties` + `APP_` env-only)**:
|
|
- [[raw/official-docs/config-12-factor-app-config]] — 12-factor §III. Config (이론 출처). ca-tmpl `APP_` env-only + no-reload 결정의 표준 근거
|
|
- **검토한 대안**:
|
|
- **대안 1: Spring Cloud Config Server** — [[raw/official-docs/config-spring-cloud-config-server-official]] (중앙 git-backed + `@RefreshScope`; 인프라 SPOF + bootstrap 의존)
|
|
- **대안 2: k8s ConfigMap + Spring Cloud Kubernetes auto-reload** — [[raw/official-docs/config-spring-cloud-kubernetes-configmap-reload]] (3-level reload: `refresh`/`restart_context`/`shutdown`; partial-state 디버깅 + k8s lock-in)
|
|
- **대안 3: AWS AppConfig (feature flag + deployment strategy)** — [[raw/official-docs/config-aws-appconfig-feature-flag-deployment]] (managed validator + CloudWatch auto-rollback; AWS lock-in + per-call billing)
|
|
- **대안 4: LaunchDarkly / Unleash (feature flag service)** — [[raw/company-tech-blogs/config-launchdarkly-feature-flag-best-practice]] (SaaS A/B/canary + user-targeting; 외부 의존 + flag debt + cost)
|
|
- **비교 핵심**: 12-factor config가 ca-tmpl `APP_` env-only + no-runtime-reload 결정의 이론 출처. Spring Cloud Config Server는 인프라 SPOF + bootstrap 의존 부담. k8s ConfigMap reload는 partial-state 디버깅 어려움. LaunchDarkly/Unleash는 product-grade A/B/canary 요구 발생 시 진입점 — ca-tmpl이 의도적으로 위임한 영역 (50+ flag 또는 product team 운영 요구 시 도입 검토).
|
|
|
|
## 판정 기준
|
|
|
|
| 구분 | 기준 |
|
|
| --- | --- |
|
|
| Decision | 코드 수정 없이 env만으로 서버별 동작을 전환 |
|
|
| Allowed | Spring 런타임이 직접 읽는 native env(`SPRING_PROFILES_ACTIVE` 등)만 원래 이름 유지. **application-owned env 는 예외 없이 `APP_*`** (D2, 2026-06-05 확정 — datasource/server 등 Spring property 로 *매핑*되는 키도 operator-facing 이름은 `APP_*`) |
|
|
| Forbidden | profile별로 같은 의미의 env key 이름을 다르게 정의 |
|
|
| Required config | `APP_NAME`, error exposure, log, trace, datasource, outbound timeout/retry, adapter enablement, security/CORS. profile 은 Spring-native `SPRING_PROFILES_ACTIVE` 필수(unset 시 startup fail) — D6 확정으로 `APP_PROFILE` 미사용 |
|
|
| Failure condition | required env 누락, invalid enum/range, prod unsafe toggle이 startup에서 감지되지 않으면 실패 |
|
|
|
|
## Feature Flag / Reload Defaults
|
|
|
|
| item | default | allowed | forbidden |
|
|
| --- | --- | --- | --- |
|
|
| feature flag | startup env flag | runtime flag with registry owner | hidden code toggle |
|
|
| canary | out of core | platform rollout with runbook | undocumented partial rollout |
|
|
| config reload | no runtime reload | secret manager reload with validation | silent changed behavior |
|
|
| flag registry | env registry row required | external flag system mapping | unregistered flag |
|
|
|
|
## 테스트 계약
|
|
|
|
- required env 누락 시 startup fail-fast.
|
|
- prod profile에서 body logging enabled면 실패.
|
|
- prod profile에서 internal error detail exposure enabled면 실패.
|
|
- disabled adapter가 bean/use case path에서 사용되면 실패.
|
|
- `.env`/application.yml/registry 3-way 불일치(필수 env 누락, orphan, 미등록 `APP_` 키) 시 `verifyEnvKeys` build 실패 (registry SSOT, check C).
|
|
- feature flag registry owner 강제: 모든 runtime/canary flag(`@FeatureFlag` annotation 또는 `APP_FEATURE_*` env)는 `env-keys.yaml`에 row가 존재하고 `owner_branch` field가 비어 있지 않아야 함. 측정 방법: bean에서 `@Value("${app.feature.*}")` 또는 `@FeatureFlag` 사용 시 해당 key가 yaml에 row로 존재 verify. 미존재 또는 owner 누락 시 fail.
|
|
|
|
## 결정-근거 매핑
|
|
|
|
> 본 branch 의 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다. Decision ID 는 안정적으로 유지한다. company-tech-blog 출처는 `company-case-study` 로 표기하며 공식 best practice 로 일반화하지 않는다.
|
|
|
|
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
|
|---|---|---|---|---|---|
|
|
| D1 | 운영 계약 전체를 env 로 제어 (코드 수정 없이 서버별 동작 전환) | N/A — 운영 계약 전체를 env 로 제어하는 1택. 대안(코드 하드코딩 / profile 별 분기 코드)은 12-factor §III 가 거부 | `raw/official-docs/config-12-factor-app-config.md#TWELVE-FACTOR-CONFIG-C1`, `raw/official-docs/config-12-factor-app-config.md#TWELVE-FACTOR-CONFIG-C2` | `official-reference` (12-factor manifesto, not formal standard) | 12-factor 본문은 prefix grouping 을 권장하지 않음 — `APP_` 그룹화 정당성은 별도 |
|
|
| D2 | `APP_` prefix 전면 통일 (application-owned env). **SSOT = `env-keys.yaml` registry** (2026-06-05 사용자 결정) | N/A — `APP_` 전면 통일 1택. prefix 없거나 다른 prefix 면 외부 의존 env(`SPRING_*`/`JAVA_OPTS`)와 시각 구분 불가. datasource/server 등 Spring-native 매핑 키도 일관성 위해 `APP_` 통일(Spring 표준명 예외 두지 않음) | `team-decision` (2026-06-05) — prefix 규약은 어떤 official source 도 명시 안 함(12-factor `TWELVE-FACTOR-CONFIG-C5` 는 "granular orthogonal controls" 만 언급). 일관성·시각 구분 위한 팀 결정 | `team-decision` (no external source) | 현행 코드(`application.yml`)는 `DB_*`/`LOG_*`/`CORS_*`/`OIDC_*`/`SERVER_*` 사용 → **`APP_*` 로 rename 하는 코드 마이그레이션이 후속 작업**(§Audit `ENV_PREFIX_DRIFT` RESOLVED). registry 가 ground-truth, 코드가 따라옴 |
|
|
| D3 | no runtime reload (Spring Cloud Config Server / k8s ConfigMap auto-reload / AppConfig 거부) | 기본 no-reload. runtime reload 는 secret manager reload + startup validation test 가 연결될 때만 허용(secrets branch). 그 외 config 변경은 재배포로만 | `raw/official-docs/config-spring-cloud-config-server-official.md#SCC-SERVER-C1`, `raw/official-docs/config-spring-cloud-kubernetes-configmap-reload.md#SCK-RELOAD-C1`, `raw/official-docs/config-aws-appconfig-feature-flag-deployment.md#AWS-APPCONFIG-C1` (대안 capability 만 인용 — 본 결정은 대안의 trade-off 거부) | `official-vendor-doc` (대안 capability 근거) | 대안의 capability 인용은 "거부 이유" 의 사실 기반일 뿐 "no runtime reload 가 best practice" 의 증거는 아님 |
|
|
| D4 | Duration unit = `30s` 1택, ISO-8601 `PT30S` forbidden | N/A — 가독성 1택. Spring Binder 가 `30s`/`PT30S`/`30` 모두 허용하므로 기술 분기가 아닌 팀 규약 | `raw/official-docs/config-spring-boot-externalized-configuration.md#SPRING-EXTCONFIG-C1` (Spring Boot 가 `30s` / `PT30S` / `30` 세 형식 모두 허용함을 확인), `raw/official-docs/config-spring-boot-externalized-configuration.md#SPRING-EXTCONFIG-C3` (DataSize `10MB` suffix 허용 확인) — **형식 선택** 자체는 팀 가독성 규약 (`UNSUPPORTED_IMPL_DECISION`): Spring 공식 근거는 "두 형식이 동등하다"는 기계적 가능성만 지지하며 `30s` 가 더 권장된다는 증거는 없음 | `official-vendor-doc` (포맷 허용 범위) | Spring Boot 가 양쪽 모두 허용하므로 `30s` 1택 규약 자체는 팀 결정 — 기계적으로는 `PT30S` 도 동작함 |
|
|
| D5 | boolean = `true/false` only (`1/0`, `on/off` forbidden) | N/A — 일관성 1택. Spring Binder 가 `1/0`·`on/off` 도 허용하나 contract 수준 1택 | UNSUPPORTED_DECISION — 일관성 운영 결정. 외부 official 근거 없음 | none | branch 자체 정합성 규칙 |
|
|
| D6 | profile = `SPRING_PROFILES_ACTIVE` **단독** (2026-06-06 확정: `APP_PROFILE` 도입 포기) | N/A — profile 은 application-owned config 값이 아니라 **런타임 환경 선택자**(Spring native 영역)이므로 `SPRING_PROFILES_ACTIVE` 단독. `APP_PROFILE` 별도 도입은 정보 이중화 + mismatch fail 비용만 추가 → 포기. `SPRING_PROFILES_ACTIVE` unset 시 startup fail(default profile 미부여)로 환경 명시 강제 | `raw/official-docs/config-spring-boot-externalized-configuration.md#SPRING-EXTCONFIG-C4` (relaxed binding: `spring.profiles.active` → `SPRING_PROFILES_ACTIVE`) + `team-decision` (단독 채택) | `official-vendor-doc` (relaxed binding 메커니즘) + `team-decision` | profile selector 는 D2 `APP_` 통일의 예외(Spring 런타임이 직접 읽는 native env). 향후 product 요구로 앱이 profile 을 자체 노출/검증해야 하면 그때 `APP_PROFILE` 재검토 |
|
|
| D7 | env drift = custom Gradle task `verifyEnvKeys` (registry ↔ application.yml ↔ `.env` 3-way lock-step). B 확정(2026-06-08): **registry = SSOT** (check C), `.env.example` 미사용 | N/A — drift 검증 도구 1택. 대안(수동 리뷰/외부 lint)은 CI 자동 강제 불가 | UNSUPPORTED_DECISION — 도구 선택 운영 결정 | none | 외부 official 근거 없음. registry 미등록 키는 build fail(check C) |
|
|
| D8 | `APP_MULTI_INSTANCE_ENABLED` flag = multi-instance contract 5종 강제 + fail-fast. **집행 = `SmartInitializingSingleton` validator bean + contract test 이중** (2026-06-06) | `false`(default)면 single-instance 허용. `true` 면 5종 contract(lock/stampede/leader/rate-limit/migration) bean presence 를 `SmartInitializingSingleton` 이 `getBeanProvider` 로 검사 → 1개라도 없으면 `throw`(startup 중단) | UNSUPPORTED_DECISION — flag 자체는 branch 정합성(외부 근거 없음). 집행 메커니즘은 `team-decision` + `UNSUPPORTED_IMPL_DECISION` (아래 trade-off) | none (flag) / `team-decision` (집행) | trade-off: `EnvironmentPostProcessor` 는 bean 정의 이전이라 presence 검사 불가 → 부적합. `SmartInitializingSingleton`(refresh 완료 전, 모든 singleton 초기화 직후)이 `ApplicationReadyEvent`(트래픽 직전)보다 이르게 fail. contract test 는 CI 회귀 방지 이중 |
|
|
| D9 | feature flag 기본값 = env-startup flag, runtime/canary flag = registry row + owner 필수 | 기본 env-startup flag. runtime/canary flag 가 필요할 때만 registry row + `owner_branch` + rollout/rollback rule 필수(없으면 불허) | `raw/official-docs/config-aws-appconfig-feature-flag-deployment.md#AWS-APPCONFIG-C2` (operational flag use case), `raw/official-docs/config-aws-appconfig-feature-flag-deployment.md#AWS-APPCONFIG-C5` (auto-rollback 보완 기능 비교 baseline), `raw/company-tech-blogs/config-launchdarkly-feature-flag-best-practice.md#LD-FF-C1` ~ `LD-FF-C5` | `official-vendor-doc` (AppConfig 비교 baseline) + `company-case-study` (LaunchDarkly — 일반화 금지) | LaunchDarkly 는 SaaS 사례. AppConfig capability 인용은 "ca-tmpl 이 비싼 대안을 도입하지 않는 이유" 의 비교 근거일 뿐 |
|
|
| D10 | **계층형 validation** (2026-06-05 사용자 결정): ① 단순 제약(필수·범위·정규식) = `@Validated` + JSR-303 선언 **기본**, ② JSR-303 로 표현 불가한 조건부/교차필드만 compact constructor 에서 처리 — 단 invalid 면 **`throw`(fail-fast)**, lenient default 금지 | 제약 종류로 분기: 단순 제약이면 `@Validated`+JSR-303(선언적, startup 자동 fail). 조건부/cross-field(예: `enabled=true` 일 때만 origins 필수)면 constructor 에서 throw. 정상 default(예: `enabled=false` 시 빈 origins)는 invalid 아님 → default 허용 | `raw/official-docs/config-spring-boot-externalized-configuration.md#SPRING-EXTCONFIG-C5` (Spring Boot 가 `@Validated` 를 인식해 JSR-303 `jakarta.validation` 제약을 자동 실행함을 공식 확인) + `team-decision` (계층 분리 + no-lenient 규약) | `official-vendor-doc` (`@Validated` 메커니즘) + `team-decision` (계층 분리 규약) | 현행 `CorsSettings` 는 `@Validated` 없이 constructor + 음수 maxAge lenient default → **본 결정에 맞게 (a) 단순 제약은 `@Validated` 로, (b) 음수 maxAge 는 throw 로 코드 수정 후속**(§Audit `VALIDATION_POLICY_DRIFT`/`INVALID_RANGE_LENIENT` RESOLVED) |
|
|
|
|
## 구현 가이드
|
|
|
|
> ✅ **naming SSOT 확정(2026-06-05)**: env 변수 naming = `env-keys.yaml` registry 의 `APP_*` 전면 통일(D2). 본 §의 anchor 인 ca-tmpl 실제 코드(`application.yml`)는 현재 `DB_*`/`LOG_*`/`CORS_*`/`OIDC_*`/`SERVER_*` 를 쓰므로 **`APP_*` 로 rename 하는 코드 마이그레이션이 본 branch 구현의 일부**다. 아래 표의 "현행 env" 컬럼은 마이그레이션 *대상*(before), 목표는 `APP_*`(after).
|
|
|
|
### 1. env → property → Settings 3층 바인딩 구조 (actually-implemented)
|
|
|
|
> **Trace**: D1(env 전체 제어)·D2(prefix)·D10(`@ConfigurationProperties`) / `SPRING-EXTCONFIG-C5`. anchor = `src/app-bootstrap/src/main/resources/application.yml` L2 주석 "Mirrors src/.env … input validation lives in the *Settings records".
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: `*Settings` record 명명 + `<module>/settings/` 패키지 위치 — 어떤 external source 도 규정 안 함. trade-off: 기존 ca-tmpl 컨벤션 답습(이미 5개 클래스가 따름) → 일관성 우선.
|
|
|
|
| Layer | 위치 | 역할 | 상태 |
|
|
|---|---|---|---|
|
|
| A. operator env | `src/.env` (git-tracked 단일 소스, placeholder 소비) | 운영자가 세팅하는 실제 env 변수 | `actually-implemented` (`.env.example` 미사용 — RESOLVED) |
|
|
| B. `${ENV}` 브리지 | `application.yml` | env → Spring property 매핑. Spring-native(`spring.*`/`server.*`/`logging.*`) 또는 custom `ca-skeleton.*` 로 분기 | `actually-implemented` |
|
|
| C. `*Settings` record | `<module>/settings/<Domain>Settings.java`, `@ConfigurationProperties(prefix="ca-skeleton.<group>")` | 타입 바인딩 + allowed-value 검증의 집(home) | `actually-implemented` (5종, 아래) |
|
|
|
|
현존 `*Settings` (코드 grep 확인): `bootstrap/settings/BootstrapSettings`(`@Validated`), `bootstrap/settings/LoggingSettings`, `adapter-web/settings/PresentationSettings`, `adapter-web/settings/SecuritySettings`, `adapter-web/settings/CorsSettings`. Spring property prefix 는 `app.*` 가 아니라 **`ca-skeleton.*`** 다.
|
|
|
|
### 2. fail-fast 메커니즘 (혼합 — 통일 안 됨)
|
|
|
|
> **Trace**: D10 / `SPRING-EXTCONFIG-C5` + §테스트 계약. anchor = `BootstrapSettings.java`, `CorsSettings.java`.
|
|
>
|
|
> - **집행 컴포넌트 확정(2026-06-06, D8)**: env 조합 기반 fail-fast(prod-unsafe toggle, multi-instance 5종)는 `SmartInitializingSingleton` validator bean 이 context refresh 완료 전 1회 검사 → invalid 면 `throw`. (`EnvironmentPostProcessor` 는 bean presence 검사 불가라 부적합, `ApplicationReadyEvent` 는 늦음). contract test 로 회귀 방지 이중.
|
|
|
|
| 검증 스타일 | 메커니즘 | 예시 | 상태 |
|
|
|---|---|---|---|
|
|
| 필수-무default 필드 | `@Validated` + `@NotBlank`/`@NotNull` → 누락/blank 시 `BindValidationException` startup fail | `BootstrapSettings.appName` | `actually-implemented` |
|
|
| 단순 제약(필수·범위·정규식) | `@Validated` + JSR-303(`@NotBlank`/`@Min`/`@Positive` 등) → startup 자동 fail-fast | 신규 작성 기준(D10 ①). `BootstrapSettings` 가 선례 | `planned`(`CorsSettings.maxAge` 등에 적용 후속) |
|
|
| 조건부/교차필드 | compact constructor 에서 검사 후 invalid 면 `throw`(fail-fast, lenient 금지) | `CorsSettings`(`enabled=true`+empty origins). 단 음수 maxAge 는 현행 lenient default → **`throw` 로 수정 후속** | `actually-implemented`(스타일) / lenient 부분은 `planned` 수정 |
|
|
| prod-unsafe / multi-instance fail | env 조합(`APP_LOG_BODY*`+prod, 또는 `APP_MULTI_INSTANCE_ENABLED=true`+5종 bean) 위반 시 `SmartInitializingSingleton` validator 가 `throw` | `ProdProfileSafetyTest` + multi-instance contract test (미존재) | `planned` (집행 컴포넌트는 확정, 코드 미작성) |
|
|
|
|
> ✅ **정책 확정(2026-06-05, D10)**: 단순 제약 = `@Validated`+JSR-303, 조건부/교차필드 = constructor + `throw`(lenient 금지). 따라서 신규 `*Settings` 작성 기준이 명확하다. 현행 `CorsSettings` 는 (a) 단순 제약을 `@Validated` 로 끌어올리고 (b) 음수 maxAge lenient default 를 `throw` 로 바꾸는 코드 수정이 후속(`VALIDATION_POLICY_DRIFT`/`INVALID_RANGE_LENIENT` RESOLVED — §Audit).
|
|
|
|
### 3. profile 해석 (actually-implemented, 단 단일화)
|
|
|
|
> **Trace**: D6 / `SPRING-EXTCONFIG-C4`. anchor = `application.yml` L16-18 `spring.profiles.active: ${SPRING_PROFILES_ACTIVE}`.
|
|
|
|
현행 코드는 `SPRING_PROFILES_ACTIVE` **단독** 사용 — **D6 확정(2026-06-06)과 정합**. `APP_PROFILE` 은 도입하지 않으므로 우선순위/mismatch-fail 로직은 구현 대상 아님. `application.yml` L18 `spring.profiles.active: ${SPRING_PROFILES_ACTIVE}` 가 SSOT이며, unset 시 placeholder 미해소로 startup fail(default profile 미부여) — `actually-implemented`.
|
|
|
|
### 4. env drift 검증 — `verifyEnvKeys` 3-way lock-step (`actually-implemented`)
|
|
|
|
> **Trace**: D7. anchor = `src/build.gradle` `verifyEnvKeys` task + `docs/registries/env-keys.yaml`.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: gate 형태(custom Gradle task)는 도구 선택 운영 결정(D7 자체 UNSUPPORTED). trade-off: registry ↔ application.yml ↔ `.env` 3-way 를 CI 에서 자동 강제.
|
|
|
|
**B 확정(2026-06-08): registry = enforced SSOT.** `.env.example` 은 두지 않음(`src/.env` 가 git-tracked 단일 소스 → redacted 사본 중복). `verifyEnvKeys` 게이트 3-check: (A) application.yml 의 required placeholder(inline default 없는 `${VAR}`) ⊆ `.env`, (B) `.env` orphan 0, **(C) `.env` 의 모든 `APP_` 키 ∈ `env-keys.yaml`(registry SSOT 강제). `SPRING_*` native 는 미추적.** `check` 에 `dependsOn`. 게이트 통과: `verifyEnvKeys: OK — 68 env keys, 64 required placeholders covered, 55 APP_ keys registered`.
|
|
|
|
### 5. 코드 마이그레이션 체크리스트 (본 branch 결정의 ca-tmpl 코드 반영)
|
|
|
|
> 본 branch 의 확정 결정이 만드는 실제 코드 작업. 모두 ground-truth 대조로 도출됨(§Audit).
|
|
|
|
| # | 작업 | 근거 결정 | 파일 |
|
|
|---|---|---|---|
|
|
| M1 | env 변수 `DB_*`/`LOG_*`/`CORS_*`/`OIDC_*`/`SERVER_*` → `APP_*` rename (registry `env-keys.yaml` 이름에 정렬). `SPRING_PROFILES_ACTIVE` 등 Spring native 는 유지 | D2 | `application.yml`, `src/.env` |
|
|
| M2 | `application.yml` L147 `ca-skeleton.cmd.app-name` → `ca-skeleton.bootstrap.app-name` (latent bug fix) | Advisory | `application.yml` |
|
|
| M3 | `CorsSettings`: 단순 제약을 `@Validated`+JSR-303 로, 음수 maxAge lenient default → `throw` | D10 | `CorsSettings.java` |
|
|
| M4 | `SmartInitializingSingleton` validator bean 작성: prod-unsafe + `APP_MULTI_INSTANCE_ENABLED` 5종 bean presence 검사 → `throw` | D8 | `app-bootstrap` (신규) |
|
|
| M5 | `verifyEnvKeys` Gradle task: registry ↔ application.yml ↔ `.env` 3-way lock-step (check C = registry SSOT 강제). `.env.example` 미사용 | D7 | `build.gradle`, `env-keys.yaml` |
|
|
|
|
> **OUT_OF_BRANCH_SCOPE**: adapter on/off 3-layer(`@ConditionalOnProperty` + ArchUnit static + `AdapterDisabledException`)는 governing doc §29 G-I 영역이지만 owner 는 [[raw/branch-notes/feature-integration-adapter-templates]] — 본 §에 명세 남기지 않음(§Coverage 위임 행 참조).
|
|
|
|
## 구현 완료 기록 (2026-06-06 1차 + 2026-06-08 B) — M1~M5 `actually-implemented`
|
|
|
|
> ca-tmpl `src/` 실 코드에 M1~M5 전부 반영. `./gradlew check` (전 모듈 test + ArchUnit `CleanArchitectureTest` + `verifyCleanArchitectureDependencies` + `verifyEnvKeys`) **BUILD SUCCESSFUL**. 리뷰 체인 ca-architect-sentinel / ca-spec-reviewer / ca-quality-reviewer **모두 PASS**.
|
|
> **2026-06-08 B 후속**: registry = enforced SSOT 로 전환 — `env-keys.yaml` as-built 55키 전면 정렬(39행 추가 + 2 이름충돌 해소) + `verifyEnvKeys` check C 추가. 독립 검증: `verifyEnvKeys` BUILD SUCCESSFUL(`55 APP_ keys registered`), `:app-bootstrap:test`·`:adapter-web:test` BUILD SUCCESSFUL, live `APP_` 키 missing 0.
|
|
|
|
| # | 작업 | 상태 | 핵심 구현 사실 |
|
|
|---|---|---|---|
|
|
| M1 | env `DB_*`/`LOG_*`/`CORS_*`/`OIDC_*`/`SERVER_*` → `APP_*` | `actually-implemented` | `src/.env` + `application.yml` placeholder 전면 rename. **scope = audit `ENV_PREFIX_DRIFT` 의 5 prefix 정확히** (PRESENTATION_API_BASE_PATH·SECURITY_PUBLIC_PATHS 는 목록 외라 유지). `SERVER_*`→`APP_SERVER_*`(D2 전면통일). 매핑: DB_→APP_DATASOURCE_, LOG_→APP_LOG_, CORS_→APP_SECURITY_CORS_(ORIGINS/ALLOW_CREDENTIALS/MAX_AGE 는 registry 명), OIDC_→APP_SECURITY_JWT_. 정직성 위해 `SecuritySettings`/`LoggingSettings` 로그 문자열 + 매칭 test 단언도 갱신. SPRING_*·SPRING_PROFILES_ACTIVE native 유지. |
|
|
| M2 | `ca-skeleton.cmd.app-name` → `ca-skeleton.bootstrap.app-name` | `actually-implemented` | `application.yml` L147 + `application-test.yml` 둘 다 수정. latent bug(클래스는 `ca-skeleton.bootstrap` 바인딩)는 full-context 기동에서만 발현했던 것 — `@WebMvcTest` slice 라 기존 test 는 통과했었음. |
|
|
| M3 | `CorsSettings` 계층형 validation | `actually-implemented` / `locally-verified` | `@Validated` + `@PositiveOrZero`(maxAge<0 → `BindValidationException` startup fail). cross-field(`enabled=true`+empty origins)는 compact constructor `throw`(D10 prose 예시, 기존 warn+fail-closed 대체). logger 제거. `CorsSettingsTest` 4 메서드 재작성(`ValidationAutoConfiguration` 주입). |
|
|
| M4 | `SmartInitializingSingleton` startup 가드 + 플래그 도입 | `actually-implemented` / `locally-verified` | 신규 `StartupSafetyValidator`(`bootstrap.runtime`) + `RuntimeSafetyConfig`(@Bean wiring) + `RuntimeSafetySettings`(`@ConfigurationProperties("ca-skeleton.runtime")`). prod profile + (`APP_ERROR_DETAIL_EXPOSURE_ENABLED`\|`APP_LOG_BODY_CAPTURE_ENABLED`)=true → `throw`. `APP_MULTI_INSTANCE_ENABLED`=true + 5종 coordination bean(name 기반 presence) 누락 → `throw`. 세 플래그를 `.env`/`application.yml`/`application-test.yml` 에 신규 wiring. `StartupSafetyValidatorTest` 8 메서드. profile 매칭은 의도적 case-insensitive(prod 오타 가드). |
|
|
| M5 | env drift Gradle task `verifyEnvKeys` | `actually-implemented` / `locally-verified` | **B 확정(2026-06-08): registry = enforced SSOT.** `.env.example` 미사용(`src/.env` 가 git-tracked 단일 소스). 게이트 3-check: (A) application.yml required placeholder ⊆ `.env`, (B) `.env` orphan 0, **(C) `.env` 의 모든 `APP_` 키 ∈ `env-keys.yaml`** (registry 미등록 키는 build fail; `SPRING_*` 미추적). `check` 에 `dependsOn`. **`verifyEnvKeys: OK — 68 env keys, 64 required placeholders covered, 55 APP_ keys registered`**. (초기 2026-06-06 설계는 surface-only A/B 였으나 2026-06-08 B 결정으로 check C + registry 전면 정렬 추가 — 아래 결정 노트.) [[raw/blog-topics/env-example-drift-gate-gradle-2026-06-06]] |
|
|
|
|
**registry(`docs/registries/env-keys.yaml`) 정렬 — 2026-06-06 1차 + 2026-06-08 B 완성**:
|
|
- 1차(2026-06-06): `APP_PROFILE` row 제거(D6 폐기), `SERVER_PORT`→`APP_SERVER_PORT`(D2), `APP_MULTI_INSTANCE_ENABLED` 추가(D8), 헤더 convention/Last-updated 갱신.
|
|
- **B(2026-06-08): registry 를 as-built 55 `APP_` 키와 전면 정렬.** 누락 39행 추가(datasource extras 7 → env-driven, server 12 → env-driven, log granular 17 → log-management, CORS 3 → security). 이름 충돌 해소: `APP_LOG_LEVEL` 단일 → `APP_LOG_LEVEL_{ROOT,APP,SPRING,WEB,SQL}` 5분할(code 이름 채택), `APP_SHUTDOWN_TIMEOUT`(container-runtime) → `APP_SERVER_SHUTDOWN_TIMEOUT`(env-driven, termination-grace 정렬은 container-runtime cross-ref 주석 보존). 독립 검증: live `APP_` 55키 전부 registry 존재(missing 0).
|
|
|
|
> **결정 노트(2026-06-08, B = registry SSOT)**: 초기 2026-06-06 구현은 "drift 정답 소스 = application.yml surface, registry 1:1 강제 불가"로 갔으나(check A/B only), 사용자가 **B(registry = enforced SSOT)** 선택. 따라서 ① registry 를 as-built 와 전면 정렬, ② `verifyEnvKeys` 에 check C(모든 live `APP_` 키 ∈ registry) 추가, ③ `build.gradle` 주석을 "registry SSOT lock-step"으로 갱신. cross-branch 이름/owner 2건은 사용자 결정(이름=code 채택, `APP_SERVER_*` owner=env-driven). M1 의 SERVER_* rename 은 D2 전면통일 우선(registry 2026-05-22 주석/governing §9 의 "SERVER_* native"는 stale).
|
|
|
|
## 엣지·실패·의존
|
|
|
|
> R4 캡처용. 정상 경로 외 실패/엣지 + 다른 계약 의존.
|
|
|
|
- **실패·엣지 경로**:
|
|
- `APP_NAME` 누락/blank → `BindValidationException`, context refuses to start (`actually-implemented`, `BootstrapSettings`).
|
|
- `CORS_ENABLED=true` + `CORS_ALLOWED_ORIGINS` empty → `log.warn` + 모든 브라우저 호출 reject(fail-open 아님, fail-closed). `actually-implemented`(`CorsSettings`).
|
|
- invalid range(음수 `APP_SECURITY_CORS_MAX_AGE`) → **D10 확정에 따라 `throw`(fail-fast)**. 현행 코드의 lenient default(3600)+warn 는 throw 로 수정 후속.
|
|
- prod profile + body logging / internal error detail exposure ON → fail 기대이나 enforcing test 부재(`planned`).
|
|
- `SPRING_PROFILES_ACTIVE` unset → `${SPRING_PROFILES_ACTIVE}` placeholder 미해소 → startup fail(default profile 없음). 엣지: 의도적 default 미부여인지 확인 필요.
|
|
- **다른 계약 의존** (env 값 semantics 위임 — 본 branch 는 *env→Settings 바인딩·검증 계약*을 소유, 값 정책은 owner branch):
|
|
- [[raw/branch-notes/feature-secrets-config-source-contract]] — `DB_PASSWORD`/JWT signing key 등 secret-classified env (registry `owner_branch` 확인). 이 계약이 secret 해소 방식을 바꾸면 본 branch 의 바인딩 layer 영향.
|
|
- [[raw/branch-notes/feature-log-management-contract]] — `LOG_*`(level/file/async/json) → `LoggingSettings`. 본 branch 는 바인딩, 로그 semantics 는 위임.
|
|
- [[raw/branch-notes/feature-security-operational-baseline]] — `CORS_*`/`OIDC_*` → `CorsSettings`/`SecuritySettings`.
|
|
- [[raw/branch-notes/feature-outbound-http-client-baseline]] — outbound timeout/retry/CB env (registry `APP_OUTBOUND_*`; 단 코드 미존재 `planned`).
|
|
- [[raw/branch-notes/feature-distributed-tracing-contract]] — tracing enable/sample-rate env.
|
|
- [[raw/branch-notes/feature-cache-consistency-contract]] — cache redis env(`APP_CACHE_REDIS_*`/`APP_CACHE_*_TTL`) → 값 semantics 위임(registry `owner_branch`).
|
|
- [[raw/branch-notes/feature-integration-adapter-templates]] — adapter on/off `@ConditionalOnProperty`(OUT_OF_SCOPE here).
|
|
- **D8 multi-instance**: `APP_MULTI_INSTANCE_ENABLED` 를 `feature-runtime-health-lifecycle-contract`·`feature-background-job-async-contract`·`feature-cache-consistency-contract`·`feature-domain-event-outbox-contract`·`feature-rate-limit-idempotency-contract`·`feature-migration-startup-contract` 6개가 consume. 본 flag 의미 변경 시 6개 모두 영향.
|
|
|
|
## 검증해야 할 주장
|
|
|
|
| Claim | Why uncertain | How to verify | Status |
|
|
|---|---|---|---|
|
|
| ca-tmpl `APP_` prefix 가 12-factor "granular orthogonal controls" 와 양립 | 12-factor 는 grouping 을 권장하지 않음 — prefix grouping 이 orthogonality 를 약화시키는지 불확실 | env-keys.yaml registry 에 각 key 의 orthogonality 명시 + ArchUnit/registry-scan 으로 cross-coupling 탐지 | `needs-confirmation` |
|
|
| ~~`.env.example` drift verifier 가 registry 와 100% 일치 보장~~ → `verifyEnvKeys` 가 registry↔application.yml↔`.env` 100% 일치 강제 | (해소) | `verifyEnvKeys` check C 가 live `APP_` 키 ⊆ registry 강제 + 독립 검증 missing 0 | `actually-implemented` (B, 2026-06-08) |
|
|
| `APP_MULTI_INSTANCE_ENABLED=true` 시 5종 contract test 가 모두 fail-fast 동작 | 5종 contract test 가 아직 작성되지 않음 | feature-runtime-health-lifecycle / feature-cache-consistency 등 5 branch 의 contract test 작성 후 통합 검증 | `planned` |
|
|
| ~~`SPRING_PROFILES_ACTIVE` 와 `APP_PROFILE` 불일치 시 startup fail~~ | — | — | `wont-fix` (2026-06-06: `APP_PROFILE` 도입 포기, D6) |
|
|
| prod profile 에서 body logging / internal error detail exposure enabled 시 startup fail | 구현 미확인 | `ProdProfileSafetyTest` contract test 구현 — `SPRING_PROFILES_ACTIVE=prod` + `APP_LOG_BODY_CAPTURE_ENABLED=true` 조합에서 `SmartInitializingSingleton` validator 가 startup fail 시키는지 verify | `planned` |
|
|
| feature flag registry owner 강제 | env-keys.yaml registry schema 미확정 | env-keys.yaml schema 에 `owner_branch` field 추가 + `@FeatureFlag` annotation processor 가 yaml 와 cross-check | `planned` |
|
|
| AppConfig / LaunchDarkly 채택 trigger (50+ flag 또는 product team 운영) | branch 가 의도적으로 위임한 영역 | flag 수가 50 초과하거나 A/B canary 요구가 발생할 때 별도 검토 trigger | `needs-confirmation` |
|
|
|
|
## 관심사 커버리지
|
|
|
|
> 기준: `governing_docs = wiki/projects/ca-tmpl/config-and-adapter-templates.md` (canonical §9 Env config + §29 G-I Adapter). 상태: `covered-here` / `delegated` / `missing`. 기준 SSOT: `rules/coverage-gate.md`.
|
|
|
|
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
|
|--------|------|-------|--------|------|
|
|
| env prefix / naming 계약 | covered-here | — | OK | D2 — `APP_*` 통일 확정(2026-06-05). 코드 rename 후속 작업 |
|
|
| Duration `30s` 포맷 | covered-here | — | OK | D4 / `SPRING-EXTCONFIG-C1,C3` |
|
|
| boolean `true/false` only | covered-here | — | OK | D5 |
|
|
| no-runtime-reload | covered-here | — | OK | D3 |
|
|
| env drift 검증 | covered-here | — | OK | D7 — `verifyEnvKeys` 3-way(registry SSOT, check C) `actually-implemented` (B, 2026-06-08) |
|
|
| `@ConfigurationProperties + @Validated` | covered-here | — | OK | D10 — 계층형 validation 확정(2026-06-05). CorsSettings 코드 수정 후속 |
|
|
| profile 해석/matrix | covered-here | — | OK | D6 — `SPRING_PROFILES_ACTIVE` 단독 확정(2026-06-06) |
|
|
| error detail exposure toggle | covered-here | — | OK | §테스트 계약 (registry `APP_ERROR_DETAIL_EXPOSURE_ENABLED`; 코드 `SERVER_ERROR_INCLUDE_*`) |
|
|
| body logging toggle | covered-here | — | OK | §테스트 계약 (registry `APP_LOG_BODY_CAPTURE_ENABLED`) |
|
|
| datasource / pool env | covered-here | — | OK | registry `APP_DATASOURCE_*` / 코드 `DB_*` (§1 표) |
|
|
| required env fail-fast | covered-here | — | OK | D10 / `BootstrapSettings` |
|
|
| adapter on/off — Layer1 `@ConditionalOnProperty` | delegated | [[raw/branch-notes/feature-integration-adapter-templates]] | OK | governing §29 G-I; §구현 가이드 OUT_OF_SCOPE 주석 |
|
|
| adapter on/off — Layer2 ArchUnit static | delegated | [[raw/branch-notes/feature-integration-adapter-templates]] | OK | governing §29 G-I |
|
|
| adapter on/off — Layer3 `AdapterDisabledException` | delegated | [[raw/branch-notes/feature-integration-adapter-templates]] | OK | governing §29 G-I |
|
|
| outbound timeout/retry/CB env 값 | delegated | [[raw/branch-notes/feature-outbound-http-client-baseline]] | OK | registry `owner_branch` |
|
|
| tracing enable/sample-rate env 값 | delegated | [[raw/branch-notes/feature-distributed-tracing-contract]] | OK | registry `owner_branch` |
|
|
| log level/sampling/file env 값 | delegated | [[raw/branch-notes/feature-log-management-contract]] | OK | registry `owner_branch` |
|
|
| security/CORS/JWT env 값 | delegated | [[raw/branch-notes/feature-security-operational-baseline]] | OK | registry `owner_branch` |
|
|
| secret-classified env (DB_PASSWORD, JWT key) | delegated | [[raw/branch-notes/feature-secrets-config-source-contract]] | OK | registry `owner_branch` |
|
|
| cache redis env 값 | delegated | [[raw/branch-notes/feature-cache-consistency-contract]] | OK | registry `owner_branch` |
|
|
|
|
**missing: 0** — governing doc 의 모든 관심사가 owner 보유. env naming(D2)·validation(D10)·profile(D6)·D8 집행·prefix bug·env drift(D7) 전부 RESOLVED + `actually-implemented`. 잔여 🟡 0건. Blocking 아님.
|
|
|
|
## Audit & Findings (2026-06-05 — /branch-spec ca-tmpl ground-truth 대조)
|
|
|
|
> ca-tmpl `src/` + `docs/registries/` 를 읽기 전용으로 대조해 발견한 drift. **사용자 작성 결정 영역이므로 자동 rewrite 하지 않고 정합 권고만 남긴다**(CLAUDE.md §11, branch-spec §2). 해소는 `/branch-spec` 재실행 또는 사용자 결정.
|
|
|
|
| 라벨 | 내용 | 증거 | 권고 (사용자 결정) |
|
|
|---|---|---|---|
|
|
| `ENV_PREFIX_DRIFT` ✅ RESOLVED (2026-06-05) | env 변수 naming 이 **3-way** 불일치였음: 노트 D2 / `env-keys.yaml`(`APP_*`) / 코드 `application.yml`(`DB_*`·`LOG_*`·`CORS_*`·`OIDC_*`·`SERVER_*`) | registry 에 `DB_URL` 등 0건, application.yml 에 `APP_DATASOURCE` 등 0건 (grep) | **결정: `APP_*` 전면 통일, SSOT = registry**(D2). 코드(`application.yml`+`src/.env`)를 `APP_*` 로 rename 하는 것이 본 branch 구현 작업의 일부 |
|
|
| `REGISTRY_CODE_DRIFT` ✅ RESOLVED (B, 2026-06-08) | env-keys.yaml 이 as-built env 이름/surface 와 매칭 안 됐음(48행 vs 55키, granular 키 다수 누락) | 위와 동일 grep | **registry 를 as-built 55키와 전면 정렬(39행 추가 + 2 이름충돌 해소) + `verifyEnvKeys` check C 가 registry↔.env 를 CI 강제.** 독립 검증 missing 0 |
|
|
| `REGISTRY_GITIGNORED` ✅ ACCEPTED (사용자 결정 2026-06-09) | ca-tmpl `.gitignore` 가 `/docs` 전체를 ignore(`CLAUDE.md`/`.claude`/`.codex` 등 AI 툴링과 함께한 **의도적 repo 정책**) → SSOT registry(`env-keys.yaml`)가 version-control 안 됨. drift 가드(check C / RegistryTest)는 파일 부재 시 `assumeTrue` 로 **SKIP**(통과 아님). | `.gitignore:2:/docs`, `git ls-files` 미추적 | **사용자 결정(2026-06-09): 현 정책 유지** — registry 는 local dev artifact, docs/ 전체 gitignore 유지. **한계 수용**: fresh clone/CI(docs 부재)에서 registry drift 가드는 강제되지 않고 SKIP. 따라서 "registry=enforced SSOT"는 *registry-present(로컬) 환경에서만* 성립함을 명시. (재고 시: docs/registries 만 un-gitignore, 또는 wiki SSOT→mirror CI 동기화.) |
|
|
| `VALIDATION_POLICY_DRIFT` ✅ RESOLVED (2026-06-05) | D10 "모든 바인딩 `@Validated` 강제" vs `CorsSettings` 는 `@Validated` 없이 constructor 검증 | `CorsSettings.java`(no `@Validated`), `BootstrapSettings.java`(`@Validated`) | **결정: 계층형 — 단순 제약 `@Validated`+JSR-303, 조건부/교차필드만 constructor + throw**(D10). `CorsSettings` 코드 조정 후속 |
|
|
| `PROFILE_DUALITY_DRIFT` ✅ RESOLVED (2026-06-06) | D6 의 `APP_PROFILE` env 가 코드에 부재(`SPRING_PROFILES_ACTIVE` 단독)였음 | `application.yml` L18 | **결정: `APP_PROFILE` 도입 포기, `SPRING_PROFILES_ACTIVE` 단독**(D6). mismatch-fail 로직 미구현, Claims 행 `wont-fix` |
|
|
| `ENV_FILE_NAME_DRIFT` ✅ RESOLVED (2026-06-06) | D7 `.env.example` vs 실제 `src/.env` | `application.yml` L2 주석 | **결정: `.env.example` 두지 않고 `src/.env`(tracked) 단일 소스로 통일**(사용자 2026-06-06). drift 게이트는 `verifyEnvKeys`(`.env`↔application.yml). |
|
|
| `INVALID_RANGE_LENIENT` ✅ RESOLVED (2026-06-05) | §판정 기준 "invalid range → fail" vs `CorsSettings` 음수 maxAge → default+warn(lenient) | `CorsSettings` compact ctor | **결정: invalid range → `throw`(fail-fast)**(D10). `CorsSettings` 음수 maxAge default 를 throw 로 수정 후속 |
|
|
| `SETTINGS_PREFIX_INTERNAL_DRIFT` ✅ 진단 완료 (2026-06-06) — **latent bug** | `application.yml` L147 `ca-skeleton.cmd.app-name` 이 stale. 클래스+테스트는 `ca-skeleton.bootstrap.app-name` 로 일관(다른 4개 `*Settings` 도 `ca-skeleton.<group>` 컨벤션). 실제 기동 시 `BootstrapSettings.appName` 미바인딩 → `@NotBlank` startup fail 날 버그 | `BootstrapSettings.java`+`BootstrapSettingsTest.java`(both `ca-skeleton.bootstrap`) vs `application.yml` L147 (`ca-skeleton.cmd`) | **클래스가 SSOT. ca-tmpl `application.yml` L147 `cmd:` → `bootstrap:` 수정(코드 후속 bugfix)**. 신규 `*Settings` 는 `ca-skeleton.<group>` 컨벤션 |
|
|
| `LENIENT_DEFAULT_EXCEPTIONS` ✅ ACCEPTED (사용자 결정 2026-06-09) | D10 "lenient default 금지"는 `CorsSettings` 에 적용(throw 로 수정, RESOLVED)했으나, `LoggingSettings`(`bootstrap.settings`)·`SecuritySettings`(`adapter-web.settings`)는 여전히 warn-and-default. 감사가 D10 위배로 잡음. **그러나 둘 다 careless 가 아니라 문서화된 근거 있는 예외**: (1) `LoggingSettings` — logback 이 `<springProperty>` 로 *이미* 자기 default 로 바인딩한 뒤라 record 는 *operator 경고 surface* 일 뿐(여기서 throw 해도 logback 은 이미 진행). (2) `SecuritySettings` L28 — "audience 없음 → audience 검증 skip" 은 *선택적 보안 기능 토글*이지 typo 마스킹 fallback 이 아님. | `LoggingSettings.java`(File/Async/Json compact ctor `log.warn`+default), `SecuritySettings.java:28` | **사용자 결정(2026-06-09): lenient 유지** — D10 은 "*의미 있는 invalid 를 silent default 로 가리지 말 것*"이 취지이며, 위 둘은 owning-library(logback)/optional-feature 라 예외가 정당. D10 을 *보편 강제*가 아니라 *예외 명시 규약*으로 정합. (audience 를 prod 필수로 하려면 별도 prod-profile fail-fast 결정 — 본 branch 범위 밖.) |
|
|
| `REGISTRY_VALIDATION_UNENFORCED` ✅ RESOLVED (2026-06-09) | registry `env-keys.yaml` 가 high-risk numeric 키에 `validation: positive_int`/`non_negative_int` 컬럼을 선언하나 코드가 강제 안 함(Spring-native 로 흘러가 Hikari/Tomcat 가 늦게·cryptic 하게 reject). 감사 "fictional validation columns". | `RuntimeNumericBoundsValidator.java`(신규), `RuntimeSafetyConfig`(@Bean) | **신규 `RuntimeNumericBoundsValidator`(`SmartInitializingSingleton`, 고위험 numeric만) 가 resolved Spring property 를 읽어 범위 위반 시 fail-fast — `APP_*` 키 이름 명시 메시지. pool max/min-idle, tomcat max/min-spare/max-conn/accept-count 6키. `RuntimeNumericBoundsValidatorTest` 4 메서드(`:app-bootstrap:test` 144/144 green). 이로써 positive_int/non_negative_int 컬럼이 *실제 강제*. log.* 등 logback-owned·Duration 키는 owning-lib 위임(범위 밖).** |
|
|
|
|
## 마주친 문제
|
|
|
|
- 아직 없음.
|
|
|
|
## 관련 일일 노트
|
|
|
|
- [[raw/daily-notes/2026-05-27]]
|
|
|
|
## 완료 후 정리
|
|
|
|
- PR 링크:
|
|
- 리뷰 메모:
|
|
- 머지 결과 / 배포 환경:
|
|
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
|
- `actually-implemented` 항목: M1 env `APP_*` 전면통일(.env/application.yml/Settings 로그문자열/test), M2 `ca-skeleton.bootstrap.app-name` bug fix, M3 `CorsSettings` 계층형 validation, M4 `StartupSafetyValidator`(prod-unsafe + multi-instance presence) + 3 플래그 wiring, M5 `verifyEnvKeys` 3-way gate(registry SSOT, check C), **registry `env-keys.yaml` as-built `APP_` 키 전면 정렬(B, 2026-06-08: 39행 추가 + 2 이름충돌 해소)**, **M6 `RuntimeNumericBoundsValidator`(2026-06-09 — 고위험 numeric pool/tomcat 6키 fail-fast, registry `positive_int`/`non_negative_int` 컬럼 실제 강제, `RuntimeNumericBoundsValidatorTest` 4) + `RuntimeSafetyConfig` @Bean wiring**.
|
|
- **2026-06-09 갱신**: live `APP_` 키 수 = **57**(검증: `grep '^APP_' src/.env | sort -u | wc -l`). 본문의 historical "55"(2026-06-08 게이트 출력)는 그 시점 값 — 현재 57. lenient 정책은 `LENIENT_DEFAULT_EXCEPTIONS`(§Audit) 로 정합(LoggingSettings/SecuritySettings 의도적 예외).
|
|
- `locally-verified` 항목: `./gradlew check` BUILD SUCCESSFUL(전 모듈 test + ArchUnit + verifyCleanArchitectureDependencies + verifyEnvKeys), `verifyEnvKeys` drift 주입→FAIL / clean→OK + check C 단독 발화 확인, `:app-bootstrap:test`·`:adapter-web:test` BUILD SUCCESSFUL(독립 재검증), live `APP_` 55키 registry missing 0, 리뷰 체인(sentinel/spec/quality) 전부 PASS.
|
|
- `prod-verified` 항목: 없음(skeleton, prod 배포 이력 없음).
|
|
- **추출하지 않을 항목** (planned / documented-only / abandoned): adapter on/off 3-layer(owner: integration-adapter-templates), outbound/tracing/cache/security/secret 값 semantics(각 owner branch), multi-instance 5종 contract bean 실제 구현(각 owner branch, 본 branch 는 presence 계약만 소유), `APP_PROFILE`(D6 abandoned).
|