Files
clean-architecture-backend-…/docs/testing/TESTING_STRATEGY.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

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 의 세 레인은 "테스트를 분류하려고" 나눈 것이 아니라 하나의 소스셋으로 표현할 수 없는 클래스패스 차이 때문에 존재한다. 합치면 검증 자체가 성립하지 않는다.

  • sampleOffTestsrc/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-fixturestest 만 자동으로 배선한다:

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-jpapostgresqlIntegrationTestcheck 에 붙어 있지 않다), Docker 없는 환경에서 check 가 실패하지 않게 하는 유일한 방법이다.

7. LLM 에이전트에게 적용할 때

이 저장소는 에이전트 협업을 전제로 설계되어 있다. 테스트 생성을 맡길 때는 다음 순서를 강제한다.

  1. 이 테스트가 §2 의 어느 레벨인지 판정하고 근거를 적는다.
  2. §3 표에서 해당 소스셋을 찾는다.
  3. 이미 존재하는 fixture 를 먼저 검색한다.
  4. 테스트를 작성한다.
  5. 판정한 레벨보다 큰 레벨로 작성하지 않았는지 확인한다 (@SpringBootTest 를 썼다면 왜 필요한지 설명할 수 있어야 한다).