Files
clean-architecture-backend-…/docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md
T
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

168 lines
11 KiB
Markdown

# 검증 표면 축소 설계 — 스켈레톤을 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
:<leaf>: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 <path>: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/문서/테스트를 같은 변경에서 고친다.