Files
DongHyeonkaandClaude Opus 5 ef947e5bb0 refactor(build,ci): 현재 상태 검증을 걷어내고 불변조건만 남기는 검증 표면 축소
외부 리뷰("현재 상태를 유지하기 위한 검증이 너무 많고, 그 검증 자체를
다시 검증하는 구조까지 생겼다")를 설계 문서로 정리하고 코드로 반영한다.
설계·판단 근거는 docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md.

삭제
- .github/ci-gate-matrix.yml(1,025줄) + verify-gate-matrix.sh(568줄):
  Gradle task graph와 workflow graph에 이미 있는 정보의 3중 복제
- verify-gradle-wrapper.sh(799줄): workflow 바이트 해시 잠금.
  wrapper 검증은 gradle/actions/wrapper-validation(full SHA 핀)에 위임
- DeveloperExperienceContractTest 등의 CI YAML mutation 테스트:
  애플리케이션 test suite가 GitHub Actions YAML 파서를 검증하던 계층 역전
- 문서 drift 파서: verifyReadmeCommands, verifyRunbookReferences,
  verifyDocumentedLeafCount, verifyTestSourceSetRegistry
- 빈 레지스트리를 지키던 커스텀 YAML 파서: verifyTrivyignore,
  verifyQuarantineSunset, flaky-quarantine.yaml
- verifyConfigurationPropertiesProcessor, verifyOneTypePerFile:
  각각 ca.spring-config convention과 Checkstyle OneTopLevelClass가 대체
- 정상 입력으로도 성공할 수 없던 messaging always-fail task
- ModuleRegistry의 JSON 필드 집합 정확 일치, sample-portfolio negative guard

이동
- java/quality/spring 공통 설정을 configure(subprojects) 블록에서
  ca.java-conventions / ca.quality-conventions / ca.java-library /
  ca.spring-library convention plugin으로
- 아키텍처 검증을 ca.architecture로, JPA·messaging qualification을
  gradle/qualification/ 아래로, verifyEnvKeys를 :app-bootstrap 소유로

완화
- Git revision은 releaseCheck·아카이브 생성에서만 요구. 일반 빌드는 SNAPSHOT
- SpotBugs/FindSecBugs는 로컬 check에서 빼고 qualityCheck 레인으로

task 계층
- leaf check는 그 leaf만. architectureCheck / qualityCheck /
  configContractCheck / integrationCheck / ci / releaseCheck로 이름 분리

CI
- _reusable-gradle.yml 신규. checkout + wrapper validation + JDK/캐시 공통화
- fileserver-release.yml -> fileserver-certification.yml (CD가 아니라 certification)
- GitHub Actions = CI + artifact, Argo CD = CD 경계를 docs/ci-cd/boundary.md로 고정

순증감 +3,274 / -7,483.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 20:33:19 +09:00
..

src — 빌드 스크립트 / 환경 변수 참조

src/ 는 Gradle 멀티모듈 루트입니다. 모듈 경계·의존 방향 규칙은 루트 CLAUDE.mdAGENTS.md, 모듈별 규칙은 각 모듈의 CLAUDE.md 가 SSOT 입니다.

이 문서는 build.gradle.env 의 코드 주석에서 덜어낸 설정 항목 설명과 결정 근거를 모아둔 참조용 기록입니다. 두 파일에는 짧은 기능 주석과 "자세한 내용은 README 참조" 포인터만 남기고, "왜 이렇게 했나"는 여기서 풀어 설명합니다.


build.gradle — 빌드 / 검증 게이트

모든 모듈의 check 태스크는 아래 verify 게이트에 의존합니다. 빌드를 통과하려면 검사가 모두 green 이어야 합니다.

게이트 하는 일
verifyCleanArchitectureDependencies 모듈 간 의존 방향이 허용된 범위 안에 있는지 검사
verifyRuntimeModuleMembership registry의 두 composition root membership과 실제 main project dependency가 정확히 일치하는지 검사
:app-bootstrap:verifyEnvKeys env-keys.yamlapplication.ymlsrc/.env.example ↔ 타입 설정 메타데이터가 어긋나지 않는지 검사
verifyApplicationCoreDependencyPurity application-core의 production 의존이 project-only이고 클래스패스에 프레임워크가 없는지 검사
verifyNoIgnoredSourcePackages Git이 실을 수 없는 Java 소스 파일이 없는지 검사

architectureCheck 하나가 위 네 개를 모두 실행합니다.

2026-09에 삭제한 게이트. verifyOneTypePerFile(Checkstyle의 OneTopLevelClass가 같은 규칙을 파싱된 파일에 대해 검사한다), verifyTrivyignore·verifyQuarantineSunset(빈 레지스트리를 지키는 수백 줄짜리 커스텀 YAML 파서), verifyReadmeCommands·verifyDocumentedLeafCount· verifyRunbookReferences·verifyTestSourceSetRegistry(문서 파서), verifyConfigurationPropertiesProcessor(ca.spring-config convention plugin이 대체). 근거는 docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md.

Local bootstrap

./gradlew bootstrapbootstrapCompilebootstrapDependenciesbootstrapMigrateAndStartbootstrapSampleContractbootstrapSmoke를 순서대로 실행합니다. DB와 app lifecycle은 저장소 루트의 base/local Compose 조합이 소유하며, app startup Flyway가 끝나 public health endpoint가 준비되어야 다음 단계로 넘어갑니다. src/.env는 env 설정의 SSOT이고 bootstrap이 별도 env template을 만들지 않습니다.

Traceable version + dependency locking

  • 모든 project version은 <MAJOR>.<MINOR>.<PATCH>+<12자리 git sha>입니다. base version은 -PreleaseVersion/RELEASE_VERSION, revision은 -PgitRevision/GIT_SHA/GITHUB_SHA 순으로 주입하고, 로컬에서는 현재 Git commit을 읽습니다.
  • 모든 JAR manifest에 Implementation-VersionBuild-Revision을 기록합니다. Git metadata도 revision property도 없는 상태는 traceable artifact를 만들 수 없으므로 configuration 단계에서 실패합니다.
  • 모든 subproject가 lockAllConfigurations() + LockMode.STRICT를 사용하고, lock state는 Gradle 기본 <module>/gradle.lockfile에 둡니다. 이 경로는 Renovate Gradle manager 기본 인식 경로와 같습니다.
  • lock 갱신은 ./gradlew resolveAndLockAll --write-locks 한 가지 명령으로 수행합니다. 이 task는 --write-locks가 없으면 실패하며, 일반 build가 lock state를 조용히 다시 쓰지 못하게 합니다.

Reproducible archives (D10)

모든 AbstractArchiveTask는 file timestamp 보존을 끄고, file order를 재현 가능하게 정렬하며, directory/file mode를 각각 0755/0644로 고정합니다. Java toolchain은 21이고 로컬·CI patch version은 root .tool-versions의 Temurin 값으로 맞춥니다.

두 번의 clean, no-cache bootJar SHA-256 비교는 다음 명령으로 실행합니다.

cd ..
bash .github/scripts/verify-reproducible-build.sh

이 검사는 동일 toolchain·동일 source revision 안에서 archive 재현성을 검증합니다. 서로 다른 JDK vendor/build나 container base image까지 byte-for-byte 같음을 주장하지 않습니다.

-parameters 컴파일러 플래그

  • 결정. 모든 subproject 의 Java 컴파일에 -parameters 플래그를 직접 설정합니다.
  • 근거. Spring MVC 는 @PathVariable / @RequestParam 의 이름을 reflection 의 parameter metadata 에서 읽습니다. 이 플래그가 없으면 파라미터 이름이 arg0, arg1 로 컴파일되어 바인딩이 깨집니다. Spring Boot Gradle 플러그인은 이 플래그를 자동으로 켜 주지만, 이 프로젝트는 플러그인을 apply false 로 두기 때문에 자동 적용이 일어나지 않습니다. 그래서 각 subproject 의 JavaCompile 에 직접 설정합니다.

verifyCleanArchitectureDependencies

  • 하는 일. config/architecture/modules.jsonallowed_dependencies를 읽고, 실제 Gradle 프로젝트 의존(api / implementation / compileOnly / runtimeOnly)이 그 범위를 벗어나면 빌드를 실패시킵니다.
  • JSON registry가 의존 방향의 SSOT 입니다. 새 모듈이나 새 production 의존 edge를 추가하면 registry와 ArchUnit 규칙(CleanArchitectureTest)을 함께 갱신해야 합니다. settings와 gate는 같은 registry를 읽고, 등록되지 않은 leaf나 허용되지 않은 edge를 fail-closed로 거부합니다.

verifyRuntimeModuleMembership

  • 하는 일. 같은 registry의 runtime_compositions와 각 leaf의 runtime_memberships를 읽어 app-bootstrap/sample-portfolio의 실제 api/implementation/compileOnly/runtimeOnly project dependency와 정확히 대조합니다.
  • opt-in의 의미. membership이 빈 GraphQL/gRPC/WebSocket/Mongo leaf는 독립 빌드 대상이지만 두 shipped runtime에는 없습니다. app-bootstrap의 conditionalTransportTest test-only classpath는 실제 채택 전에 세 inbound transport를 함께 qualification하기 위한 evidence composition입니다.
  • 변경 규칙. production edge를 추가하거나 제거할 때 allowed_dependencies, runtime_memberships, 실제 Gradle dependency를 같은 변경에서 갱신하지 않으면 check가 실패합니다.

세 opt-in inbound transport의 test-only composition, 실제 wire 경계, positive-count/zero-skip 증거는 다음 release-blocking aggregate로 실행합니다.

./gradlew conditionalTransportQualification

파일당 public 최상위 타입 1개 (code-conventions I6)

Checkstyle이 소유합니다 — OneTopLevelClassOuterTypeFilename(config/checkstyle/checkstyle.xml). 각 leaf의 checkstyleMain/checkstyleTest가 그 leaf의 check에서 돕니다.

verifyOneTypePerFile이라는 루트 태스크가 있었고 삭제했습니다. src/main/java를 줄 단위 정규식으로 읽었고 세 가지가 틀렸습니다: package-private 최상위 타입이 보이지 않았고(126개 main 소스가 한 번도 매칭되지 않아, 파일 하나에 package-private 타입 다섯 개가 있어도 통과했다), src/main/java만 읽었고, ^public 앵커 때문에 블록 주석이나 텍스트 블록의 public으로 시작하는 줄을 선언으로 셌습니다. Checkstyle은 파싱된 파일에 같은 질문을 하고, leaf 단위로 돕니다.

:app-bootstrap:verifyEnvKeys

  • 소유. app-bootstrap. 이 질문("이 애플리케이션의 배포에 무엇을 줘야 하는가")은 composition root의 것이고, ./gradlew :domain-core:check가 알아야 할 사항이 아닙니다. 루트 집계 이름은 configContractCheck이고 정의는 src/gradle/config-contract.gradle입니다.
  • 하는 일. docs/registries/env-keys.yaml, application.yml, src/.env.example 세 곳을 lock-step(서로 어긋나지 않게) 으로 유지합니다. env-keys.yamlAPP_ 키의 SSOT 이고, drift 가 생기면 빌드를 실패시킵니다.
  • 막으려는 것 3가지. (1) 필수 env 가 조용히 누락되는 것, (2) 더 이상 쓰지 않는 stale env 키가 .env 에 남는 것, (3) 실제로 쓰는 APP_ 키가 registry 에 등록되지 않고 빠져나가는 것.
  • 검사 항목.
    • A. application.yml 의 placeholder 중 inline default(${VAR:default})가 없는 필수 placeholder(${VAR})는 반드시 .env 에 존재해야 합니다.
    • B. .env 의 모든 키는 application.yml 의 어떤 ${...} placeholder 가 참조해야 합니다. (아무도 안 쓰는 키는 orphan 으로 간주해 실패)
    • C. .env 의 모든 APP_ 키는 registry 에 - name: <KEY> 행이 있어야 합니다.
  • SPRING_* 키는 왜 registry 추적 대상이 아닌가. SPRING_* 는 Spring Boot 가 정의한 native 키라 프로젝트가 소유한 계약이 아닙니다. 그래서 C 검사는 일부러 APP_ prefix 로만 범위를 좁혔습니다.
  • 비고. src/.env 가 프로젝트의 커밋된 env 파일이며, 별도의 .env.example 템플릿은 두지 않습니다.

verifyPublicPathSnapshot

  • 하는 일. deny-by-default public path 표면이 승인 없이 바뀌면 빌드를 실패시킵니다.
  • 배경. 이 앱은 deny-by-default 입니다. 즉 SECURITY_PUBLIC_PATHS 가 먹이는 명시적 permitAll() 경로를 제외한 모든 요청은 인증을 요구합니다(src/.envSecuritySettings.publicPaths()SecurityConfig). 이 public 표면이 바뀌는 순간이 곧 보호되던 엔드포인트가 조용히 공개로 노출되는 지점입니다. 그래서 그 표면을 snapshot 으로 떠 두고, 미승인 변경에 빌드를 실패시킵니다.
  • 승인 방법. verifyPublicPathSnapshot 은 항상 읽기 전용입니다. reviewer 가 변경을 승인한 뒤 ./gradlew updatePublicPathSnapshot -PapprovePublicPathChange 로 snapshot 을 명시적으로 다시 생성합니다. 공개 경로 변경은 보안 리뷰 대상으로 보고 재생성된 snapshot 을 함께 커밋합니다.
  • 결정 — 무엇을 snapshot 했나 (프로젝트 선택). 초기안은 기동 시 SecurityFilterChain.getFilters() 를 introspection 하는 방식이었습니다. 하지만 그 reflection 은 Spring 버전마다 깨지기 쉽습니다(permitAll matcher 가 RequestMatcherDelegatingAuthorizationManager 의 private 필드에 숨어 있음). 그래서 permitAll() 을 실제로 먹이는 결정적 SSOT 인 SECURITY_PUBLIC_PATHS 자체를 snapshot 합니다. 탐지 목표(공개 경로 변경은 무조건 게이트를 실패시킨다)는 같고, 메커니즘은 더 견고합니다.
  • snapshot 위치. docs/security/public-paths-snapshot.txt. 이 파일은 커밋된 필수 보안 baseline 입니다. CI 는 Gradle 실행 전에 파일이 비어 있지 않고 Git에 추적되는지 검사하므로 fresh checkout 에서 누락되거나 untracked 상태면 즉시 실패합니다. 승인된 변경만 update task로 재생성한 뒤 보안 리뷰와 함께 커밋합니다.

Trivy suppression과 플래키 격리 — 정책은 유지, 파서는 삭제

Trivy suppression. repo 루트 .trivyignore.yaml이 유일한 suppression 소스이고, 모든 Trivy 호출이 --ignorefile .trivyignore.yaml로 명시합니다. 항목은 id, 비어 있지 않은 statement, 90일 이내의 미래 expired_at을 갖춰야 합니다. 이 규칙은 그대로이고, 강제하는 주체가 .github/CODEOWNERS 리뷰어로 바뀌었습니다. verifyTrivyignore는 105줄짜리 손으로 쓴 YAML 파서였고 — 들여쓰기 추적, 인라인 스칼라 처리, 따옴표 제거 — 지키던 파일은 만들어진 이래 계속 비어 있었습니다. 실제 항목이 생기고 그것이 drift하기 시작하면 그때 자동화합니다. 진짜 항목을 상대로, 진짜 YAML 라이브러리로.

플래키 격리. 간헐 실패 테스트에 JUnit 기본 @Tag("quarantine")를 붙이면 메인 testexcludeTags 'quarantine'로 제외하므로 merge를 막지 않고, ./gradlew quarantineTest(비차단)로만 돕니다. 이 두 줄은 유지됩니다.

flaky-quarantine.yaml 레지스트리와 verifyQuarantineSunset(14일 sunset + drift 검사)은 삭제했습니다. 250줄짜리 YAML 파서 + Java 렉서(주석과 문자열 리터럴 안의 @Tag("quarantine")를 걸러내려고 인덱스 보존 렉서를 직접 구현)로 항목이 0개인 레지스트리를 지키고 있었습니다. 순서가 반대입니다 — 실제로 격리된 테스트가 생기고, 그게 주차장이 되기 시작할 때 도입할 정책입니다.

CI 게이트 배선

  • 소유 범위. 이 계약은 게이트 배선(어떤 게이트가 CI 에서 돌고 실패 시 어떻게 릴리스를 막는가)을 소유합니다. 개별 scanner/tool/severity 정책 은 owner 브랜치가 소유하며, 그 20행 매핑의 in-repo SSOT는 Gradle task graph와 GitHub Actions job graph 그 자체입니다.

    .github/ci-gate-matrix.yml(1,025줄, 107개 게이트 행)과 .github/scripts/verify-gate-matrix.sh (568줄)는 삭제했습니다. 그 표는 이미 두 그래프에 있는 정보의 세 번째 사본이었고, 검사기는 세 사본을 서로 같게 유지하는 일을 했습니다. 결과적으로 체크 하나를 추가하려면 Gradle · workflow · 표 · 검사기 기대값 · Java 계약 테스트 다섯 곳을 같이 고쳐야 했습니다.

  • 워크플로. .github/workflows/ci-quality-gates.ymlrelease-gate 잡이 모든 release-blocking 게이트의 fan-in(단일 required status check)입니다. 플래키 quarantine 잡은 의도적으로 needs 에서 제외(비차단)됩니다. 위임 게이트(Trivy SCA/이미지 스캔)는 dependency-vulnerability.yml 가 소유하며, GitHub Actions 는 워크플로 간 needs 를 못 쓰므로 branch protection 의 required check 합집합으로 묶습니다.


.env — 환경 변수 레퍼런스

spring-dotenvsrc/.env 를 읽어 외부화 설정을 주입합니다(bootRun 의 working dir 가 src/ 라 이 파일이 잡힙니다). 아래는 섹션별 키 설명입니다. 따로 표기가 없으면 restart-only(값 변경 시 재기동 필요)로 간주하세요.

App identity

  • APP_NAMEspring.application.name 과 JSON 로그의 app 필드. 자유 문자열.
  • SPRING_PROFILES_ACTIVE — 활성 Spring profile. 보통 local | dev | stage | prod. JSON 로그의 profile 필드도 이 값을 씁니다.

Runtime safety (기동 시 StartupSafetyValidator 가 fail-fast 검사, D8)

  • APP_ERROR_DETAIL_EXPOSURE_ENABLED — 응답에 내부 에러 상세를 노출할지. true | false. prod 프로필에서는 반드시 false 여야 하며, 아니면 기동이 실패합니다.
  • APP_LOG_BODY_CAPTURE_ENABLED — 요청/응답 body 를 로그에 캡처할지. true | false. prod 에서는 반드시 false, 아니면 기동 실패.
  • APP_MULTI_INSTANCE_ENABLEDtrue 면 인스턴스 협조용 빈 5종(lock / cache-stampede / leader / rate-limit / migration)이 모두 있어야 하며, 하나라도 없으면 기동이 실패합니다.
  • APP_RATE_LIMIT_ENABLED — provider-neutral edge rate-limit interceptor 활성화 (429 + Retry-After + X-RateLimit-* 응답). 기본값은 false이며, true로 바꿀 때는 APP_RATE_LIMIT_PROVIDER=redis와 canonical coordination role을 함께 구성해야 합니다.
  • APP_RATE_LIMIT_CLIENT_IP_MODE — 클라이언트 IP 판별 방식. remote-addr-only | forwarded-headers-trusted. 신뢰된 ingress/LB 가 X-Forwarded-For 를 앱 도달 전에 덮어쓸 때만 forwarded-headers-trusted 를 쓰세요. 아니면 IP 위조에 노출됩니다.
  • APP_IDEMPOTENCY_TTL — idempotency 레코드 기본 TTL. duration(예: 24h, 72h). 오래 도는 use case 는 최대 72h 까지 override 가능. (D6)

Async executor

@Async ThreadPoolTaskExecutor 풀 크기 설정입니다.

  • APP_ASYNC_EXECUTOR_CORE_SIZE — 항상 살아있는 워커 수. 1 이상 정수.
  • APP_ASYNC_EXECUTOR_MAX_SIZE — 워커 수 상한. core-size 이상.
  • APP_ASYNC_EXECUTOR_QUEUE_CAPACITY — 백로그 큐 용량. bounded(유한) 필수, unbounded 금지(D7). 1 이상 정수.

다섯 master switch (activation SSOT)

한 bootJar가 다섯 어댑터를 모두 싣고, 각각은 아래 스위치 하나로만 켜집니다. 전부 기본 false 이고, 다섯이 모두 꺼진 배포는 외부 자원 없이 기동합니다. 값의 SSOT는 dev.caskeleton.shared.activation.MasterSwitch이며 docs/registries/env-keys.yaml이 같은 이름을 등록합니다.

환경 변수 Spring property 기본값
APP_PERSISTENCE_JPA_ENABLED ca-skeleton.persistence-jpa.enabled false
APP_PERSISTENCE_MONGO_ENABLED ca-skeleton.persistence-mongo.enabled false
APP_MESSAGING_ENABLED app.messaging.enabled false
APP_NOTIFICATION_PLATFORM_ENABLED ca-skeleton.notification.platform.enabled false
APP_GRAPHQL_ENABLED backend.graphql.enabled false

어댑터가 꺼진 것과 없는 것은 다릅니다. 다섯 어댑터의 클래스는 언제나 아티팩트 안에 있고, 운영자는 재빌드 없이 스위치만으로 켭니다. 클래스가 없으면 애초에 켤 수 없습니다.

켜진 capability가 의존하는 것이 꺼져 있으면 기동이 거부되고, 거부 메시지는 설정해야 할 정확한 property 이름을 말합니다(CapabilityDependencyValidator). 예:

This deployment enables capabilities whose dependencies are off:
  - ca-skeleton.outbox.enabled=true needs relational persistence to store rows;
    set ca-skeleton.persistence-jpa.enabled=true or turn the outbox off.

종속 선택자 둘:

  • APP_GRAPHQL_DEPLOYMENT_MODE — GraphQL이 켜지면 필수이고 기본값이 없습니다. 예전의 boolean과 enum 두 기본값이 서로 다른 말을 했기 때문에, 안전 태세는 배포가 명시적으로 고릅니다. 허용되는 값은 런타임 환경(local/dev/prod)마다 다릅니다.
  • APP_NOTIFICATION_PLATFORM_MODESERVING(기본) 또는 INGEST_ONLY. 선택 사항이고, 허용 값은 NotificationModeSsotTest가 enum에서 파생합니다.

Compose와 런타임 스모크

  • Docker Compose 최소 버전 2.24.4. SSOT는 src/config/runtime/compose-profile-contracts.json 이고, 정본 스크립트가 검증합니다.
  • 진입점은 둘뿐이고, 그 둘만이 증거입니다. 워크플로에 명령 일부를 인라인하면 one-shot 없이 도는 레인이 초록으로 보고됩니다.
# 정적: profile별 정확한 service set, 병합된 모델 전체, mount target 유일성
./scripts/verify-compose-profile-contracts.sh

# 동적: 15개 blocking 레인을 zero-skip으로. create → up --wait → 필수 one-shot →
#       sanitized evidence → 고유 project teardown
./scripts/run-compose-runtime-smoke.sh --matrix src/config/runtime/compose-profile-contracts.json

--lane <id>는 실패 재현용이고, 레인 하나가 초록인 것은 matrix가 통과했다는 증거가 아닙니다.

Optional integration adapters

선택형 Kafka / Redis / Slack / Google Email 어댑터 템플릿입니다. 기본은 전부 비활성(비활성 = 선택 모듈의 기본값). Layer 1 의 @ConditionalOnProperty 가 enabled 일 때만 실제 어댑터를 등록하고, 아니면 fail-fast sentinel 이 포트를 충족합니다(Layer 3).

  • APP_CACHE_REDIS_ENABLED — Redis 캐시 어댑터 on/off. true | false.

  • APP_CACHE_CANONICAL_DEFAULT_PROVIDER — canonical default semantic region 선택. disabled(기본) | redis. redis는 canonical Redis CACHE role binding을 함께 요구합니다.

  • APP_CACHE_REDIS_CLIENT_MODEmanaged는 내장 Lettuce runtime, external은 프로젝트가 제공한 RedisClient bean을 사용합니다. 아래 세 키는 활성화 스위치가 아니라 선택자입니다. 어떤 capability가 켜지는지는 위의 master switch가 정하고, 이 값들은 켜진 capability가 무엇으로 동작할지만 고릅니다. 예전에는 "빈 값 = 비활성"으로 설명돼 있었고, 그 문장이 남아 있는 동안 두 개의 활성화 모델이 공존했습니다.

  • APP_MESSAGING_BROKER — 활성 메시지 브로커 id(예: kafka). APP_MESSAGING_ENABLED=true일 때 필수이고, 빈 값이면 기동이 거부되면서 이 키 이름을 지목합니다. 이 키를 비워도 메시징이 꺼지지는 않습니다 — 끄는 것은 master switch입니다.

  • APP_MESSAGING_KAFKA_BROKERShost:port CSV. APP_MESSAGING_BROKER=kafka 일 때만 필수, 아니면 빈 값.

  • APP_NOTIFICATION_SLACK_PROVIDER — Slack provider id(예: webhook). notification 플랫폼이 켜졌을 때 어떤 provider를 쓸지 고르는 값입니다.

  • APP_NOTIFICATION_EMAIL_PROVIDER — email provider id(예: google-email). 위와 같습니다.

Outbound HTTP client

현재 canonical activation은 다음 두 설정 트리만 사용합니다.

ca-skeleton:
  capabilities:
    http-client:
      expected-state: DISABLED
      bindings: {}
  providers:
    http-client: {}
  • 기본 DISABLED는 binding/provider definition이 모두 비어 있어야 하며 DISABLED_VERIFIED만 게시하고 client, executor, pool, retry/CB registry를 만들지 않습니다.
  • ACTIVE는 exact destination/provider/operation-catalog binding을 요구합니다. 현재 유일한 buffered-classic readiness card가 NOT_IMPLEMENTED이므로 provider resource 생성 전에 fail-closed합니다. 아직 운영 HTTP provider를 활성화할 수 있다는 뜻이 아닙니다.
  • 기존 APP_OUTBOUND_HTTP_*app.outbound.http.*는 canonical 설정이 아닙니다. 루트 src/.env, app-bootstrap의 application YAML, env-key registry에서 제거됐으며 canonical composition에 입력하면 상태와 무관하게 기동을 거부합니다.
  • 다만 sample-portfolio의 application YAML에는 legacy facade를 시연하기 위해 15개 키가 남아 있습니다. 이 모듈은 fixture/reference consumer이고 production 의존성이 아니며, 그 YAML은 :app-bootstrap:verifyEnvKeys가 검사하는 세 파일에 포함되지 않습니다. "제거됐다"는 문장이 저장소 전체를 가리킨다고 읽히지 않도록 범위를 명시합니다.
  • legacy JDK facade가 필요한 fork만 canonical composition 밖에서 OutboundHttpSettings.bindLegacy(Binder)와 legacy configuration을 명시적으로 import합니다. timeout/retry/CB/response-size 설정은 그 migration API 내부 계약일 뿐 canonical provider readiness를 증명하지 않습니다.

Logging

Root / app 레벨 — 허용값은 모두 TRACE | DEBUG | INFO | WARN | ERROR | OFF.

  • APP_LOG_LEVEL_ROOT — root 로거 레벨.
  • APP_LOG_LEVEL_APP — 앱 패키지 레벨.

패키지별 레벨(root 를 덮어씀) — 동일 허용값.

  • APP_LOG_LEVEL_SPRING / APP_LOG_LEVEL_WEB — 각 패키지 레벨.
  • APP_LOG_LEVEL_SQLDEBUG 로 두면 JPA/jdbc 연결 후 SQL 문이 출력됩니다.

파일 출력 + rolling

  • APP_LOG_FILE_ENABLEDtrue 면 rolling JSON 파일 appender 를 붙입니다.
  • APP_LOG_FILE_PATHbootRun working dir(src/) 기준 상대 경로 또는 절대 경로.
  • APP_LOG_FILE_MAX_SIZE — 파일 1개 최대 크기(단위 KB | MB | GB).
  • APP_LOG_FILE_MAX_HISTORY — 보관할 rolled archive 개수. 1 이상 정수.
  • APP_LOG_FILE_TOTAL_SIZE_CAP — 전체 rolled 파일 용량 상한(단위 KB | MB | GB, 0 = 비활성).

Async appender

  • APP_LOG_ASYNC_ENABLEDtrue 면 appender 를 AsyncAppender 로 감싸 non-blocking I/O.
  • APP_LOG_ASYNC_QUEUE_SIZE — back-pressure 전 in-memory 큐 깊이. 1 이상 정수.
  • APP_LOG_ASYNC_DISCARDING_THRESHOLD — 남은 큐 용량이 이 값 미만이면 TRACE/DEBUG/INFO 이벤트를 버립니다(WARN/ERROR 는 항상 유지). 0 = 절대 버리지 않음. 0 이상 정수.

JSON 인코더 세부

  • APP_LOG_JSON_TIMEZONE — IANA timezone(예: UTC, Asia/Seoul) 또는 default(JVM 기본).
  • APP_LOG_JSON_TIMESTAMP_PATTERN — 타임스탬프 패턴. 보통 ISO 8601 (yyyy-MM-dd'T'HH:mm:ss.SSSXXX).
  • APP_LOG_JSON_INCLUDE_CALLER_DATAtrue 면 file/method/line 을 추가. 성능 비용이 큽니다.
  • APP_LOG_JSON_LOGGER_NAME_LENGTH0 = 로거 이름 전체, 양수 = 패키지 축약(예: 36dev.caskeleton.bootstrap.Food.c.bootstrap.Foo 로).

Sampling (SamplingTurboFilter)

  • APP_LOG_SAMPLING_RATEINFO 이하 로그를 남길 확률. [0.0, 1.0] float. prod 는 0.1(10% 샘플링)이 권장, WARN/ERROR 는 항상 유지. 1.0 = 샘플링 없음(dev/local/staging 기본).

Distributed tracing

  • OTEL_EXPORTER_OTLP_ENDPOINT — OTLP exporter endpoint. 빈 값 = exporter off(스켈레톤에서 OTel SEAM 미활성). 값이 있으면 유효한 URL 이어야 하며 기동 시 TracingProperties 가 검증합니다.
  • APP_TRACING_ENABLEDfalse 면 tracing seam 은 꺼지지만, meta.traceIdRequestLoggingFilter 가 W3C traceparent 로 여전히 생성합니다(D4 disabled-fallback 보장).
  • APP_TRACING_SAMPLE_RATE — per-profile 기본값을 덮어쓰는 샘플 비율. [0.0, 1.0] float.
    • per-profile 기본값(D6): prod = 0.01, staging = 0.10, dev/local = 1.0.
    • 결정 — 빈 값으로 두는 이유(D-1 ISSUE-1 fix). 빈 값이어야 per-profile resolver 의 기본값이 실제 tracer sampler 까지 도달합니다(TracingSampleRateResolver 가 SSOT). 값을 박으면 프로필별 기본값이 무시되므로, 특정 비율을 강제하고 싶을 때만 채웁니다.

Privacy: user_principal pseudonymization

  • APP_PRIVACY_PSEUDONYMIZATION_SALT — secret 등급 HMAC-SHA-256 salt. __LOCAL_DEV_ prefix 는 로컬 전용 sentinel 값이며, prod 에서는 secret-manager 가 주입하는 실제 값을 써야 합니다.

Spring Boot bootstrap

  • SPRING_BANNER_MODEoff | console | log.
  • SPRING_MAIN_LAZY_INITIALIZATIONtrue 면 빈 생성을 첫 사용 시점까지 지연.
  • SPRING_MAIN_LOG_STARTUP_INFOtrueStarting/Started 로그 출력.
  • SPRING_THREADS_VIRTUAL_ENABLEDtrue 면 Tomcat 요청 처리에 Java 21 virtual threads 사용.

Jackson — deserialization policy

모든 request DTO 는 Jackson 경계를 지납니다. 아래 4개 스위치는 잘못된 입력을 조용히 강제 변환하지 않고 즉시 실패하게 만듭니다. 개별 DTO 에 클래스 레벨 @JsonIgnoreProperties(ignoreUnknown = true) 로 이 정책을 완화하는 것은 금지이며 ArchUnit 규칙으로 막혀 있습니다.

  • ..._FAIL_ON_UNKNOWN_PROPERTIEStrue: 타입에 선언되지 않은 JSON 키를 거부(Jackson 2.13+ 기본).
  • ..._FAIL_ON_NULL_FOR_PRIMITIVEStrue: primitive 필드에 JSON null 이 와도 0/false 로 강제 변환하지 않고 "필수 필드 누락" 에러로 노출. 또는 wrapper 타입(Integer/Long/Boolean) 과 Optional<T> 를 쓰세요.
  • ..._FAIL_ON_IGNORED_PROPERTIEStrue: JSON 에 @JsonIgnore 처리된 필드가 들어오면 throw (조용히 버리는 대신 계약 drift 를 노출).
  • ..._READ_UNKNOWN_ENUM_VALUES_AS_NULLfalse(Jackson 기본 유지): 모르는 enum 값이 조용히 null 이 되지 않고 throw 되어 VALIDATION_FAILED 로 드러나게 합니다.

Jackson — serialization policy

응답 생성 쪽 정책입니다. 현재 Jackson/Spring Boot 기본값과 같지만 명시적으로 못박아, 미래에 Spring Boot 기본값이 바뀌어도 wire 계약이 조용히 깨지지 않게 합니다(spring.mvc.problemdetails.enabled=false 와 같은 근거). JacksonSerializationPolicyTest 가 강제하며, new BigDecimal(double) 생성자는 no_bigdecimal_double_constructor ArchUnit 규칙으로 금지됩니다.

  • ..._WRITE_DATES_AS_TIMESTAMPSfalse(D2 / RFC 3339): java.time 값을 ISO-8601 문자열로 직렬화(OffsetDateTime"...Z", LocalDate"YYYY-MM-DD"). true 면 epoch 숫자나 [y,m,d,...] 배열로 나가 datetime 계약이 깨집니다.
  • BigDecimal plain output — Jackson 3에는 별도 WRITE_BIGDECIMAL_AS_PLAIN 설정 키가 없습니다. JacksonSerializationPolicyTest"12300000000.00" plain 출력을 직접 검증하고, no_bigdecimal_double_constructor ArchUnit 규칙이 부정확한 new BigDecimal(double) 생성을 금지합니다. 엔드포인트별 string vs number 선택은 그대로 명시적으로 둡니다(공개/금융 API 는 string, 내부 API 는 number+plain).

Server / Tomcat

  • APP_SERVER_PORT — 1~65535 정수.
  • APP_SERVER_SHUTDOWNgraceful | immediate.
  • APP_SERVER_SHUTDOWN_TIMEOUT — duration(예: 30s | 1m | 500ms).
  • APP_SERVER_TOMCAT_MAX_THREADS — 동시 요청 워커 상한. 1 이상 정수.
  • APP_SERVER_TOMCAT_MIN_SPARE_THREADS — idle 워커 풀 하한. 0 이상 정수.
  • APP_SERVER_TOMCAT_ACCEPT_COUNT — 들어오는 TCP 연결의 OS backlog 큐 깊이. 0 이상 정수.
  • APP_SERVER_TOMCAT_MAX_CONNECTIONS — 동시에 열 수 있는 연결 수 상한. 1 이상 정수.
  • APP_SERVER_TOMCAT_CONNECTION_TIMEOUT — duration(예: 20s | 1m).
  • APP_SERVER_COMPRESSION_ENABLEDtrue | false.
  • APP_SERVER_COMPRESSION_MIN_RESPONSE_SIZE — 이 크기 미만 payload 는 압축하지 않음(bytes 또는 단위, 예: 1024 | 1KB | 2KB).
  • APP_SERVER_FORWARD_HEADERS_STRATEGYnone | native | framework. LB/proxy 뒤에서 X-Forwarded-* 를 신뢰할지.
  • APP_SERVER_ERROR_INCLUDE_STACKTRACEalways | never | on_param.
  • APP_SERVER_ERROR_INCLUDE_MESSAGEalways | never | on_param.

Presentation

  • PRESENTATION_API_BASE_PATH — 모든 controller 앞에 붙는 leading-slash 경로(예: /api | /v1 | "").

Auth (OIDC resource server)

  • APP_SECURITY_JWT_ISSUER — OIDC issuer URI(Keycloak realm, Auth0 tenant 등). 필수 — 없으면 기동 실패. 예: https://keycloak.example.com/realms/ca-skeleton.
  • APP_SECURITY_JWT_AUDIENCE — 기대하는 aud claim. 빈 값으로 두면 audience 검증을 건너뜁니다.
  • SECURITY_PUBLIC_PATHS — 인증을 우회하는 경로 CSV. api-base-path 뒤의 full path 를 씁니다(예: /api/healthcheck). 이 값의 변경은 verifyPublicPathSnapshot 게이트가 감시합니다(위 build.gradle 설명 참조).

CORS

  • APP_SECURITY_CORS_ENABLEDtrue | false.
  • APP_SECURITY_CORS_ORIGINS — 허용 origin CSV(예: http://localhost:3000,https://app.example.com).
  • APP_SECURITY_CORS_ALLOWED_METHODS — 허용 메서드 CSV. 빈 값이면 기본값 사용(GET, POST, PATCH, PUT, DELETE, OPTIONS).
  • APP_SECURITY_CORS_ALLOWED_HEADERS — 허용 헤더 CSV. * = 모든 헤더 허용.
  • APP_SECURITY_CORS_ALLOW_CREDENTIALStrue | false.
  • APP_SECURITY_CORS_MAX_AGE — preflight 캐시 TTL(초).

프로파일과 데이터베이스

프로파일마다 데이터스토어가 다르고, 그 차이는 app-bootstrap/src/main/resources/application-*.yml 가 소유합니다.

프로파일 데이터스토어 스키마 소유자 외부 인프라
local (기본) H2 in-memory Hibernate ddl-auto: create-drop 없음
dev PostgreSQL Flyway db/migration/postgresql 필요
prod PostgreSQL Flyway db/migration/postgresql 필요

local./gradlew :app-bootstrap:bootRun 의 기본값(src/.envSPRING_PROFILES_ACTIVE=local) 이라 Docker 없이 바로 뜹니다. 아래 APP_DATASOURCE_* 값은 local 에서는 쓰이지 않고 application-local.yml 이 덮어씁니다.

local 이 검증하는 것은 wiring·요청/응답·애플리케이션 로직이고, 검증하지 않는 것은 migration 과 vendor 동작입니다. H2 에는 migration tree 가 없어 migration 에만 존재하는 테이블(capability schema registry, polling-delivery·inbox stream, Spring Integration lock)이 만들어지지 않습니다. 해당 capability 는 local 기본값에서 꺼져 있고, 켜면 테이블 없음으로 실패합니다.

dev 를 호스트에서 띄우려면(SPRING_PROFILES_ACTIVE=dev) PostgreSQL 이 필요합니다. docker-compose.local.ymldb 서비스가 루프백(127.0.0.1:5433)에만 게시하며, 이 주소가 아래 APP_DATASOURCE_URL 의 커밋된 기본값입니다. 저장소 루트에서 실행합니다.

docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --wait db

컨테이너로 띄우는 ./gradlew bootstrap 경로는 이 호스트 포트를 쓰지 않습니다. compose 가 app 컨테이너의 APP_DATASOURCE_URL 을 내부 네트워크 주소 jdbc:postgresql://db:5432/... 로 덮어씁니다.

ca-skeleton.persistence.vendor(postgresql | h2)가 어느 vendor 구성을 조립할지 고르는 단일 스위치입니다. 값이 둘 중 하나가 아니면 기동이 실패하고, prod 에서 h2 이거나 datasource URL 이 jdbc:h2: 이면 PersistenceVendorProdSafetyValidator 가 기동을 거부합니다(env 로 덮어써도 동일).

Database (Postgres)

  • APP_DATASOURCE_URL — JDBC URL(예: jdbc:postgresql://host:5432/dbname). dev·prod 에서 쓰이며, 커밋된 기본값 jdbc:postgresql://localhost:5433/ca_skeleton 은 위 compose db 서비스의 호스트 주소입니다.
  • APP_DATASOURCE_USERNAME / APP_DATASOURCE_PASSWORD — DB 접속 계정.
  • APP_DATASOURCE_DRIVER — Hibernate dialect 에 맞는 드라이버(예: org.postgresql.Driver).
  • APP_DATASOURCE_DDL_AUTOnone | validate | update | create | create-drop. prod 는 validate 또는 none(JpaSchemaSafetyValidator 가 기동 시 강제). local 은 이 값을 쓰지 않습니다 — application-local.ymlcreate-drop 으로 고정합니다.
  • APP_DATASOURCE_SHOW_SQLtrue 면 SQL 을 로그로 echo.
  • APP_DATASOURCE_FORMAT_SQL — SQL pretty-print(SHOW_SQL=true 일 때만 의미 있음).
  • APP_DATASOURCE_OPEN_IN_VIEW — Hibernate OSIV. prod 에서는 피하세요.

HikariCP 커넥션 풀

  • APP_DATASOURCE_POOL_MAX_SIZE — DB 동시 연결 최대 수. 1 이상 정수.
  • APP_DATASOURCE_POOL_MIN_IDLE — warm 하게 유지하는 최소 idle 연결 수. 0 이상 정수.
  • APP_DATASOURCE_CONNECTION_TIMEOUTacquire() 가 실패하기 전 대기 시간(ms).
  • APP_DATASOURCE_POOL_IDLE_TIMEOUT — idle 연결 회수 임계 시간(ms).
  • APP_DATASOURCE_POOL_MAX_LIFETIME — 연결의 최대 수명(ms). broker timeout 전에 rotate 하도록 설정합니다.

Management / Actuator

  • 결정 — management 포트 분리. actuator 엔드포인트를 앱 API 와 같은 소켓에 노출하지 않으려고 별도 management 포트를 둡니다.
  • MANAGEMENT_SERVER_PORT — 1~65535 정수. APP_SERVER_PORT(8080)와 달라야 합니다.