# 검증 표면 축소 설계 — 스켈레톤을 qualification framework에서 되돌리기 날짜: 2026-09-16 근거: 외부 리뷰 "현재 상태를 유지하기 위한 검증이 너무 많고, 그 검증 자체를 다시 검증하는 구조까지 생겼다" ## 0. 리뷰 기준점과 현재 체크아웃의 차이 리뷰는 이 저장소의 **이전 스냅샷**을 보고 작성됐다. 실제 작업 전에 항목별로 재측정했고, 이미 해결된 항목은 "완료"로 확정하고 남은 항목만 작업 대상으로 삼는다. | 리뷰 주장 | 리뷰가 본 값 | 현재 실측 | 판정 | | --- | --- | --- | --- | | `settings.gradle` 183줄 validator | 183줄 | 16줄 (`ca.architecture-registry` 설정 플러그인으로 이전) | 완료 | | 모듈 수 정확히 18개 강제 | 있음 | 없음 | 완료 | | runtime composition이 정확히 `app-bootstrap` | 있음 | `runtime_compositions`를 JSON에서 읽음 | 완료 | | JSON 필드 집합 정확히 일치 | 있음 | `ModuleRegistry.groovy:88,124`에 그대로 있음 | **작업 대상** | | `sample-portfolio` negative re-entry guard | 있음 | `ModuleRegistry.groovy:215`에 그대로 있음 | **작업 대상** | | `build-logic` 없음 | 없음 | 존재 (9개 convention plugin) | 부분 완료 | | version catalog 없음 | 없음 | `gradle/libs.versions.toml` 140줄 | 완료 | | `adapter/inbound/web/build.gradle` 799줄 OpenAPI | 799줄 | 256줄, codegen 없음 | 완료 | | leaf `check`가 저장소 전체 검사 | 그랬음 | 루트 `check`로 이미 이전 | 부분 완료 | | `fileserver-release.yml`의 `kubectl apply` | 있음 | 이미 제거됨 | 완료 | | `httpclient-release.yml` | 있음 | 파일 자체가 없음 | 해당 없음 | | `ci-gate-matrix.yml` 282줄 / 37 gate | 282줄 | **1,025줄 / 107 gate** | **작업 대상(악화)** | | `verify-gate-matrix.sh` | 있음 | 568줄 | **작업 대상** | | `verify-gradle-wrapper.sh` 740줄 | 740줄 | **799줄** | **작업 대상** | | `DeveloperExperienceContractTest` 1,100줄 | 1,100줄 | 1,141줄 (CI YAML mutation test 25개) | **작업 대상** | | `src/build.gradle` 2,469줄 | 2,469줄 | **3,211줄** | **작업 대상(악화)** | | always-fail Messaging task | 있음 | 9개 그대로 | **작업 대상** | | 모든 빌드에 Git SHA 강제 | 있음 | 그대로 (`build.gradle:47`) | **작업 대상** | ## 1. 채택하는 판단 기준 리뷰의 핵심 원칙을 이 저장소의 결정 규칙으로 승격한다. 1. **현재 상태(Current State)가 아니라 불변조건(Invariant)을 검증한다.** "모듈이 N개다", "필드가 정확히 이 집합이다", "문서에 적힌 수가 레지스트리와 같다"는 현재 상태다. "ID가 중복되지 않는다", "domain이 framework를 참조하지 않는다"는 불변조건이다. 2. **검증기를 검증하지 않는다.** validator를 mutation해서 validator가 실패하는지 보는 task는 스켈레톤의 기본 빌드 정책이 아니다. 3. **자동으로 구성할 수 있는 것은 검증으로 강제하지 않는다.** convention plugin으로 주입한다. 4. **로컬 `check`는 로컬이어야 한다.** leaf의 `check`는 그 leaf만 검사한다. 5. **릴리스 불변조건을 일반 개발 빌드에 강제하지 않는다.** 6. **문서 drift는 빌드 실패 사유가 아니다.** 커스텀 Markdown/Java 파서를 유지하지 않는다. 7. **GitHub Actions = CI + artifact 생산, Argo CD = CD.** CI에 클러스터 배포 자격증명을 넣지 않는다. 8. **Template maintainer용 검증과 Template consumer용 검증을 분리한다.** 이 기준은 기존의 D8 결정("quality 블록을 convention plugin으로 빼지 않는다")을 **대체한다**. D8의 3번 근거(build-logic이 플러그인 버전을 두 번 선언하게 된다)는 이미 무효다 — `build-logic/settings.gradle`이 메인 빌드의 `libs.versions.toml`을 읽고 있으므로 버전은 한 곳에 있다. ## 2. 목표 task 계층 ```text ::check 컴파일 + 단위 테스트 + spotless + checkstyle + errorprone (그 leaf만) check (root) 모든 leaf의 check architectureCheck 의존 방향 · 런타임 멤버십 · application-core 순수성 · Git 미추적 패키지 qualityCheck SpotBugs + FindSecBugs (전 leaf) configContractCheck :app-bootstrap:verifyEnvKeys integrationCheck 통합/슬라이스 레인 ci check + architectureCheck + qualityCheck + configContractCheck releaseCheck ci + 아카이브 위생 + public path snapshot + 릴리스 provenance ``` qualification(JPA readiness, Messaging evidence, notification evidence, transport 등)은 어느 것도 `check` / `ci`에 걸지 않는다. 명시적으로 이름을 불러야 실행된다. ## 3. 변경 목록 ### 3.1 삭제 | 대상 | 줄 수 | 이유 | | --- | ---: | --- | | `.github/ci-gate-matrix.yml` | 1,025 | Gradle task graph와 workflow graph에 이미 있는 정보의 3중 복제 | | `.github/scripts/verify-gate-matrix.sh` | 568 | 위 복제본의 정합성 검사기 | | `.github/scripts/verify-gradle-wrapper.sh` | 799 | workflow 바이트 해시 잠금. 공격자는 해시도 같이 고치면 되고, 개발자는 주석 하나에 해시를 갱신해야 한다 | | `DeveloperExperienceContractTest`의 wrapper/gate mutation test 25개 | ~700 | 애플리케이션 test suite가 GitHub Actions YAML 파서를 검증 | | always-fail Messaging skeleton task 9개 | ~85 | 정상 입력으로도 성공할 수 없는 task. TODO를 Gradle API로 만든 것 | | `verifyReadmeCommands` | 105 | 커스텀 Markdown 명령 파서 | | `verifyRunbookReferences` | 75 | 커스텀 runbook 식별자 파서 | | `verifyDocumentedLeafCount` | 78 | 문서에 적힌 leaf 수 = 전형적인 현재 상태 검증 | | `verifyTestSourceSetRegistry` | 92 | 문서 표 ↔ source set 대조 파서 | | `verifySpotBugsAnalysisFailureContract` | 58 | 검증기의 검증 | | `verifyConfigurationPropertiesProcessor` | 90 | build.gradle을 regex로 읽는 검증 → convention으로 대체 | | `verifyOneTypePerFile` | 8 | 이미 `checkstyleMain` 별칭. 호출자를 `checkstyleMain`으로 바꾸고 이름 폐기 | | `verifyTrivyignore` | 105 | 빈 registry를 지키는 커스텀 YAML 파서 | | `verifyQuarantineSunset` | 250 | 빈 registry를 지키는 커스텀 YAML + Java 파서 | | `blankJavaCommentsAndLiterals` | 95 | 위 두 개만 쓰던 Java 렉서 흉내 | | `ModuleRegistry`의 필드 집합 정확 일치 · sample-portfolio negative guard | ~25 | 확장 차단 · 삭제된 모듈의 역사가 영구 invariant | 합계 약 4,150줄. ### 3.2 이동 | 대상 | 현 위치 | 새 위치 | 이유 | | --- | --- | --- | --- | | java/quality/spring 공통 설정 | `build.gradle`의 `configure(subprojects…)` | `ca.java-conventions` · `ca.quality-conventions` · `ca.java-library` · `ca.spring-library` | 모듈이 자신의 성격을 스스로 선언 | | `verifyCleanArchitectureDependencies` 외 3개 | `build.gradle` | `ca.architecture` | 아키텍처 규칙을 한 곳에 | | JPA readiness registry + release gate | `build.gradle` ~610줄 | `gradle/qualification/jpa-qualification.gradle` | 빌드 정책과 certification 분리 | | Messaging evidence manifest | `build.gradle` ~600줄 | `gradle/qualification/messaging-qualification.gradle` | 동일 | | `verifyEnvKeys` | 루트 task, 루트 `check` | `:app-bootstrap` 소유, `configContractCheck` | 환경 계약은 composition root의 책임 | | 모듈 의존 edge 존재/자기참조 검사 | settings 단계(`ModuleRegistry`) | `verifyCleanArchitectureDependencies` | settings에서 죽으면 복구 수단이 없다 | ### 3.3 완화 | 대상 | 현재 | 변경 후 | | --- | --- | --- | | Git revision | 없으면 **모든** 빌드가 configuration 단계에서 실패 | 일반 빌드는 `0.0.1-SNAPSHOT`/`unknown`. `releaseCheck`·아카이브 생성에서만 요구 | | SpotBugs / FindSecBugs | 전 leaf `check` 블로킹 | `qualityCheck` (CI lane). 로컬 `check`에서 제외 | | `.trivyignore.yaml` | 커스텀 파서가 expiry/reason 강제 | 파일은 유지, 규칙은 문서화 + CODEOWNERS 승인 | | `flaky-quarantine.yaml` | 커스텀 파서 + 14일 sunset 강제 | 레지스트리 삭제. `@Tag("quarantine")` 제외와 `quarantineTest`는 유지(각 3줄) | ### 3.4 CI ```text .github/workflows/ ├── _reusable-gradle.yml 신규 — checkout + wrapper validation + JDK/캐시 + Gradle 호출 ├── ci-quality-gates.yml → 재사용 workflow 호출로 축약 ├── dependency-vulnerability.yml 유지 (dependency-review + submission + Trivy) ├── link-check.yml 유지 ├── release.yml image/SBOM 생산까지. 클러스터 배포 없음 ├── fileserver-certification.yml ← fileserver-release.yml 개명 (CD가 아니라 certification) └── 나머지 feature qualification 유지, 전부 재사용 workflow 사용 ``` wrapper 검증은 `gradle/actions/wrapper-validation`(full SHA 핀)에 맡기고, 재사용 workflow 한 곳에서만 선언한다. full SHA 핀은 리뷰 판단대로 **유지**한다. ### 3.5 CI/CD 경계 ```text GitHub Actions ──► test / scan / image build / SBOM / push ──► GitOps repo manifest ──► Argo CD ──► K8s ``` `docs/ci-cd/boundary.md`로 고정한다. GitHub Actions는 `kubectl apply` / `helm upgrade` / `argocd app sync`를 하지 않는다. Argo CD auto-sync를 쓰면 CI에 클러스터 자격증명이 필요 없다. ### 3.6 Template maintainer vs consumer `docs/ci-cd/template-vs-consumer.md`로 구분을 명시한다. - Template CI: sample 모듈 제거 가능성, optional 모듈 조합 빌드, 레지스트리 확장 가능성 - Consumer CI: 자기 애플리케이션의 test / architecture / security / release 파생 프로젝트가 가져가면 안 되는 workflow와 task를 목록으로 적는다. ## 4. 유지하는 것 (리뷰가 "잘한 것"으로 분류) `verifyCleanArchitectureDependencies`, dependency locking(STRICT), full SHA action 핀, dependency-review, Trivy 스캔, path filter 기반 feature CI, nightly 분리, Spotless, `-Werror`/`-Xlint`, ErrorProne, 재현 가능한 아카이브. ## 5. 검증 방법 - `./gradlew help --offline`로 configuration 성공 - 변경한 leaf마다 `./gradlew :check --offline` - `./gradlew architectureCheck --offline` - 워크플로 YAML은 `python3 -c "import yaml…"`로 파싱 확인 - 삭제한 task 이름이 저장소 어디에도 남지 않았는지 `grep` ## 6. 명시적 위험 1. leaf 62개에 `plugins {}` 블록을 추가한다. 적용 순서가 바뀌므로 leaf별 `check`로 확인한다. 2. dependency locking이 STRICT라, 어떤 leaf의 configuration에 의존성이 추가되면 락 파일이 깨진다. 따라서 convention 이동은 **해석되는 의존성 집합을 바꾸지 않는 범위**로 제한한다. `ca.spring-config`는 이미 processor를 선언한 leaf만 opt-in한다. 3. 삭제하는 task 이름을 참조하는 workflow/문서/테스트를 같은 변경에서 고친다.