refactor(build,ci): CI를 단계별로 재편하고 감사 잔여 114건 정리

빌드·CI 레이어 전수 감사(133건) 중 Track A 에서 처리한 E등급 19건을 뺀
나머지를 마무리한다. 한 커밋인 이유는 서로 얽혀 있기 때문이다 — 워크플로가
`checkstyleMain` 을 부르려면 루트가 먼저 Checkstyle 을 붙여야 하고, 모듈 43개가
convention 을 적용하려면 build-logic 이 먼저 그 플러그인을 등록해야 한다.
영역별로 쪼개면 중간 커밋이 빌드되지 않는다.

## CI 단계 분리 (워크플로 29파일 3,360줄 → 19파일 2,692줄, 잡 69 → 64)

모듈이 아니라 단계로 자른다. 기존 28개는 `web-*` `jpa-*` 처럼 모듈로 갈려
있어서 같은 일이 파일마다 중복됐다.

- stage 1 `ci-quality-gates.yml` + `pr-adapters.yml`(신규, 잡 단위 경로 필터) — pull_request
- stage 2 `integration-main.yml`(신규) — push:main + 03:00. 문서 게이트는 여기에 둔다
- stage 3 `release.yml`(신규, 릴리스 워크플로 7개 중 5개 흡수) — push: tags v*

setup 블록 59회 복붙 → `.github/actions/setup-gradle-java` 1개(잡당 13줄 → 5줄).
잡 8개 삭제, 각각 대체 잡을 확인했다. `verifyCleanArchitectureDependencies` 실행
횟수가 태그당 9 → 6, PR당 8 → 4 로 줄었다.

## 컨테이너 릴리스 신설

이미지를 만드는 것이 아무것도 없었다. Dockerfile 은 있었지만
build-push-action / bootBuildImage / jib 사용처가 0건이고, `*-release.yml` 8개는
테스트 후 아티팩트만 올렸다 — 이름만 릴리스였다.

Boot 레이어드 추출 + thin-JAR 엔트리포인트로 Dockerfile 을 고치고 릴리스
워크플로에 이미지 빌드·푸시·SBOM·스캔을 넣었다. 로컬 빌드로 검증했다:
레지스트리 content 241MB, 기동 3.7초, uid 1000, 헬스체크 200.
코드만 바뀐 릴리스는 7.68MB 만 재푸시한다(이전이라면 156MB).
CI 는 배포하지 않는다 — 매니페스트와 ArgoCD 는 별도 repo 로 간다.

## 게이트 정리

- gate-matrix 의 개수 고정 해제: `EXPECTED_GATE_COUNT=49` 와 하드코딩된 49개 id
  목록을 지우고 불변식으로 대체(필드·enum, 워크플로/잡 실재, id 중복,
  `release_blocking: true` 는 실제로 release-gate 의 needs 여야 함).
  행을 추가하려면 테스트부터 고쳐야 하던 구조를 풀었다. 커버리지 8/28 → 28/28
- 문서 게이트 4개를 `check` 에서 떼어 `verifyDocumentationContracts` 로 묶고
  stage 2 에 배치. 어겨도 런타임은 멀쩡하므로 개발을 막지 않는다
- `verifyOneTypePerFile`(정규식 Java 파싱, 126파일 미탐) → Checkstyle
  `OneTopLevelClass` + `OuterTypeFilename`. main 위반 0건, test 의 fixture 29건은
  정책을 넓히지 않고 suppressions 에 사유와 함께 명시 제외
- leaf 하나의 `check` 가 끌고 오던 저장소 전역 게이트 18개를 재배치.
  `:domain-core:check` 가 13 태스크 11초로 끝난다
- convention 플러그인 2개 신설(`ca.platform-module`, `ca.grpc-platform-module`),
  플랫폼 모듈 43개에 적용. 손수 짠 Test 태스크 17개를 `strictTestLanes` 로 전환
  (태스크 이름 전부 보존 — CI 가 이름으로 부른다)
- `ca.api-surface` 의 정규식 Java 파서를 javac parse-only 로 교체
  (기존 베이스라인 3개와 바이트 동일 확인)
- 죽은 태스크 5개 삭제, `src/gradle` 1,713 → 1,440줄, 모듈 build.gradle
  3,072 → 2,977줄

## 검사가 검사를 못 하고 있던 것들

- 11개 계약 테스트가 gitignore 된 `src/.env` 를 요구했다. `.gitignore` 자신이
  "examples beside it are the tracked contract, never a real one" 이라고 적어둔
  규칙과 어긋난다. 깨끗한 체크아웃에는 그 파일이 없으므로 CI 에서 돌 수 없었다.
  추적되는 `.env.example` 로 돌린다
- **`.env.local.example` 이 5432 를 가리키는데 compose 는 5433 을 게시한다.**
  이 파일을 복사해 시작하는 신규 개발자는 DB 연결에 실패한다. 이걸 잡으라고
  만든 테스트가 추적 안 되는 파일을 읽어서, 이미 설정이 끝난 머신에서만 돌고
  정작 처음 받는 사람에겐 아무 검사도 안 하고 있었다. 포트를 고치고 테스트를
  추적 파일로 돌렸다
- `MongoModuleBoundaryTest` 의 `DO_NOT_INCLUDE_JARS` 때문에 임포트가 0개가 되어
  규칙 10개가 "failed to check any classes" 로 실패 중이었다. 이 레인에서는
  모듈 자기 클래스가 jar 로 올라온다. `importPackages(ROOT)` 가 이미 서드파티를
  거르므로 옵션은 불필요했다
- `ReleaseManifestTaskExistenceTest` 가 build 파일 텍스트에서 `tasks.register(`
  만 찾아, convention 의 `lane('...')` 로 바뀐 태스크를 미등록으로 오판했다
- `ProfileSeparationContractTest` 는 런처가 주입하는 `src/.env` 가 맞는 대상이라
  그대로 두되, 파일이 없으면 명시적으로 skip 한다 — "안 돌았다" 가 "통과했다"
  로 읽히지 않게

## 검증 (전부 깨끗한 체크아웃에서, 커밋 전에 실행)

`verify-gradle-wrapper.sh` PASS · `verify-gate-matrix.sh` OK(drift 0) ·
워크플로 YAML 전수 파싱 OK · actionlint 지적 0 · `gradlew help` ·
`verifyCleanArchitectureDependencies` · `build-logic test` ·
`:app-bootstrap:test` **1001 tests 실패 0 스킵 5** · `:domain-core:check` ·
`verifyDocumentationContracts`.

## 남은 문제

- 첫 `v*` 태그는 이미지 취약점 스캔에서 실패한다(CRITICAL/HIGH 9건:
  ubuntu 베이스 2, tomcat-embed-core 3, amqp-client 3, httpcore5 2).
  억제를 넣지 않았다 — 릴리스 1회차를 초록으로 만들려고 임계값을 내리면
  게이트가 장식이 된다. 의존성·베이스 갱신이 선행돼야 한다
- `fileserver-v*` / `web-v*` / `websocket-v*` 태그는 이제 아무 run 도 만들지
  않는다(배포 단위가 하나라는 결정에 따른 것)
- main push 마다 무거운 레인 3개가 새로 돈다 — 러너 분이 늘어난다
- `ProfileSeparationContractTest` 가 찾아낸 4개 값(cache command-timeout,
  cache positive-soft-ttl, idempotency provider, rate-limit command-timeout)이
  `.env.example` 과 인라인 기본값 사이에서 갈린다. 런타임 설정 판단이라
  건드리지 않았다

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-16 18:27:15 +09:00
co-authored by Claude Opus 5
parent 1535481794
commit 9bc2e75fe5
132 changed files with 4002 additions and 3083 deletions
@@ -32,6 +32,8 @@ dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.Notification
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.ProviderRuntimeAssembler
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.ProviderType
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.SmtpProviderRuntimeAssembler
dev.caskeleton.adapter.outbound.notification.platform.callback.MapProviderCallbackAdapterRegistry
dev.caskeleton.adapter.outbound.notification.platform.callback.MapProviderEventProjectorRegistry
dev.caskeleton.adapter.outbound.notification.platform.dispatch.AttemptPermit
dev.caskeleton.adapter.outbound.notification.platform.dispatch.CapabilityReconciliationGateway
dev.caskeleton.adapter.outbound.notification.platform.dispatch.ConfiguredProfileCatalog
@@ -57,6 +59,7 @@ dev.caskeleton.adapter.outbound.notification.platform.dispatch.SingleTenantConte
dev.caskeleton.adapter.outbound.notification.platform.dispatch.UuidV7Generator
dev.caskeleton.adapter.outbound.notification.platform.observation.LoggingNotificationAudit
dev.caskeleton.adapter.outbound.notification.platform.observation.LoggingNotificationMetrics
dev.caskeleton.adapter.outbound.notification.platform.observation.MicrometerNotificationMetrics
dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationHealthReporter
dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationHealthSnapshot
dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationServingThresholds
@@ -1,7 +1,7 @@
# CI 단계 분리 + 컨테이너 릴리스 도입
- 작성: 2026-09-16
- 상태: Track A 진행 중 / Track B·C 착수 대기
- 상태: Track A 완료(커밋 3개) / Track B 진행 중 / Track C 대기
- 근거 감사: 빌드·CI 레이어 전수 리뷰 133건 (파일 110개 / 12,800줄)
## 확정된 결정
@@ -106,3 +106,65 @@
- finding 전체(243KB, `file:line` 근거): `scratchpad/gradle-audit/findings/R1~R8.md`
- 등급 분포: A=25 B=36 C=17 D=31 E=24 · 정리 시 3,368줄 감소 추정
- 아키텍처 위반 0건 (`modules.json` 전수 대조, messaging/grpc 격리 확인)
## 진행 기록
### Track A — 완료 (2026-09-16)
커밋 `2a8d34f` docs / `e345191` fix(ci) / `1535481` refactor(build,src).
E등급 19건 처리. 깨끗한 worktree 체크아웃에서 검증:
`./gradlew help` 통과, 아키텍처 게이트 20개 클래스 174 tests 실패 0 스킵 0,
`-p build-logic test` 통과, `verify-gradle-wrapper.sh` PASS,
`verify-gate-matrix.sh` OK.
감사 범위 밖이었으나 main 이 실제로 깨져 있던 것 두 건도 함께 고쳤다:
`src/gradle/libs.versions.toml``app-bootstrap config/*.yml` 15개가
git 에 없어 깨끗한 체크아웃에서 빌드가 불가능했다.
**절차 교훈**: 커밋을 4번 시도해 전부 되돌렸다. 원인은 깨끗한 체크아웃 검증을
커밋 ** 에 한 것. 이후로는 워킹트리를 커밋하지 않고 후보 커밋 객체로 만들어
새 체크아웃에서 검증한 뒤에만 커밋한다.
### 감사 findings 중 서브에이전트가 반박해 기각한 것
리뷰 결과를 그대로 집행하지 않는다. 수정 담당이 근거를 들어 반박한 건은 기각한다.
- `runtimeClasspathManifest` "출력 미사용" — 거짓. `RuntimeMembershipClasspathAgreementTest`
읽고 `app-bootstrap/build.gradle:415``test dependsOn` 이 걸려 있다
- JUnit 태그 3중 재설정 "충돌" — Gradle 9.0.0 에서 `useJUnitPlatform{}` 은 누적된다(실측)
- `persistence-jpa:226` outbound→inbound — 클래스패스가 아닌 태스크 엣지라 게이트 범위 밖
- `ca.api-surface` "소비자 없음" — 거짓. `ci-gate-matrix.yml:90-100`
`verify-gate-matrix.sh` 가 소비한다. 유지하고 정규식 렌더러만 javac 파싱으로 교체
- `ca.dependency-policy` "의존성 잠금이 대체한다" — 거짓. 락파일은 무엇이 있는지를
기록할 뿐 무엇이 없어야 하는지를 막지 않고, `--write-locks` 는 추가를 조용히 수용한다
- `ca.runtime-membership``moduleRegistryRepositoryRoot` "미사용" — 거짓.
`RuntimeMembershipFunctionalTest.java:123` 이 쓴다
### 아키텍처 게이트가 실제로 뭘 검사하는지 측정 (2026-09-16)
`allowEmptyShould(true)` 가 96곳에 있어 "규칙이 클래스 0개를 검사하고 초록으로
통과하는 것 아니냐"를 의심했다. 추측 대신 측정했다 — 스크래치 worktree 에서
96곳을 전부 `false` 로 뒤집고 아키텍처 스위트를 돌렸다.
**174개 중 3개만 실패했다.** 93곳의 억제는 실제로 클래스를 검사하는 규칙에
방어적으로 붙어 있었다. 비어 있는 3개는 전부 "이 구조를 추가하면 이 규칙을
지켜라" 형태의 선행 가드이고, 템플릿이라 아직 해당 구조가 없다:
- `AGGREGATE_ROOT_SETTERS_ARE_NOT_PUBLIC``set*` 를 가진 `@AggregateRoot` 없음
- 테넌트 스코프 리포지토리 없음
- `CrudRepository` 를 재구현한 프로덕션 타입 없음
결론: 아키텍처 게이트는 속 빈 게이트가 아니다. 의심이 틀렸다.
### HEAD 에서 발견된 실제 실패 2건 (감사 findings 밖)
1. `MessagingCapabilityRegistryContractTest``src/build.gradle` 의 **소스 문자열
6개**를 assert 했다. 결과에 영향 없던 검증 45줄을 지우자 구현이 아니라 테스트가
먼저 깨졌다. 계약 검사로 교체: 모든 스켈레톤이 공용 가드를 통과하는지와,
그 가드가 실제로 throw 하는지만 본다.
2. `MongoModuleBoundaryTest``DO_NOT_INCLUDE_JARS` 때문에 임포트가 0개가 되어
규칙 10개 전부가 "failed to check any classes" 로 실패하고 있었다. 이 레인에서는
모듈 자기 클래스가 jar 로 클래스패스에 올라온다. 옵션 제거로 해결(실험으로 확인).
`importPackages(ROOT)` 가 이미 서드파티를 걸러내므로 옵션은 불필요했다.
-1
View File
@@ -71,7 +71,6 @@ smoke 이고 regression 일 수 있다.
| `:adapter:inbound:web` | `nginxProxyTest` | qualification |
| `:adapter:inbound:web` | `testFixtures` | fixtures |
| `:adapter:inbound:web` | `webfluxContractTest` | qualification |
| `:adapter:inbound:websocket` | `brokerRelayTest` | integration |
| `:adapter:inbound:websocket` | `jettyWebSocketTest` | qualification |
| `:adapter:inbound:websocket` | `nginxWebSocketTest` | qualification |
| `:adapter:inbound:websocket` | `testFixtures` | fixtures |