외부 리뷰("현재 상태를 유지하기 위한 검증이 너무 많고, 그 검증 자체를
다시 검증하는 구조까지 생겼다")를 설계 문서로 정리하고 코드로 반영한다.
설계·판단 근거는 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>
11 KiB
테스트 전략 — 레벨 정의와 소스셋 매핑 (SSOT)
- 기준 일자: 2026-09-07
- 상태: 활성 문서. 아래 §3 표는 사람이 유지한다.
verifyTestSourceSetRegistry가 이 문서의 §3 표와 실제 Gradle 소스셋 선언의 불일치를 빌드 실패로 만든다. - 근거 리뷰:
docs/reviews/2026-09-07-app-bootstrap-module-code-review.md(BOOT-014, BOOT-015, BOOT-016)
1. 이 문서가 존재하는 이유
이 저장소는 이미 테스트 레벨 계약을 기계로 강제하고 있었다 —
TestTaxonomyArchitectureTest 가 contract/architecture 트리의 Testcontainers 의존을 금지하고,
slice 애노테이션 혼합을 막고, fixture 유출을 잡는다. 없던 것은 사람이 읽을 수 있는 정의였다.
그 결과 계약은 "패키지 이름"에만 걸려 있었고 "어느 소스셋이 컴파일하는가"에는 걸려 있지 않았다.
Testcontainers 를 쓰는 통합 테스트 9개가 app-bootstrap/src/test 안에 있었고, @Testcontainers 5개
중 가드가 있는 것은 하나뿐이었다. 즉 ./gradlew :app-bootstrap:test — 이 저장소가 leaf 별 기본
명령으로 권장하는 바로 그 명령 — 이 Docker 데몬을 요구했다 (BOOT-014).
그래서 규칙을 두 가지 방식으로 동시에 고정한다. 사람은 이 문서를 읽고, 빌드는 §3 표를 읽는다.
2. 레벨 정의
레벨은 **이름이 아니라 "어디까지 실제로 붙여서 검증하는가"**로 정의한다. smoke, regression,
acceptance 같은 말은 범위가 아니라 목적이라 레벨이 될 수 없다 — 하나의 E2E 테스트가 동시에
smoke 이고 regression 일 수 있다.
| 레벨 | 무엇을 검증 | 외부 시스템 | 소스셋 |
|---|---|---|---|
| unit | 클래스·함수·도메인 규칙 | 없음 | src/test |
| slice | 프레임워크 한 계층 | 인메모리/모의 | src/test |
| contract | 모듈 경계의 형태와 약속 | 없음 | src/test |
| architecture | 코드 의존 관계, 테스트 분류 자체 | 없음 | src/test |
| integration | 실제 인프라와의 연결 | 실제 DB/브로커/스토리지 | src/integrationTest 또는 leaf 전용 레인 |
| qualification | 벤더·프로토콜·배포 형상 | 실제 벤더 런타임 | leaf 전용 레인 |
| build-qualification | 빌드·조립 계약 자체 | 없음 (별도 클래스패스) | leaf 전용 레인 |
| performance | 지연·처리량 | 실제에 가까움 | leaf 전용 레인 |
2.1 소스셋을 나누는 기준은 하나다
테스트 코드는 production 패키지 구조를 그대로 미러링한다. 별도 소스셋으로 분리하는 것은 실행 환경·의존성·클래스패스가 달라지는 경우뿐이다.
unit/, service/, repository/, regression/ 같은 폴더는 만들지 않는다. 서로 다른 분류 축을
한 디렉터리에 섞으면 UserServiceTest 가 어디에 속하는지 아무도 답할 수 없게 된다. 이 저장소의
src/test 는 이미 production 패키지를 미러링하고 있으며 그 상태를 유지한다.
2.2 build-qualification 은 폴더 취향이 아니다
app-bootstrap 의 세 레인은 "테스트를 분류하려고" 나눈 것이 아니라 하나의 소스셋으로 표현할 수
없는 클래스패스 차이 때문에 존재한다. 합치면 검증 자체가 성립하지 않는다.
sampleOffTest—src/test와 같은 소스 파일을sample-portfolio없는 클래스패스로 다시 컴파일한다. "샘플을 지워도 템플릿이 성립하는가"의 증명이며, 같은 파일을 두 클래스패스로 컴파일하는 것이 그 정의다.conditionalTransportTest— GraphQL/gRPC/WebSocket 을 테스트 전용으로만 클래스패스에 올린다. 이 의존을testImplementation으로 옮기면 "기본 클래스패스에는 없다"는 증명 대상 명제가 그 순간 거짓이 된다.functionalTest— Gradle TestKit 이 별도 Gradle 빌드를 띄운다.
3. 소스셋 레지스트리 (기계 검증 대상)
이 표를 읽어 실제 sourceSets 선언과 대조하던 verifyTestSourceSetRegistry 는 2026-09에 삭제했다
(Markdown 표 파서였고, <!-- registry:begin --> 마커가 사라지면 계약이 산문으로 되돌아가는 것을
막으려고 마커 존재 자체까지 검사했다). 레인을 추가하면 이 표도 같이 고친다. 아래 옛 설명은 표를
어떻게 읽어야 하는지에 대한 기준으로 남긴다: 표에 없는 소스셋을
추가하거나 표에 있는 소스셋을 지우면 빌드가 실패한다.
| Gradle 경로 | 소스셋 | 레벨 |
|---|---|---|
:adapter:inbound:graphql |
testFixtures |
fixtures |
:adapter:inbound:web |
jettyCompatTest |
qualification |
:adapter:inbound:web |
nginxProxyTest |
qualification |
:adapter:inbound:web |
testFixtures |
fixtures |
:adapter:inbound:web |
webfluxContractTest |
qualification |
:adapter:inbound:websocket |
jettyWebSocketTest |
qualification |
:adapter:inbound:websocket |
nginxWebSocketTest |
qualification |
:adapter:inbound:websocket |
testFixtures |
fixtures |
:adapter:outbound:httpclient |
httpClientPerformanceTest |
performance |
:adapter:outbound:httpclient |
jmh |
performance |
:adapter:outbound:httpclient |
testFixtures |
fixtures |
:adapter:outbound:objectstorage |
objectStorageAwsQualificationTest |
qualification |
:adapter:outbound:objectstorage |
objectStorageMinioContractTest |
integration |
:adapter:outbound:objectstorage |
objectStorageMinioFaultTest |
integration |
:adapter:outbound:persistence-jpa |
jpaPlatformPerformanceTest |
performance |
:adapter:outbound:persistence-jpa |
postgresqlIntegrationTest |
integration |
:adapter:outbound:persistence-jpa |
testFixtures |
fixtures |
:adapter:outbound:persistence-mongo |
mongoPerformanceTest |
performance |
:adapter:outbound:persistence-mongo |
testFixtures |
fixtures |
:app-bootstrap |
conditionalTransportTest |
build-qualification |
:app-bootstrap |
functionalTest |
build-qualification |
:app-bootstrap |
integrationTest |
integration |
:app-bootstrap |
sampleOffTest |
build-qualification |
:messaging:messaging-kafka |
jmh |
performance |
:messaging:messaging-rabbit |
jmh |
performance |
:messaging:messaging-testkit |
jmh |
performance |
:sample-portfolio |
posterImageMigrationTest |
qualification |
:shared-contract |
edgeRateLimitContractTest |
contract |
src/test 는 모든 leaf 가 갖는 기본 소스셋이므로 표에 적지 않는다.
4. 판단표 — 새 테스트를 어디에 쓰는가
대상 코드가 정해지면 위치와 방식이 기계적으로 결정되어야 한다.
| 대상 | 레벨 | 협력자 | 위치 |
|---|---|---|---|
| 도메인 엔티티·값 객체 | unit | 없음 | 해당 leaf src/test |
| 유스케이스 | unit | 손으로 만든 Fake (Mockito 아님) | application-core/src/test |
시작 검증기 (*Validator) |
unit | MockEnvironment |
app-bootstrap/src/test |
@Configuration 조립 |
slice | ApplicationContextRunner |
app-bootstrap/src/test |
| 컨트롤러 | slice | @WebMvcTest + 모의 유스케이스 |
adapter/inbound/web/src/test |
| JPA 리포지토리 매핑 | integration | Testcontainers PostgreSQL | postgresqlIntegrationTest |
| 아웃박스·멱등성 행 수명주기 | integration | Testcontainers PostgreSQL | app-bootstrap/src/integrationTest |
| 브로커 발행/수신 | integration | 실제 브로커 | leaf 전용 레인 |
| 에러 응답 스키마 | contract | 없음 (스냅샷) | app-bootstrap/src/test/.../contract |
| 의존 방향·패키지 경계 | architecture | 없음 (ArchUnit) | app-bootstrap/src/test/.../architecture |
4.1 금지
src/test안에서org.testcontainers의존 —TestTaxonomyArchitectureTest가 막는다 (BOOT-014).- 필요 없는
@SpringBootTest. 조립을 검증할 것이 아니면ApplicationContextRunner나 순수 단위 테스트로 충분하다. - slice 애노테이션 혼합 (
@WebMvcTest+@DataJpaTest) — Spring 이 지원하지 않는다. - production 코드가 test fixture 에 의존하는 것.
- 픽스처를
TestUtil·CommonUtil같은 이름으로 묶는 것. 역할을 드러내는 이름 (fixture/,fake/,container/,assertion/) 을 쓴다.
5. 공용 테스트 지원 코드 — testFixtures
표준은 java-test-fixtures 하나다 (ADR-BUILD-001). 공용 테스트 지원 코드는
src/testFixtures/java 에 두고, 다른 leaf 는 testFixtures(project(':x')) 로 소비한다.
두 관례가 공존하던 상태(BOOT-015)는 해소됐다. ca.testkit-publisher 컨벤션 플러그인과 그것을 쓰던
testkit 소스셋 5개는 모두 이관됐고, 플러그인 자체도 제거됐다. 이관하면서 드러난 사실 하나는 기록해
둘 값어치가 있다: testkit* 구성이 testImplementation 을 상속했기 때문에 fixture 들은 각 leaf 가
선언한 모든 테스트 라이브러리를 말없이 보고 있었다. testFixturesImplementation 으로 옮기면서
그 표면이 드러났고, 다섯 leaf 에서 도합 30개가 넘는 의존을 명시적으로 적어야 했다.
test 가 아닌 lane 은 fixture 를 소비한다고 선언해야 한다 — java-test-fixtures 는 test 만
자동으로 배선한다:
strictTestLanes {
sourceSet('postgresqlIntegrationTest') { compilesAgainst 'main', 'testFixtures' }
}
디렉터리는 역할을 드러내는 형태를 권고한다 (fixture/, fake/, container/, assertion/).
TestUtil·CommonUtil 같은 무의미한 이름 묶음은 금지한다.
6. CI 단계 매핑
폴더만 나누고 CI 에서 한꺼번에 돌리면 분리의 의미가 없다.
커밋 / IDE → unit · slice · contract · architecture (`test`)
Pull Request → + integration (integration 레인)
머지 / 스테이징 → + build-qualification (functionalTest, sampleOffTest,
conditionalTransportTest)
야간 / 스케줄 → + qualification · performance
check 에는 인프라 레인을 붙이지 않는다. 이것은 이 저장소가 이미 따르고 있는 관례이며
(persistence-jpa 의 postgresqlIntegrationTest 도 check 에 붙어 있지 않다), Docker 없는
환경에서 check 가 실패하지 않게 하는 유일한 방법이다.
7. LLM 에이전트에게 적용할 때
이 저장소는 에이전트 협업을 전제로 설계되어 있다. 테스트 생성을 맡길 때는 다음 순서를 강제한다.
- 이 테스트가 §2 의 어느 레벨인지 판정하고 근거를 적는다.
- §3 표에서 해당 소스셋을 찾는다.
- 이미 존재하는 fixture 를 먼저 검색한다.
- 테스트를 작성한다.
- 판정한 레벨보다 큰 레벨로 작성하지 않았는지 확인한다 (
@SpringBootTest를 썼다면 왜 필요한지 설명할 수 있어야 한다).