Files
clean-architecture-backend-…/docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md
T
2026-09-17 15:03:36 +09:00

18 KiB

검증 표면 축소 설계 — 스켈레톤을 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.ymlkubectl 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 계층

:<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.gradleconfigure(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

.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 경계

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/문서/테스트를 같은 변경에서 고친다.

실행 결과 (2026-09-16)

실제로 한 것

Gradle

파일
src/build.gradle 3,211줄 349줄
src/settings.gradle 16줄 16줄 (변경 없음)
src/build-logic/src/main/groovy/ 9 플러그인 16 플러그인

루트에서 사라진 것과 간 곳:

대상 처리
configure(subprojects) 306줄 ca.java-conventions + ca.quality-conventions + ca.java-library / ca.spring-library / ca.spring-config / ca.jmh-benchmarks
verifyCleanArchitectureDependencies, verifyApplicationCoreDependencyPurity, verifyNoIgnoredSourcePackages ca.architecture (루트 적용) + architectureCheck
JPA readiness registry + release gate 685줄 gradle/qualification/jpa-qualification.gradle
Messaging evidence 300줄 gradle/qualification/messaging-qualification.gradle
verifyEnvKeys 280줄 gradle/config-contract.gradle, :app-bootstrap이 적용, configContractCheck
always-fail Messaging 태스크 9개 삭제 → docs/roadmap/messaging-r2.md
verifyReadmeCommands, verifyRunbookReferences, verifyDocumentedLeafCount, verifyTestSourceSetRegistry, verifyDocumentationContracts 삭제
verifyConfigurationPropertiesProcessor + blankJavaCommentsAndLiterals 삭제 → ca.spring-config
verifyOneTypePerFile 삭제 → 호출자가 checkstyleMain을 직접 부른다
verifyTrivyignore, verifyQuarantineSunset 삭제. flaky-quarantine.yaml도 삭제
verifySpotBugsAnalysisFailureContract 삭제. 검사기 본체(spotBugsAnalysisFailures)는 ca.quality-conventions로 이동

ModuleRegistry: 필드 집합 정확 일치 → 필수 필드 존재로 완화. self-dependency / 미지의 id / sample-portfolio edge 검사는 settings에서 verifyCleanArchitectureDependencies로 이동.

Git revision: 없으면 configuration 실패 → 0.0.1-SNAPSHOT + sourceRevision='unknown'. verifyReleaseProvenancereleaseCheck에서만 요구한다.

CI

대상 처리
.github/ci-gate-matrix.yml (1,025줄 / 107 gate) 삭제
.github/scripts/verify-gate-matrix.sh (568줄) 삭제
.github/scripts/verify-gradle-wrapper.sh (799줄) 삭제 → gradle/actions/setup-gradle의 기본 wrapper validation
잡마다 반복되던 wrapper-validation 3줄 블록 59개 composite action 안으로 이동
_reusable-gradle.yml 신규. 단순 Gradle 잡이 호출
fileserver-release.yml fileserver-certification.yml로 개명
docs/ci-cd/boundary.md, docs/ci-cd/template-vs-consumer.md 신규

테스트

대상
DeveloperExperienceContractTest 1,141줄 (wrapper mutation 24개) 379줄
ConditionalTransportQualificationContractTest 860줄 (gate matrix 18개) 94줄
MessagingCapabilityRegistryContractTest 태스크 존재를 요구 태스크 부재를 요구

SampleRemovalSmokeContractTest의 gate matrix 대조 제거.

리뷰와 다르게 결정한 것

  1. ci-quality-gates.ymlci.yml로 개명하지 않았다. 리뷰 §25는 워크플로 이름 정리를 제안했지만, 같은 절에서 "중요한 것은 workflow 개수가 아니라 공통 setup을 복사하지 않는 것"이라고 했다. 개명은 문서·테스트 20여 곳을 건드리고 얻는 것이 이름뿐이다. fileserver-release.yml만 개명했다 — 그건 이름이 틀렸기 때문이다(릴리스하지 않는다).
  2. ca.spring-config는 opt-in이다. 리뷰 §14는 convention이 자동으로 processor를 넣는 그림을 보여 주지만, 모든 configuration이 STRICT로 락되어 있어서 지금 선언하지 않은 leaf에 넣으면 락이 깨진다. 지금 선언한 15개 leaf가 명시적으로 적용한다.
  3. integrationCheckci에 넣지 않았다. 리뷰 §10의 계층은 `ci = check + architectureCheck
    • integrationCheck지만, 이 저장소의 통합 레인 상당수는 컨테이너 런타임이 필요하고 이미 integration-main.yml`(main push)과 nightly로 분리돼 있다. PR 게이트에 Docker를 요구하면 리뷰가 비판한 "로컬에서 돌릴 수 없는 check"가 된다.
  4. verifyRuntimeModuleMembership은 단순화하지 않았다. 리뷰는 "현재보다 중복"이라 봤지만, 현재 구현은 선언된 멤버십이 아니라 해석된 runtime closure를 비교한다 — 다른 검증이 답하지 않는 질문이다. 중복이었던 85줄은 이미 이전 작업에서 제거돼 있었다.
  5. springDependencyManagement를 1.1.6 → 1.1.7로 올렸다. 리뷰에 없는 항목이다. build-logic이 같은 플러그인을 적용해야 하는데, 루트는 Spring Boot 플러그인 때문에 이미 1.1.7로 해석하고 있었다(buildEnvironment1.1.6 -> 1.1.7로 표시). 카탈로그가 아무도 해석하지 않는 버전을 적고 있었고, 그대로 두면 convention plugin과 leaf가 서로 다른 버전을 쓰게 된다.

검증

./gradlew -p build-logic test              BUILD SUCCESSFUL (56 tests)
./gradlew verifyDependencyLocks            BUILD SUCCESSFUL (62 leaf, 락 변화 없음)
./gradlew architectureCheck                BUILD SUCCESSFUL
./gradlew configContractCheck              BUILD SUCCESSFUL
./gradlew ci releaseCheck integrationCheck --dry-run   BUILD SUCCESSFUL
./gradlew check --continue                 :sample-portfolio:test 3건만 실패 (아래)
./gradlew :app-bootstrap:test              BUILD SUCCESSFUL
./gradlew :app-bootstrap:functionalTest    BUILD SUCCESSFUL
python3 -c "yaml.safe_load(...)"           workflow 20개 + composite action 파싱 OK

락 파일이 한 줄도 바뀌지 않았다는 것이 convention 이동의 핵심 근거다 — 해석되는 의존성 집합이 그대로라는 뜻이다.

이번 작업으로 드러난 기존 결함

셋 다 HEAD(d00c762)에서 동일하게 재현되며, 이번 변경이 원인이 아니다.

  1. :sample-portfolio:test 3건 실패. JpaLiveEventReplayAdapter@Repository인데 생성자 3번째 파라미터 java.time.Duration을 만족시킬 빈이 없다. 샘플의 @ComponentScan("dev.caskeleton") 이 이 어댑터를 집어오고, app-bootstrap은 이 타입을 아예 참조하지 않는다. retention을 어디서 받을지(typed settings)는 설계 결정이라 이번 빌드 리팩터링에서 건드리지 않았다. 미해결.
  2. :adapter:outbound:messaging:verifyJsonSchemaRuntimeGraph — 한 번도 통과한 적 없음. libs.* 카탈로그 accessor(Provider)를 String으로 받는 클로저에 넘겨서 실행 즉시 MissingMethodException으로 죽었다. check가 이 태스크에 의존했지만 항상 앞선 실패가 먼저 빌드를 멈췄다. 고쳤고, 고치자 진짜 문제가 드러났다 — 카탈로그는 jackson-core:3.0.2를 적는데 Jackson BOM은 3.1.5로 해석한다. 버전 고정은 현재 상태 검증이므로 모듈 존재 검사로 바꿨다.
  3. BuildVerificationPurityContractTest 5건 실패. public path snapshot의 입력이 src/.env에서 커밋된 config/security.yml로 옮겨졌는데(src/.env*는 gitignore라 CI 체크아웃에 없다) 픽스처는 계속 .env를 썼다. 스크립트가 맞고 테스트가 이전 계약을 설명하고 있었다. 고쳤다.