".env.example 을 추가하려다 깨달은 것 — 우리 .env 는 이미 tracked 였다. drift 게이트의 정답 소스는 템플릿이 아니라 코드가 실제로 요구하는 surface 다."
핵심 논지
흔한 패턴: secret 때문에 .env 를 gitignore 하고 redacted .env.example 을 commit. 하지만 .env.example 은 "작성 시점 스냅샷"이라 새 env 가 생겨도 갱신 안 돼 drift → 신규 합류자가 복사해 띄우면 누락 env startup 실패.
반전(이 프로젝트의 실제 결정): ca-tmpl 은 src/.env 자체를 git-tracked 로 둔다(로컬 dev 기본값 포함, secret 은 로컬 sentinel). 이 경우 .env.example 은 password 만 가린 중복 사본이라 가치가 거의 없다. 처음엔 D7 문언대로 .env.example + verifyEnvExample 을 만들었다가, 리뷰에서 ".env 가 이미 tracked 인데 example 이 왜 필요?"라는 지적으로 제거 → task 를 verifyEnvKeys 로 rename.
결론적 게이트(custom Gradle task verifyEnvKeys)는 template 파일이 아니라 코드 surface 를 기준으로 두 방향만 강제:
application.yml 의 ${VAR}(inline default 없는 것=required) 가 전부 src/.env 에 존재(누락 0).
src/.env 의 모든 키가 application.yml 어딘가 ${...} 로 실제 소비됨(orphan/stale 0).
inputs.files(...) 선언으로 Gradle up-to-date 캐싱과 호환, check 에 dependsOn 연결해 CI 필수 게이트화.
교훈: ".env.example drift 막기"는 수단이지 목적이 아니다. 진짜 목적은 "코드가 요구하는 env 와 운영자가 가진 env 가 일치하는가". .env 가 tracked 라면 example 은 군더더기이고, 게이트는 application.yml ↔ .env 를 직접 보는 게 맞다.
왜 registry 기준이 아니라 application.yml surface 기준인가 (설계 결정)
contract registry(env-keys.yaml)는 여러 미구현 branch 의 키까지 포함 → registry 와 .env 를 1:1 강제하면 코드에 없는 phantom 키 수십 개를 넣어야 함(운영자가 무시할 값).
"운영자가 .env 만으로 앱을 띄울 수 있는가"가 진짜 목적 → 검증 기준은 실제 config surface(application.yml placeholder) 가 맞다. registry 는 governance SSOT 로 별도 유지.
교훈: drift gate 의 "정답 소스"는 빌드 가능한 표면이어야지, 미래 계약을 담은 레지스트리가 아니다.
곁가지 주제
12-factor §III config 와 APP_ prefix 전면 통일(외부 의존 env 와 시각 분리)의 트레이드오프.
boolean true/false-only, Duration 30s-only 같은 "기계적으로는 동등하나 팀 규약으로 1택" 결정을 어떻게 문서화/강제하나.