53 KiB
title, source_type, status, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label, contract_packet_sha256
| title | source_type | status | id | kind | project | work_item | inherits | refines | overrides | depends_on | contract_packet | branch | parent_branch | related_projects | governing_docs | tags | created | target_merge | status_label | contract_packet_sha256 | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| branch / feature-env-driven-runtime-configuration | branch-note | raw | BR-CA-SKELETON-OPERATIONAL-CONTRACT-004 | project-work-item | ca-skeleton-operational-contract | WI-CA-SKELETON-OPERATIONAL-CONTRACT-004 |
|
1 | feature-env-driven-runtime-configuration |
|
|
|
2026-05-21 | in-progress | 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 descriptorraw/branch-notes/feature-capability-provider-selection-contract 이전 범위는 판정 메커니즘뿐이다.
APP_MULTI_INSTANCE_ENABLEDflag 자체,env-keys.yamlrow,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라벨이 붙어 있어 재판정 대상임이 노트 자체에 기록되어 있다.
부모 (필수)
- Parent project (canonical SSOT): raw/project-notes/ca-skeleton-operational-contract
ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
1 - 패킷 스키마:
contract_packet: 1 - 완료 조건: env configuration 6필드 contract와 invalid-config test가 통과한다
상속한 프로젝트 결정
| 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 |
브랜치 지역 결정
기존 branch-local 결정은 아래
## Decision Evidence Map의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|
선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|
묶음
- 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
본 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_/gsubstring 충돌로SPRING_MAIN_LOG_STARTUP_INFO훼손. 둘 다 resolved.
면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- raw/interviews/startup-fail-fast-config-validation-2026-06-06 —
SmartInitializingSingletonvsEnvironmentPostProcessorvsApplicationReadyEvent, 계층형@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 전환 자체가 더 좋은 글감(블로그 갱신 시 반영).
목표
local/dev/staging/prod 서버별 동작이 코드 수정 없이 env로 전환되어야 합니다. error exposure, logging, tracing, adapter enablement, timeout/retry/security/datasource 설정을 env contract로 고정합니다.
- 이슈:
- PR:
범위
포함 범위
- 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.yamlregistry 의APP_*.APP_전면 통일(datasource/server 등 Spring-native 매핑 키도 예외 없이APP_). 현행 코드의DB_*/LOG_*/CORS_*/OIDC_*/SERVER_*→APP_*rename 은 후속 코드 마이그레이션(§AuditENV_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 집행 컴포넌트 =
SmartInitializingSingletonvalidator 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 표기 =
30s1택. ISO-8601PT30S형식은 forbidden (가독성/일관성).@DurationUnit을 통한 정수만 받는 형식은 허용 (예: int 30 + @DurationUnit(SECONDS)). byte는DataSize(10MB). - 2026-05-22: boolean 표기 =
true/falseonly (1/0/on/offforbidden). - 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_ACTIVEunset → 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/.envgit-tracked 단일 소스). drift 도구 =verifyEnvKeys(registry ↔ application.yml ↔.env3-way). registry(env-keys.yaml) = enforced SSOT: check C 가 live 모든APP_키의 registry 행 존재를 build-time 강제. registry 를 as-built 55키와 전면 정렬(39행 추가 +APP_LOG_LEVEL5분할 +APP_SHUTDOWN_TIMEOUT→APP_SERVER_SHUTDOWN_TIMEOUT). - 2026-05-22: multi-instance claim parsing 메커니즘 = env property
APP_MULTI_INSTANCE_ENABLEDboolean (default false). true로 설정 시 다음이 모두 강제: (a) ShedLock/distributed lock bean 등록, (b) RedissonRLockbased 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_ENABLEDrow를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 결정의 표준 근거
- raw/official-docs/config-12-factor-app-config — 12-factor §III. Config (이론 출처). ca-tmpl
- 검토한 대안:
- 대안 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)
- 대안 1: Spring Cloud Config Server — raw/official-docs/config-spring-cloud-config-server-official (중앙 git-backed +
- 비교 핵심: 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_키) 시verifyEnvKeysbuild 실패 (registry SSOT, check C).- feature flag registry owner 강제: 모든 runtime/canary flag(
@FeatureFlagannotation 또는APP_FEATURE_*env)는env-keys.yaml에 row가 존재하고owner_branchfield가 비어 있지 않아야 함. 측정 방법: 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.yamlregistry 의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.ymlL2 주석 "Mirrors src/.env … input validation lives in the *Settings records".
- UNSUPPORTED_IMPL_DECISION:
*Settingsrecord 명명 +<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종)는
SmartInitializingSingletonvalidator 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_LENIENTRESOLVED — §Audit).
3. profile 해석 (actually-implemented, 단 단일화)
Trace: D6 /
SPRING-EXTCONFIG-C4. anchor =application.ymlL16-18spring.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.gradleverifyEnvKeystask +docs/registries/env-keys.yaml.
- UNSUPPORTED_IMPL_DECISION: gate 형태(custom Gradle task)는 도구 선택 운영 결정(D7 자체 UNSUPPORTED). trade-off: registry ↔ application.yml ↔
.env3-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 + ArchUnitCleanArchitectureTest+verifyCleanArchitectureDependencies+verifyEnvKeys) BUILD SUCCESSFUL. 리뷰 체인 ca-architect-sentinel / ca-spec-reviewer / ca-quality-reviewer 모두 PASS. 2026-06-08 B 후속: registry = enforced SSOT 로 전환 —env-keys.yamlas-built 55키 전면 정렬(39행 추가 + 2 이름충돌 해소) +verifyEnvKeyscheck C 추가. 독립 검증:verifyEnvKeysBUILD SUCCESSFUL(55 APP_ keys registered),:app-bootstrap:test·:adapter-web:testBUILD SUCCESSFUL, liveAPP_키 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_PROFILErow 제거(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 주석 보존). 독립 검증: liveAPP_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(모든 liveAPP_키 ∈ 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_ORIGINSempty →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_ACTIVEunset →${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 (registryowner_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 위임(registryowner_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-contract6개가 consume. 본 flag 의미 변경 시 6개 모두 영향.
- raw/branch-notes/feature-secrets-config-source-contract —
검증해야 할 주장
| 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 위임(범위 밖).* |
마주친 문제
- 아직 없음.
관련 일일 노트
완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- wiki 추출 대상 (verified만,
wiki/projects/로만 추출):actually-implemented항목: M1 envAPP_*전면통일(.env/application.yml/Settings 로그문자열/test), M2ca-skeleton.bootstrap.app-namebug fix, M3CorsSettings계층형 validation, M4StartupSafetyValidator(prod-unsafe + multi-instance presence) + 3 플래그 wiring, M5verifyEnvKeys3-way gate(registry SSOT, check C), registryenv-keys.yamlas-builtAPP_키 전면 정렬(B, 2026-06-08: 39행 추가 + 2 이름충돌 해소), M6RuntimeNumericBoundsValidator(2026-06-09 — 고위험 numeric pool/tomcat 6키 fail-fast, registrypositive_int/non_negative_int컬럼 실제 강제,RuntimeNumericBoundsValidatorTest4) +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 checkBUILD SUCCESSFUL(전 모듈 test + ArchUnit + verifyCleanArchitectureDependencies + verifyEnvKeys),verifyEnvKeysdrift 주입→FAIL / clean→OK + check C 단독 발화 확인,:app-bootstrap:test·:adapter-web:testBUILD SUCCESSFUL(독립 재검증), liveAPP_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).