Files
llm-wiki/raw/branch-notes/feature-test-taxonomy-fixture-contract.md

462 lines
52 KiB
Markdown

---
title: branch / feature-test-taxonomy-fixture-contract
source_type: branch-note
status: raw
branch: feature-test-taxonomy-fixture-contract
parent_branch:
related_projects: [ca-skeleton]
governing_docs: [wiki/projects/ca-tmpl/skeleton-governance-registry-verification-test-scorecard, wiki/projects/ca-tmpl/sample-fixture-and-adoption]
tags: [branch, ca-skeleton, test, taxonomy, fixture]
created: 2026-05-22
target_merge:
status_label: review
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-042
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-042
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-TESTCONTAINERS-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-TEST-001@1]
refines: []
overrides: []
depends_on: []
contract_packet: 1
contract_packet_sha256: df97c653e6bc2a4b42673993d1881ecd4a683c40c133c115f9f041fef42add59
---
# branch: feature-test-taxonomy-fixture-contract
> Layer: `raw/branch-notes/` — unit/contract/architecture/slice/integration/smoke 테스트의 책임과 fixture 사용 기준을 정의합니다.
<!-- section-id: branch-parent -->
## 부모 (필수)
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§29 G-G Test taxonomy · §17/§22 Sample fixture) 의 결정/근거/금지 사항을 정제한다.
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: test level별 fixture가 실행되고 container 사용 정책을 지킨다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-TESTCONTAINERS-001@1` | Testcontainers는 persistence/outbound integration test에 사용하고 unit·architecture·contract test에서는 금지한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-TEST-001@1` | test framework는 JUnit 5다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
테스트가 많아도 실패 원인을 구분할 수 없으면 실무 skeleton으로 부족합니다. 이 branch는 어떤 계약을 어떤 테스트 레벨에서 잡을지 고정하고, sample-portfolio과 fixture가 테스트를 오염시키지 않게 합니다.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- unit test 기준.
- contract test 기준.
- architecture test 기준.
- slice test 기준.
- integration test 기준.
- smoke test 기준.
- fixture/test data policy.
### 제외 범위
- load/performance test.
- chaos engineering.
- external provider E2E test.
## 근거 (필수, 최소 1개+)
> 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
| Source | 정당화하는 결정 |
|---|---|
| [[raw/official-docs/test-taxonomy-testcontainers-official]] | Testcontainers 공식 "real services, no H2" 입장과 정합 |
| [[raw/official-docs/test-taxonomy-practical-pyramid-fowler]] | Fowler/Cohn pyramid; ca-tmpl 6-level이 contract·architecture·slice를 명시 분리 |
| [[raw/company-tech-blogs/test-pyramid-vs-trophy-kent-dodds]] | static/integration heavy; React 진영 영향 |
| [[raw/official-docs/spring-boot-test-slices-webmvctest-datajpatest-official]] | D7 — Spring slice(@WebMvcTest/@DataJpaTest) semantics 및 "여러 slice annotation 혼용 미지원" 공식 정의 (SB-SLICE-C1, SB-SLICE-C2, SB-SLICE-C3, SB-SLICE-C4) |
| [[raw/official-docs/governance-archunit-official]] | D2 — ArchUnit이 "Java 코드 architecture(package/class dependency, layer/slice, cyclic)를 plain unit test framework로 검사" 공식 정의 (AU-OFF-C1, AU-OFF-C2) |
| [[raw/official-docs/archunit-user-guide]] | D2 — package 의존 규칙 fluent DSL (ARCHUNIT-UG-C4) |
| [[raw/official-docs/archunit-conditional-on-property-3-layer-pattern]] | D2 — *Building Evolutionary Architectures* fitness function 정의 = "아키텍처 특성에 대한 객관적 무결성 평가 mechanism" (AUCP-C5) |
## 외부 근거 / 대안 조사 (2026-05-22 — Group G-G: Test Taxonomy / Fixture)
본 branch의 6 levels (unit/contract/architecture/slice/integration/smoke) + Testcontainers from integration + src/testFixtures + 5min budget 결정에 대한 외부 source.
- **채택 결정 (6-level taxonomy + Testcontainers integration only)**:
- [[raw/official-docs/test-taxonomy-testcontainers-official]] — Testcontainers 공식 "real services, no H2" 입장과 정합
- **검토한 대안**:
- **대안 1: Classic test pyramid (unit/integration/e2e)** — [[raw/official-docs/test-taxonomy-practical-pyramid-fowler]] (Fowler/Cohn pyramid; ca-tmpl 6-level이 contract·architecture·slice를 명시 분리)
- **대안 2: Test trophy (Kent Dodds)** — [[raw/company-tech-blogs/test-pyramid-vs-trophy-kent-dodds]] (static/integration heavy; React 진영 영향)
- **대안 3: Honeycomb (Spotify)** — slim unit, fat integration
- **대안 4: Fitness functions (evolutionary architecture)** — Building Evolutionary Architectures, ca-tmpl architecture-test가 일부 해당
- **비교 핵심**: ca-tmpl 6-level taxonomy는 classic pyramid에 contract·architecture·slice를 명시 분리한 형태. 5min budget + Testcontainers cost가 unit/contract/architecture를 integration과 분리한 핵심 이유. Testcontainers 공식 "real services" 입장이 ca-tmpl integration-only 강제와 정합. Test trophy/Honeycomb은 frontend·SPA 진영이라 backend ca-tmpl과 trade-off 다름.
### 추가 조사 (2026-06-15 — /branch-spec 자동조사: D2·D7 UNSUPPORTED 해소)
- **D2 (architecture test as level)** — ArchUnit 공식 + *Building Evolutionary Architectures*:
- 비교한 대안: (1) ArchUnit 전용 architecture-test level, (2) fitness function 일반 메커니즘(jQAssistant/Deptective/custom), (3) 수동 코드 리뷰.
- 조건부 결론: ca-tmpl 처럼 패키지 경계 = layer 경계인 JVM/Spring Boot 프로젝트 → Alt 1(ArchUnit). 이미 `archunit-junit5:1.3.0` 의존성 존재(도입비용 0). 복잡한 경계(그래프 탐색 필요) → Alt 2(jQAssistant, 단 GPLv3). 1~2인 단명 프로젝트 → Alt 3(수동, 단 skeleton fork 강제력 없음 → ca-tmpl 부적합).
- **잔존 갭**: "architecture-test를 unit/integration과 동급의 별도 taxonomy level로 정의한 업계 공식 표준은 없음." fitness function 개념이 "architecture test ≠ unit test"임을 book-grade authority로 간접 지지하는 수준. Open Risk(D2)에 명시.
- **D7 (Spring slice test)** — Spring Boot 공식 reference:
- 비교한 대안: (1) Spring test slice(`@WebMvcTest`/`@DataJpaTest`), (2) `@SpringBootTest` 전체 context, (3) `MockMvcBuilders.standaloneSetup`/순수 mock.
- 조건부 결론: controller HTTP wire(routing/advice/security) → `@WebMvcTest`(slice level). JPA query → `@DataJpaTest`(slice level). 전체 context wire → `@SpringBootTest`(= integration level). Spring 없는 controller 단위 → `standaloneSetup`(= unit level). 두 slice annotation 한 클래스 혼용은 Spring 공식이 "not supported"(SB-SLICE-C2) → forbidden 직접 근거.
- **잔존 갭**: "hex use-case slice 와 Spring slice 명시 분리"의 hex 측 외부 근거는 미archive — `UNSUPPORTED_IMPL_DECISION` 잔존(D7 Open Risk).
## TODO
> TODO drained 2026-05-22 — 결정은 아래 "결정 사항" / "Test Level Matrix" / "판정 기준" / "테스트 계약" 참조. taxonomy 구분/fixture 사용/test data PII/optional adapter matrix/failure ownership/CI gate mapping 모두 표 또는 결정 라인으로 반영됨. 잔존 TODO 없음.
## Work Item Contract
각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 `기준 작성`으로 남아 있으면 이 branch는 완료로 보지 않습니다.
| field | required | rule |
| --- | --- | --- |
| Decision | yes | 구현자가 선택해야 하는 기본값 |
| Allowed | yes | 허용되는 예외와 조건 |
| Forbidden | yes | 절대 금지되는 구현/문서 상태 |
| Required registry update | conditional | error/env/header/log/metric/capability 변경 시 필수 |
| Required contract test | yes | 계약 위반 시 실패해야 하는 테스트 |
| Failure condition | yes | review/build에서 실패로 판정할 상태 |
| Canonical extraction target | yes | `wiki/projects` 승급 위치 |
## 진행 중 메모
> 문서/설계 단계. 코드 구현은 ca-tmpl 측에서 진행 중이며, 본 노트의 일부 결정은 실제 구현과 drift 발생 — §Audit & Findings 참조.
- 2026-06-15 (`/branch-spec`): ca-tmpl ground truth 대조 결과, 본 branch 결정 중 architecture-test(D2)·contract-test(D1/D5)·slice(D7)·Testcontainers(D3)·sample 누수 방어(테스트 계약)는 **이미 코드에 구현**되어 있음(`actually-implemented`). 단 fixture 배치(D6)·contract 도구(D5)·sample 누수 방어 메커니즘은 결정과 코드가 불일치(§Audit). 노트의 "현재 documented-only 단계" 자기 서술은 stale.
## 결정 사항
- 2026-05-22: contract test는 운영 계약 위반을 잡고, integration test는 provider 연동을 잡음.
- 2026-05-22: architecture test는 CA boundary와 package blueprint 위반을 잡음.
- 2026-05-22: Testcontainers는 integration test부터 강제. unit/contract/architecture test는 Testcontainers 금지.
- 2026-05-22: CI time budget 기본값은 unit+contract+architecture 5분 이내, integration matrix는 별도 gate.
- 2026-05-22: contract test 도구 = (1) envelope/error/log/env shape: JSON snapshot test (`approvaltests-java` 또는 자체 snapshot) (2) OpenAPI drift: springdoc-openapi 생성 vs checked-in snapshot diff (3) consumer-driven contract는 boundary 외부 통합 시만 도입(현재 skeleton out-of-scope).
- 2026-05-22: fixture 위치 = Gradle `src/testFixtures/java/<feature>/` source set. 명명 = `*Fixture.java` (정적 factory), `*Mother.java`는 alias.
- 2026-05-22: slice test 정의 = Spring slice (`@WebMvcTest`/`@DataJpaTest`)는 허용 단 hex slice(use case + port + mapper)와 명시적으로 분리. 동일 메서드에 두 slice annotation 혼용은 forbidden.
- 2026-05-22: flaky test ownership = test file의 첫 author 또는 가장 최근 maintainer. 14일 quarantine sunset (`feature-ci-quality-gates`와 cross-link).
- 2026-05-22: flaky test quarantine 정책 SSOT는 ci-quality-gates-contract(sunset 14일). 본 branch는 flaky 발생 시 quarantine bucket 분리만 명시.
- 2026-06-19 (구현 정합 — ca-tmpl 코드 작업, 사용자 fork 확정): 노트가 "사용자 정합" 으로 남겨둔 4개 fork 를 확정하고 `app-bootstrap` test 트리에 구현.
- (1) **D3 / DIR_LEVEL**: Testcontainers 를 쓰던 `bootstrap/contract[/outbox]/` 5개 test + `OutboxContainerTestSupport``bootstrap/integration[/outbox]/` 로 재분류 + `..contract..`·`..architecture..` 패키지가 Testcontainers 에 의존하면 fail 하는 ArchUnit rule `TestTaxonomyArchitectureTest.contract_and_architecture_tests_do_not_depend_on_testcontainers`(positive-control 로 `..integration..` 에서 발화 증명) 추가 → §테스트 계약 #4 **ENFORCED**.
- (2) **D6 / FIXTURE_LAYOUT**: `src/testFixtures` 마이그레이션 대신 현행 `fixtures/` package 유지 + main classpath 누수 차단 rule `CleanArchitectureTest.production_code_does_not_depend_on_test_fixtures`(`@ArchTest`, fixtureleak violation 으로 meta-verify) 추가. 2026-05-22 의 `src/testFixtures`+`*Mother` 결정은 **superseded** — 현 fixture 는 모듈 간 공유가 아니라 source-set 분리 이점이 낮음.
- (3) **SAMPLE_GUARD**: sample 누수 방어는 기존 build-time ArchUnit module rule `production_code_does_not_depend_on_sample_portfolio` **유지** — runtime `@ActiveProfiles(prod/staging)` ApplicationContext check 는 **미채택**, §테스트 계약 #3 명세를 build-time 기준으로 갱신(prod/staging yml 신설 없음).
- (4) **D4 / CI**: 5분 budget + contract-change/blueprint-change 동반 git-diff gate 는 GitHub Actions 신설 **보류(`planned`)** — ca-tmpl 에 CI workflow 부재, CI matrix 는 [[raw/branch-notes/feature-ci-quality-gates-contract]] 와 함께 후속.
- 추가: D7 slice-mixing ban(SB-SLICE-C2) 을 `TestTaxonomyArchitectureTest.slice_tests_do_not_mix_two_spring_slice_annotations`(`@WebMvcTest`+`@DataJpaTest` 한 클래스 금지; over-block guard 포함) 으로 구현. hex-slice 분리는 convention 유지(`UNSUPPORTED_IMPL_DECISION`).
- 검증: `:app-bootstrap:test --tests '*TestTaxonomyArchitectureTest'` 6/6 PASS, `--tests '*CleanArchitectureTest'` PASS, `verifyCleanArchitectureDependencies` GREEN, architect-sentinel ready(0 blocking). 변경은 `src/test/**` 한정 — production·`src/build.gradle`·module 의존 그래프 무변경. 계획서: `ca-tmpl/docs/superpowers/plans/2026-06-19-test-taxonomy-fixture-contract.md`.
## 판정 기준
| 구분 | 기준 |
| --- | --- |
| Decision | test taxonomy를 분리해 실패 원인을 즉시 알 수 있게 함 |
| Allowed | 작은 프로젝트는 디렉터리를 합치되 test tag/name으로 구분 |
| Forbidden | contract violation을 integration test에서만 우연히 발견 |
| Required groups | unit, contract, architecture, slice, integration, smoke |
| Failure condition | 어떤 테스트가 어떤 계약을 보호하는지 문서화되지 않으면 실패 |
## Test Level Matrix
| level | owns | Testcontainers |
| --- | --- | --- |
| unit | pure function/domain rule | no |
| contract | response/log/env/error/registry contract | no |
| architecture | package/import/capability rules | no |
| slice | controller/use case/mapper slice | optional no external provider |
| integration | DB/Redis/Kafka/outbound provider | yes when provider needed |
| smoke | bootstrap/sample removal/startup | optional |
## 결정-근거 매핑
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. `Decision ID` 는 이 branch-note 안에서 안정적으로 유지.
> `선택 조건` 열(R2): "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면 `N/A`.
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | contract test는 운영 계약 위반을 잡고, integration test는 provider 연동을 잡음 | 운영 계약(envelope/error/log/env/registry) shape 위반 검증 → contract test(Testcontainers 없음). 실제 provider 연동(DB/Redis/Kafka/outbound) 검증 → integration test. 계약과 연동을 한 테스트에 섞으면 실패 원인 모호 → 항상 분리 | `raw/official-docs/test-taxonomy-practical-pyramid-fowler.md#TPP-FOWLER-C2`, `raw/official-docs/test-taxonomy-practical-pyramid-fowler.md#TPP-FOWLER-C4`, `raw/official-docs/test-taxonomy-practical-pyramid-fowler.md#TPP-FOWLER-C6` | `engineering-blog` (Fowler/Vocke 정의 + 팀 합의 원칙) | Fowler 의 narrow integration 정의는 "test double" 가정 — Testcontainers real-container 와의 일관성은 별도 검증 |
| D2 | architecture test는 CA boundary와 package blueprint 위반을 잡음 | 패키지 경계 = layer 경계인 JVM/Spring Boot → ArchUnit architecture-test(이 결정). 경계가 annotation/runtime 기반이거나 polyglot → fitness function 일반 메커니즘(jQAssistant). 1~2인 단명 프로젝트 → 수동 리뷰. ca-tmpl 은 전자 | `raw/official-docs/governance-archunit-official.md#AU-OFF-C1`, `raw/official-docs/governance-archunit-official.md#AU-OFF-C2`, `raw/official-docs/archunit-user-guide.md#ARCHUNIT-UG-C4`, `raw/official-docs/archunit-conditional-on-property-3-layer-pattern.md#AUCP-C5` (fitness function 정의) — **ca-tmpl 코드 `actually-implemented`**: `app-bootstrap/.../architecture/CleanArchitectureTest.java`, `DisabledAdapterArchitectureTest.java` (`archunit-junit5:1.3.0`, build.gradle:47) | `official-vendor-doc` (ArchUnit) + `book-concept` (Evolutionary Architectures via AUCP-C5) | "architecture-test를 별도 taxonomy level로 정의한 업계 공식 표준은 없음" — fitness function 개념이 unit test와 다른 관심사임을 *간접* 지지하는 수준. 단 ca-tmpl 코드엔 실제 구현됨 → §Audit `D2_NOW_IMPLEMENTED` |
| D3 | Testcontainers는 integration test부터 강제. unit/contract/architecture test는 Testcontainers 금지 | real service(DB/Redis/Kafka/provider) 필요 → integration test에서 Testcontainers. pure logic/계약 shape/패키지 규칙 → Testcontainers 금지(5min budget·D4 보호). H2 대체는 Testcontainers 공식이 부적합 명시 | `raw/official-docs/test-taxonomy-testcontainers-official.md#TC-OFFICIAL-C1`, `raw/official-docs/test-taxonomy-testcontainers-official.md#TC-OFFICIAL-C3`, `raw/official-docs/test-taxonomy-testcontainers-official.md#TC-OFFICIAL-C5`**ca-tmpl `actually-implemented`**: `testcontainers:postgresql`+`junit-jupiter` (app-bootstrap build.gradle:28-29), `@Testcontainers` in `contract/outbox/*` | `official-vendor-doc` (real services + H2 한계) | Testcontainers 공식은 integration 권장만, 다른 level 금지는 ca-tmpl 별도 결정 (5분 budget 보호) |
| D4 | CI time budget 기본값은 unit+contract+architecture 5분 이내, integration matrix는 별도 gate | unit+contract+architecture 합산 → 5분 gate. integration matrix → 별도 gate(시간 무제한). 5분은 local fast-feedback 목표치이지 측정된 임계값 아님 | UNSUPPORTED_DECISION (자료에 5분 정량 기준 부재) | `team-policy` (Testcontainers `TC-OFFICIAL-C4` 의 "IDE 실행 가능성" 만 간접 지지) | 실제 측정으로 5분 임계점 검증 필요 (container start cost 포함). §Claims To Verify 1행 |
| D5 | contract test 도구 = JSON snapshot (`approvaltests-java`) + OpenAPI drift (springdoc) + CDC out-of-scope | envelope/error/log/env shape → JSON snapshot. OpenAPI drift → springdoc 생성 vs checked-in diff. boundary 외부 통합 시만 → CDC(현 skeleton out-of-scope) | `raw/official-docs/test-taxonomy-testcontainers-official.md#TC-OFFICIAL-C1` (Testcontainers 자체 정의는 integration 영역) — contract 도구 자체 근거는 `feature-contract-verification-test-suite` branch 의 source 가 SSOT(위임) | `cross-branch-reference` | 본 branch 의 책임 범위 — 도구 선택 근거는 verification branch 가 owner. **DRIFT**: 실제 코드는 `approvaltests-java` 미사용, `OpenApiSnapshotTest.java` 기반 → §Audit `CONTRACT_TOOL_DRIFT` |
| D6 | fixture 위치 = Gradle `src/testFixtures/java/<feature>/` source set, 명명 `*Fixture.java` / `*Mother.java` alias | fixture가 여러 test module에서 재사용 → 공유 source set(이 결정). 단일 모듈 한정 → 해당 모듈 test 트리 내 package | UNSUPPORTED_DECISION (자료에 src/testFixtures 권장 직접 명시 없음) | `team-convention` (Gradle Java Library plugin 공식 페이지 별도 raw 등록 권고) | Gradle 공식 documentation raw source 보강 필요. **DRIFT**: 실제 코드는 `java-test-fixtures` 플러그인/`src/testFixtures` 미적용 — fixtures는 `src/test/java/.../fixtures/` package(예: `sample-portfolio/.../fixtures/SamplePortfolioFixture.java`), `*Mother.java` 없음 → §Audit `FIXTURE_LAYOUT_DRIFT` (사용자 정합 필요) |
| D7 | slice test 정의 = Spring slice 허용하되 hex slice 와 명시 분리, 동일 메서드 혼용 forbidden | controller HTTP wire(routing/advice/security) → `@WebMvcTest`. JPA query → `@DataJpaTest`. 전체 context wire → `@SpringBootTest`(=integration level). Spring 없는 controller 단위 → `standaloneSetup`(=unit level). 두 slice annotation 한 클래스 혼용 → forbidden | `raw/official-docs/spring-boot-test-slices-webmvctest-datajpatest-official.md#SB-SLICE-C1` (slice semantics — 제한된 component scan), `raw/official-docs/spring-boot-test-slices-webmvctest-datajpatest-official.md#SB-SLICE-C2` (여러 @…Test 혼용 not supported — 혼용 forbidden 직접 근거), `raw/official-docs/spring-boot-test-slices-webmvctest-datajpatest-official.md#SB-SLICE-C3` (@WebMvcTest scan 목록), `#SB-SLICE-C4` (@Component 자동 제외) — **ca-tmpl `actually-implemented`**: `@WebMvcTest` in `sample-portfolio/.../WorkLogControllerWireTest`. `@DataJpaTest` 미사용(`planned`). "hex slice 와 명시 분리" 부분은 `UNSUPPORTED_IMPL_DECISION` 잔존 | `official-vendor-doc` (혼용 금지) + `team-convention` (hex 분리) | "hex slice 와 명시 분리" 결정의 외부 근거 보강 필요 (hexagonal architecture 원전 raw 미등록) |
| D8 | flaky test ownership = test file 첫 author 또는 가장 최근 maintainer, 14일 quarantine sunset | flaky 발생 → 본 branch 는 quarantine bucket 분리만. ownership/sunset 정책 자체 → `feature-ci-quality-gates-contract` 가 SSOT(위임) | UNSUPPORTED_DECISION (본 branch 자체에 ownership/sunset 자료 인용 없음 — `feature-ci-quality-gates-contract``company-case-study` (Spotify/Google quarantine) 가 SSOT) | `cross-branch-reference` | ci-quality-gates-contract 의 company-case-study 는 official best practice 아님 |
## 구현 가이드
> *결정*이 "*무엇*을 할 것인가"라면, 본 §는 "*어디에 어떻게* 구현되는가"의 사전 명세 — 다음 구현자가 *되묻지 않아도 코드를 작성할 수 있는 수준*. anchor 는 §2(`/branch-spec`)에서 대조한 **실제 ca-tmpl 코드 경로**이며, 코드로 확인 안 된 것은 `planned` 로 표기.
> 3-rule: R1 각 cell 은 Decision ID + Supporting Claim 도출 · R2 근거 없는 detail 은 `UNSUPPORTED_IMPL_DECISION` + trade-off 한 줄 · R3 본 branch 범위 밖은 §Audit 로 이관.
### 1. Test level → 디렉터리/메커니즘 매핑
> **Trace**: D1 · D2 · D3 · D7 + Test Level Matrix. 등급은 §2 코드 grep 으로 확정.
| level | 실제 경로/메커니즘 (ca-tmpl) | 등급 |
|---|---|---|
| unit | `domain-core/src/test/java/.../unit/` + 도메인 per-class 테스트 (Testcontainers 없음) | `actually-implemented` (부분) |
| contract | `<module>/src/test/java/.../contract/``app-bootstrap`(18 classes)·`adapter-outbound`·`adapter-web`·`shared-contract`. D1 운영 계약 shape 검증 | `actually-implemented` |
| architecture | `app-bootstrap/.../architecture/``CleanArchitectureTest.java`(`domain_is_pure`, `value_objects_have_no_public_no_arg_constructor`, `production_code_does_not_depend_on_sample_portfolio`:576), `DisabledAdapterArchitectureTest.java`. `archunit-junit5:1.3.0`. D2 | `actually-implemented` |
| slice | `@WebMvcTest``sample-portfolio/.../WorkLogControllerWireTest`·`VersioningPrefixTest`. `@DataJpaTest` 없음. D7(SB-SLICE-C1/C3/C4) | `actually-implemented`(@WebMvcTest) / `planned`(@DataJpaTest) |
| integration | `app-bootstrap` `@Testcontainers` (`contract/outbox/Outbox*ContractTest`). D3(TC-OFFICIAL-C1) | `actually-implemented` |
| smoke | `app-bootstrap/.../smoke/` (bootstrap/startup). D1 | `actually-implemented`(부분) |
> - **UNSUPPORTED_IMPL_DECISION**: 6-level 을 디렉터리에 1:1 *강제*하는 메커니즘(어떤 test가 어떤 level인지 ArchUnit rule/JUnit tag로 고정)은 미정 — 현재는 디렉터리 convention 만 존재. trade-off: convention 은 가볍지만 신규 test가 잘못된 level 에 놓여도 build 가 막지 않음(강제 < 관례). 강제까지 원하면 `@Tag` + ArchUnit "test class 위치 ↔ tag 일치" rule 추가 필요(`planned`).
> - integration test 가 ca-tmpl 에서 `contract/outbox/` 하위에 위치 — Test Level Matrix 의 level 명과 디렉터리 명이 1:1 아님(outbox integration 이 contract 폴더 안). 명칭 정합은 §Audit 후보(비차단).
### 2. Fixture 배치 (D6) — 결정 vs 코드 DRIFT
> **Trace**: D6 (UNSUPPORTED_DECISION). §2 코드 대조에서 drift 확정.
- **결정 명세**: `src/testFixtures/java/<feature>/` Gradle source set + `*Fixture.java`/`*Mother.java`.
- **실제 코드(`actually-implemented`)**: fixtures 는 test source set 내 `fixtures/` package (`sample-portfolio/.../fixtures/SamplePortfolioFixture.java`) + ArchUnit violation fixtures (`architecture/violations/.../*Fixture.java`). `java-test-fixtures` 플러그인·`src/testFixtures` 디렉터리 **없음**. `*Mother.java` **없음**.
- **UNSUPPORTED_IMPL_DECISION + DRIFT**: 결정과 코드가 불일치. 다음 구현자는 *결정을 따를지 코드를 따를지* 되묻게 됨 → 사용자 정합 필요. 두 옵션의 trade-off:
- (a) 결정대로 `java-test-fixtures` 마이그레이션 — fixture 가 main classpath 로 새지 않음을 **plugin 이 강제**. 비용: source set 분리 + 모든 fixture 이동.
- (b) 결정을 코드 현실(`fixtures/` package)로 갱신 — 가볍지만 누수 차단은 별도 ArchUnit rule(`noClasses().that().resideIn("..fixtures..").should().dependOnClassesThat()...`, §Claims To Verify 2행)에 의존.
- 정합 전까지 D6 는 `UNSUPPORTED_DECISION` 유지. 상세 → §Audit `FIXTURE_LAYOUT_DRIFT`.
- **`RESOLVED` (2026-06-19)**: 옵션 (b) 채택 — `fixtures/` package 유지 + ArchUnit 누수 rule `production_code_does_not_depend_on_test_fixtures` 추가. §결정 사항 2026-06-19 / §Audit.
### 3. Sample fixture prod 누수 방어 (테스트 계약) — 메커니즘 DRIFT
> **Trace**: §테스트 계약 "sample fixture prod leakage 검사". §2 코드 대조에서 drift 확정.
- **결정 명세**: `@ActiveProfiles("prod"|"staging")` 테스트의 ApplicationContext 에서 sample package class 0개 + ArchUnit 으로 `@ActiveProfiles` prod/staging test 의 `features.sample` import 금지.
- **실제 코드(`actually-implemented`)**: 누수 방어는 build-time ArchUnit rule `production_code_does_not_depend_on_sample_portfolio` (`CleanArchitectureTest.java:576`) — production scope ↛ `sample-portfolio` **module** 의존 차단. sample 은 `features.sample` *package* 가 아니라 `sample-portfolio` *module*(test classpath only). `application-prod.yml`/`application-staging.yml` **없음**.
- **UNSUPPORTED_IMPL_DECISION + DRIFT**: 명세 메커니즘(runtime `@ActiveProfiles` + ApplicationContext bean count)과 실제(build-time module-dependency ArchUnit rule)가 다름. trade-off: build-time module rule 은 compile graph 를 막아 더 이르게 실패하지만 runtime profile-conditional 활성 여부는 검증 못 함; runtime check 는 실제 활성 bean 을 보지만 늦게 실패. 정합 권고 → §Audit `SAMPLE_GUARD_MECHANISM_DRIFT`.
- **`RESOLVED` (2026-06-19)**: build-time module rule 유지로 확정(옵션 b). runtime `@ActiveProfiles` check·prod/staging yml 미추가 — 위 trade-off 의 "이른 실패 + compile graph 차단" 을 우선. §결정 사항 2026-06-19.
### 4. Contract 도구 (D5) — 도구 DRIFT
> **Trace**: D5 (cross-branch-reference; 도구 owner 는 `feature-contract-verification-test-suite`).
- **결정 명세**: JSON snapshot = `approvaltests-java`(또는 자체) + OpenAPI drift = springdoc 생성 vs checked-in diff.
- **실제 코드(`actually-implemented`)**: `approvaltests` 의존성 **없음**. OpenAPI snapshot = `sample-portfolio/.../openapi/OpenApiSnapshotTest.java`. contract/ 디렉터리는 ArchUnit/custom 기반 contract test.
- **UNSUPPORTED_IMPL_DECISION + DRIFT**: 본 branch 는 도구 owner 아님(verification branch 위임) — 도구 결정 정합은 그 branch 가 수행. 본 노트는 drift 만 surface → §Audit `CONTRACT_TOOL_DRIFT`.
### 5. CI gate 매핑 (테스트 계약 contract-change
> **Trace**: §테스트 계약 "contract-change 동반 test 검사" · "blueprint-change 동반 architecture test 검사".
- git diff regex 기반 CI step 2종(registry/owner-branch 변경 ↔ `src/test/**/contract/` 변경 동반, blueprint/enforcement 변경 ↔ `src/test/**/architecture/` 변경 동반). **`planned`** — §2 ground truth 에서 dual-mode CI matrix workflow 미발견, canonical doc 도 "CI matrix 미작성" 명시.
- **UNSUPPORTED_IMPL_DECISION**: git diff regex 의 false positive/negative(파일 rename, 신규 registry 파일 추가 시 false miss). trade-off: regex 는 가볍지만 경로 변경에 취약 → §Claims To Verify 6행으로 검증 위임.
- **`DEFERRED` (2026-06-19, planned 유지)**: ca-tmpl 에 `.github/workflows` 부재 — CI gate 신설을 이번 구현에서 보류. 5분 budget 측정·companion-change gate 는 [[raw/branch-notes/feature-ci-quality-gates-contract]] 와 함께 후속. 본 branch 의 로컬 강제(§테스트 계약 #3·#4 + slice rule)는 ArchUnit 으로 완료. §결정 사항 2026-06-19.
## Audit & Findings
> `/branch-spec`(2026-06-15) ca-tmpl 코드 ground truth 대조에서 발견한 **결정↔코드 drift**. 사용자 작성 결정 영역이므로 자동 rewrite 하지 않고 *정합 권고만* 기록. 등급은 §2 직접 grep 으로 확정.
| Finding | 결정(노트) | 코드(ca-tmpl ground truth) | 권고 |
|---|---|---|---|
| `FIXTURE_LAYOUT_DRIFT` (D6) | `src/testFixtures/java/<feature>/` source set + `*Fixture.java`/`*Mother.java` | `java-test-fixtures` 플러그인·`src/testFixtures` 없음. fixtures = `src/test/java/.../fixtures/` package (`sample-portfolio/.../fixtures/SamplePortfolioFixture.java`), `architecture/violations/.../*Fixture.java`. `*Mother.java` 없음 | **`RESOLVED` (2026-06-19, 옵션 b)**: 현행 `fixtures/` package 유지로 확정 + main-classpath 누수 차단 rule `CleanArchitectureTest.production_code_does_not_depend_on_test_fixtures` 추가. `src/testFixtures` 결정 superseded — §결정 사항 2026-06-19 |
| `CONTRACT_TOOL_DRIFT` (D5) | `approvaltests-java` JSON snapshot + springdoc OpenAPI diff | `approvaltests` 의존성 없음. OpenAPI snapshot = `OpenApiSnapshotTest.java`. 도구 owner = `feature-contract-verification-test-suite` | 도구 결정 정합은 verification branch 에서; 본 노트는 surface 만 |
| `SAMPLE_GUARD_MECHANISM_DRIFT` (테스트 계약) | `@ActiveProfiles("prod"/"staging")` + ApplicationContext bean 0개 + `features.sample` import 금지 | build-time ArchUnit `production_code_does_not_depend_on_sample_portfolio`(`CleanArchitectureTest.java:576`); `sample-portfolio` *module*(≠ `features.sample` package); prod/staging yml 없음 | **`RESOLVED` (2026-06-19, 옵션 b)**: build-time ArchUnit module rule 유지로 확정 — runtime `@ActiveProfiles` check 미채택, §테스트 계약 #3 명세를 build-time 기준으로 갱신. §결정 사항 2026-06-19 |
| `D2_NOW_IMPLEMENTED` (positive) | D2 UNSUPPORTED + Claims `planned`; canonical `skeleton-governance-...-scorecard.md` "실제 구현 내용: 없음" | architecture-test 실제 구현됨(`CleanArchitectureTest.java`, `DisabledAdapterArchitectureTest.java`, violation fixtures 까지) | canonical project doc 의 "documented-only/없음" 서술이 stale — `/ingest``actually-implemented` 로 갱신 권고 |
| `DIR_LEVEL_NAME_DRIFT` (D3 / Test Level Matrix) | D3: "contract test 는 Testcontainers 금지" + Test Level Matrix 가 contract/integration 을 별개 level 로 분리 | `contract/outbox/Outbox*ContractTest``@Testcontainers` 사용(`OutboxAppendTransactionalContractTest.java:31`) — 실체는 *integration*-level test 가 `contract/` 디렉터리에 mis-filed (D3 와 표면상 충돌하나 본질은 위치/명칭 drift, 계약 위반 아님) | **`RESOLVED` (2026-06-19)**: Task 1 에서 outbox Testcontainers tests 를 `bootstrap/integration/outbox/` 로 재분류 완료. `contract/` tree 에 Testcontainers 의존 없음 — `TestTaxonomyArchitectureTest.contract_level_tests_have_no_testcontainers_dependency()` PASS 로 검증됨 |
## 엣지·실패·의존
> R4 캡처. 정상 경로 외 *구현 중 부딪힐* 실패/엣지 + 다른 계약 의존을 미리 열거.
- **실패·엣지 경로**:
- **ArchUnit rule typo → false negative(silent pass)**: 패키지 패턴 오탈자면 위반을 못 잡고 통과. 방어 = 의도적 violation fixture(`architecture/violations/.../*Fixture.java`)로 rule 이 실제 fail 하는지 메타검증. ca-tmpl 에 이미 존재(`actually-implemented`). §Claims To Verify 4행.
- **`@WebMvcTest` + Spring Security → context 적재 비용 증가**: security filter chain 스캔으로 slice 속도 이점 감소, 5분 budget(D4) 위협. 방어 = `@Import(SecurityConfig)` 수동 제어.
- **`@DataJpaTest` H2 기본값 ↔ Testcontainers real DB 불일치**: slice 가 H2, integration 이 Postgres 면 query 동작 차이. 방어 = `@AutoConfigureTestDatabase(replace=NONE)` (`planned``@DataJpaTest` 미도입).
- **두 slice annotation 한 클래스 혼용**: Spring 공식 "not supported"(SB-SLICE-C2) — context 가 의도와 다르게 작동. 방어 = ArchUnit rule(`planned`, §Claims To Verify 3행).
- **contract-change CI regex false miss**: 파일 rename / 신규 registry 파일이면 동반 test 강제를 우회. §Claims To Verify 6행.
- **fixture 누수**: fixture 가 main classpath 로 새면 prod 빌드 오염. D6 drift 로 현재 plugin 강제 부재 → ArchUnit rule 의존(§구현 가이드 2).
- **smoke level 실패**: bootstrap context 적재 실패(컨테이너 미기동/포트 충돌) → fail-fast; sample removal 미완 상태로 startup 시 smoke fail. Test Level Matrix 가 smoke Testcontainers 를 `optional` 로 두어 분기 모호 → 아래 gate 귀속 규칙으로 해소.
- **level → CI gate 귀속 (D4 보강)**: D4 의 5min gate 는 `{unit, contract, architecture}` **한정**. `slice`·`smoke` 중 외부 의존(Testcontainers/real provider)이 있는 것은 **integration matrix gate**(시간 무제한), 없는 것은 5min gate. 즉 gate 분기 기준은 *level 이름*이 아니라 *외부 의존 유무*. (`@DataJpaTest` H2-only slice = 5min gate, Testcontainers smoke = integration gate.)
- **다른 계약 의존**:
- [[raw/branch-notes/feature-ci-quality-gates-contract]] — flaky quarantine sunset(14일) 정책 SSOT. 본 branch D8 은 bucket 분리만 위임. 그 sunset/ownership 정책이 바뀌면 D8 영향.
- [[raw/branch-notes/feature-contract-verification-test-suite]] — contract 도구 선택 + 11 gate / snapshot 로직 SSOT. 본 branch D5 가 consume. 도구 결정 변경 시 §구현 가이드 4 / §Audit `CONTRACT_TOOL_DRIFT` 갱신.
- [[raw/branch-notes/feature-skeleton-package-blueprint-contract]] · [[raw/branch-notes/feature-architecture-enforcement-rules]] — architecture-test(D2)가 강제하는 package blueprint / boundary rule 의 정의 owner. blueprint 가 바뀌면 `CleanArchitectureTest` rule 갱신 필요.
- [[raw/branch-notes/feature-operational-error-observability-foundation]] · [[raw/branch-notes/feature-log-management-contract]] · [[raw/branch-notes/feature-env-driven-runtime-configuration]] — contract-test(D1)가 보호하는 envelope/error/log/env 계약 owner. 이들 결정 변경 시 `src/test/**/contract/` 동반 변경 필요(§테스트 계약 contract-change).
- [[raw/branch-notes/feature-sample-removal-adoption-contract]] — sample-portfolio 누수 방어 대상(§테스트 계약 / §구현 가이드 3)의 sample-off / adoption 결정 owner.
## 테스트 계약
- contract-change 동반 test 검사: PR diff에 다음 중 1개라도 변경이 포함되면(`ca-tmpl/docs/registries/*.yaml`, `feature-operational-error-observability-foundation` 결정 사항, `feature-log-management-contract` 결정 사항, `feature-env-driven-runtime-configuration` 결정 사항) PR diff에 `src/test/**/contract/` 디렉터리의 file 변경도 포함되어야 함. 측정 방법: GitHub Actions step `git diff --name-only origin/main..HEAD | grep -E "(registries/.*\.yaml|feature-(operational-error|log-management|env-driven).*\.md)"` 결과 != empty이고 `git diff --name-only origin/main..HEAD | grep "src/test/.*contract/"` 결과 == empty이면 fail.
- blueprint-change 동반 architecture test 검사: PR diff에 `feature-skeleton-package-blueprint-contract.md` 또는 `feature-architecture-enforcement-rules.md` 변경이 포함되면 `src/test/**/architecture/` 디렉터리의 file 변경도 포함되어야 함. 측정 방법: 동일 git diff regex 조합. 불일치 시 fail.
- sample fixture prod leakage 검사: production profile(`application-prod.yml`, `application-staging.yml`)이 활성된 SpringBootTest 또는 Testcontainers integration test에서 `features.sample.` package의 class가 ApplicationContext에 등록되거나 fixture로 사용되면 fail. 측정 방법: `@ActiveProfiles("prod")` 또는 `@ActiveProfiles("staging")` 테스트 실행 후 `ApplicationContext.getBeanNamesForType(...)` 결과에서 sample package class 0개여야 함. 또한 ArchUnit으로 `@ActiveProfiles` value가 prod/staging인 test class는 sample package import 금지. *(**RESOLVED 2026-06-19**: ca-tmpl 채택 메커니즘은 build-time ArchUnit module rule `production_code_does_not_depend_on_sample_portfolio` 로 확정 — 위 runtime `@ActiveProfiles`/ApplicationContext bean-count 명세는 미채택. §Audit `SAMPLE_GUARD_MECHANISM_DRIFT` / §결정 사항 2026-06-19)*
- contract/architecture test가 Testcontainers에 의존하면 실패. *(**ENFORCED 2026-06-19**: `TestTaxonomyArchitectureTest.contract_and_architecture_tests_do_not_depend_on_testcontainers``..contract..`/`..architecture..` 코퍼스에 위반 없음 + `..integration..` positive-control 로 발화 증명. manual-importer 사용 이유는 `@AnalyzeClasses(DoNotIncludeTests)` 가 test bytecode 미포함이기 때문.)*
## 검증해야 할 주장
> 공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다. 구현 전/중/후에 실제로 검증해야 하는 주장을 분리.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| unit+contract+architecture test 합산이 5분 이내 완료 가능 | Testcontainers 공식 자료 (`TC-OFFICIAL-C4`) 는 IDE 실행 가능성만 보장, 시간 budget 정량 보장 없음 | CI workflow 에서 `unit+contract+architecture` job 실행 시간 측정 + 5분 초과 시 알림 | `needs-confirmation` |
| `src/testFixtures/java/<feature>/` source set 이 도메인 분리를 실제로 강제 | Gradle source set 자체는 fixture 위치만 강제, 도메인 분리는 별도 ArchUnit rule 필요. **DRIFT**: 현재 코드는 testFixtures 미사용(`fixtures/` package) → 이 Claim 은 결정(b) 채택 시에만 유효 | ArchUnit `noClasses().that().resideIn("..testFixtures..").should().dependOnClassesThat().resideInAPackage("..features.[^.]+..")` 룰 작성 후 위반 검출 | `planned` |
| `@WebMvcTest`/`@DataJpaTest` 와 hex slice 가 한 클래스에서 혼용되지 않음 | Spring 공식(SB-SLICE-C2)은 혼용을 "not supported" 로 명시하나 hex slice 와의 분리는 별도 — 혼용 시 context 확장이 의도와 다르게 작동 가능 | ArchUnit / custom test 로 `@WebMvcTest` 또는 `@DataJpaTest` 가 붙은 class 가 hex slice 구성 요소 (use case interface 등) 와 같은 file 에 없는지 검사 | `planned` |
| ArchUnit boundary rule 이 ca-tmpl package blueprint 위반을 모두 탐지 | ArchUnit DSL 표현력 한계 가능 + rule typo 시 silent false negative | 의도적 boundary 위반 코드(violation fixture)를 추가하고 ArchUnit 이 fail 하는지 확인 — **ca-tmpl 에 `architecture/violations/.../*Fixture.java` 이미 존재(`actually-implemented`)** | `locally-verified`(메커니즘 존재) / `planned`(전수성) |
| sample-portfolio fixture 가 production scope 에서 제거됨 | 노트 명세(`@ActiveProfiles("prod")` ApplicationContext check)와 실제 메커니즘(build-time ArchUnit module rule)이 다름 — §Audit `SAMPLE_GUARD_MECHANISM_DRIFT` | `CleanArchitectureTest.production_code_does_not_depend_on_sample_portfolio`(L576) 가 production↛sample-portfolio 의존을 차단하는지 확인 — **`actually-implemented`** | `locally-verified` |
| contract-change 동반 test 검사 git diff regex 가 false positive/negative 없음 | regex 가 file 경로 변경 (rename) 또는 새 registry 파일 추가 시 false miss 가능 | 의도적으로 registry yaml 만 수정한 PR 과 src/test/contract 만 수정한 PR 각각 생성 → CI 동작 verify | `planned` |
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
> `/coverage` 가 채우는 **생성물** — 손으로 유지하지 않는다. governing 문서(`governing_docs`: [[wiki/projects/ca-tmpl/skeleton-governance-registry-verification-test-scorecard]] §Test taxonomy, [[wiki/projects/ca-tmpl/sample-fixture-and-adoption]] §Sample fixture)가 요구하는 관심사를 본 브랜치가 빠짐없이 덮는지의 결과. 기준: `rules/coverage-gate.md`.
> 상태: `covered-here`(이 브랜치 결정) / `delegated`(다른 owner 브랜치) / `missing`(아무도 안 맡음 → Blocking).
> 생성: `/coverage`(coverage-auditor, 2026-06-15). governing 2종 + sibling 브랜치 + ca-tmpl 코드 대조. 판정: **Covered (Blocking 0)**.
| 관심사 | 상태 | owner | 심각도 | 근거 |
|--------|------|-------|--------|------|
| 6-level test taxonomy 정의 (unit/contract/architecture/slice/integration/smoke) | covered-here | — | — | D1·D2·D7 + Test Level Matrix; ca-tmpl `actually-implemented` |
| Testcontainers 적용 범위 (integration부터 강제 / 타 level 금지) | covered-here | — | ⚪ Advisory | D3; 단 `contract/outbox/Outbox*ContractTest``@Testcontainers` 사용 → §Audit `DIR_LEVEL_NAME_DRIFT` |
| fixture 격리 방법 (source set vs `fixtures/` package) | covered-here | — | — | D6(UNSUPPORTED, drift) → §Audit `FIXTURE_LAYOUT_DRIFT` |
| 5min CI budget 정책 | covered-here | — | — | D4(team-policy) + §Claims 1행 |
| 6-level 디렉터리 강제 메커니즘 | covered-here (planned) | — | — | §구현 가이드 1 `UNSUPPORTED_IMPL_DECISION` + §Claims 2행 |
| sample fixture production 누수 방어 메커니즘 | covered-here | — | — | §테스트 계약 + §구현 가이드 3 → §Audit `SAMPLE_GUARD_MECHANISM_DRIFT` |
| flaky test ownership / quarantine 정책 | delegated | [[raw/branch-notes/feature-ci-quality-gates-contract]] | — | D8 위임 (§엣지·§결정 사항 링크; SSOT = sunset 14일) |
| contract test 도구 선택 (snapshot/OpenAPI) | delegated | [[raw/branch-notes/feature-contract-verification-test-suite]] | — | D5 위임 (§엣지 링크) → §Audit `CONTRACT_TOOL_DRIFT` |
| package blueprint / boundary rule 정의 | delegated | [[raw/branch-notes/feature-skeleton-package-blueprint-contract]] · [[raw/branch-notes/feature-architecture-enforcement-rules]] | — | D2 위임 (§엣지 링크) |
| sample fixture 종류 + 12 scenario + 6-field minimum | delegated | [[raw/branch-notes/feature-sample-domain-contract-fixture]] | — | governing `sample-fixture-and-adoption.md` §Sample fixture SSOT — [[raw/branch-notes/feature-sample-domain-contract-fixture]] |
| sample-off / adoption 절차 (dual-mode CI matrix · adoption checklist) | delegated | [[raw/branch-notes/feature-sample-removal-adoption-contract]] | — | governing §Sample-off SSOT — [[raw/branch-notes/feature-sample-removal-adoption-contract]] (§엣지 링크) |
## 마주친 문제
- **`OperationalContractRuntimeTest` 2건 실패 (pre-existing)**
- 원인: `@WebMvcTest` Spring context load 실패 (`ConversionFailedException`, `LenientObjectToEnumConverterFactory`). 이 branch 변경과 무관 — 해당 파일은 `b314a99` (readme refactoring) 이전 커밋에서 유래하며 본 branch 에서 수정하지 않음.
- 검증(definitive, controller 2026-06-19): `git stash push -u` 로 본 branch 변경(tracked+untracked) 전부 제거 → working tree == clean HEAD `a0534b9` 확인 → `./gradlew :app-bootstrap:test --tests '*OperationalContractRuntimeTest'` 실행 → **clean HEAD 에서도 동일하게 2건 실패**(`ConversionFailedException` / `LenientObjectToEnumConverterFactory`) → `git stash pop` 으로 변경 원복. 본 branch 도입 _전_ 코드에서 재현되므로 pre-existing 확정.
- root-cause (2026-06-20): `application.yml:339``client-ip-mode: ${APP_RATE_LIMIT_CLIENT_IP_MODE}`**기본값 없는 placeholder**`.env` 없는 `./gradlew test` 에서 미해석 리터럴이 `RateLimitClientIpMode` enum 변환 실패 → context load fail. 상세 → [[raw/errors/webmvctest-slice-configprops-enum-placeholder-no-default-2026-06-20]].
- 해결 (2026-06-20, 사용자 요청으로 본 세션에서 수정 — rate-limit 관심사라 별도 커밋 권장): `application.yml` 을 레지스트리 선언값대로 `${APP_RATE_LIMIT_CLIENT_IP_MODE:remote-addr-only}` 로 갱신. 검증: `--tests '*OperationalContractRuntimeTest'` PASS, `verifyEnvKeys: OK`, 전체 `:app-bootstrap:test` `--rerun-tasks` 3회 연속 GREEN.
- **`TestTaxonomyArchitectureTest` 전체 스위트 flaky/vacuous (2026-06-20)**
- 증상: 단독 6/6 PASS 인데 전체 스위트 첫 실행에서 positive-control 3건 간헐 FAIL(코퍼스 빈 채로). clean-check 는 빈 코퍼스에서 silent vacuous-pass 위험.
- 원인: positive-control 코퍼스가 `importPackages(String)` static 필드 — 대형 스위트/stale build 에서 빈 코퍼스 반환 가능. 상세 → [[raw/errors/archunit-importpackages-empty-vacuous-stale-build-2026-06-20]].
- 해결: positive-control/over-block 코퍼스를 `importClasses(Class…)` (결정적)로 전환 + 슬라이스 fixture `public` 승격 + 전용 `TestcontainersUsingFixture`(`..taxonomyfixtures..`); clean-check 2건은 `importPackages` 유지하되 non-vacuity 가드(`corpus.size()>0`) 추가. 검증: 전체 `--rerun-tasks` 3회 연속 GREEN.
## 묶음
<!-- GENERATED: sources:start -->
- [[raw/company-tech-blogs/ci-flaky-test-quarantine-spotify-google]]
- [[raw/company-tech-blogs/test-pyramid-vs-trophy-kent-dodds]]
- [[raw/official-docs/dx-testcontainers-java-best-practices]]
- [[raw/official-docs/governance-archunit-official]]
- [[raw/official-docs/spring-boot-test-slices-webmvctest-datajpatest-official]]
- [[raw/official-docs/test-taxonomy-practical-pyramid-fowler]]
- [[raw/official-docs/test-taxonomy-testcontainers-official]]
<!-- GENERATED: sources:end -->
<!-- GENERATED: interviews:start -->
- [[raw/interviews/archunit-manual-importer-vs-analyzeclasses]]
<!-- GENERATED: interviews:end -->
<!-- GENERATED: errors:start -->
- [[raw/errors/archunit-importpackages-empty-vacuous-stale-build-2026-06-20]]
- [[raw/errors/webmvctest-slice-configprops-enum-placeholder-no-default-2026-06-20]]
<!-- GENERATED: errors:end -->
<!-- GENERATED: blog-topics:start -->
- [[raw/blog-topics/test-taxonomy-archunit-enforcement-2026-06-19]]
<!-- GENERATED: blog-topics:end -->
> Phase C2 (ca-implementer) 실 코드 작업 완료 (2026-06-19).
### 구현 산출물 (2026-06-19 ca-implementer)
**Task 1 — Testcontainers 테스트 재분류 (`contract/` → `integration/`)**
| 파일 | 작업 |
|---|---|
| `bootstrap/contract/DistributedLockProviderContractTest.java` | 삭제 |
| `bootstrap/contract/IdempotencyUniqueScopeContractTest.java` | 삭제 |
| `bootstrap/contract/outbox/Outbox*` (4개) | 삭제 |
| `bootstrap/integration/DistributedLockProviderContractTest.java` | 생성 (package 변경만) |
| `bootstrap/integration/IdempotencyUniqueScopeContractTest.java` | 생성 (JavaDoc FQN 참조 수정) |
| `bootstrap/integration/outbox/Outbox*` (4개) | 생성 (package + 주석 수정) |
| `bootstrap/integration/package-info.java` | 생성 |
| `bootstrap/integration/outbox/package-info.java` | 생성 |
**Task 2 — `TestTaxonomyArchitectureTest.java` 생성 (manual-importer pattern)**
- Rule: `contract_and_architecture_tests_do_not_depend_on_testcontainers` (allowEmptyShould=true)
- Meta-tests: contract corpus (isFalse, non-vacuity 가드) · architecture corpus (isFalse, non-vacuity 가드) · TestcontainersUsingFixture positive control (isTrue)
- plain `@Test` (NOT `@AnalyzeClasses`) — 이유: `@AnalyzeClasses``DoNotIncludeTests` 로 test bytecode 미포함
- **하드닝 (2026-06-20)**: positive-control/over-block 코퍼스를 `importClasses(Class…)` 결정적 import 로 전환(flaky/vacuous 수정 — §마주친 문제). clean-check 2건만 `importPackages` + `corpus.size()>0` 가드. positive-control 용 `taxonomyfixtures/TestcontainersUsingFixture.java` 신설(`..contract../..architecture..` 밖), 슬라이스 fixture `public` 승격.
**Task 3 — `slice_tests_do_not_mix_two_spring_slice_annotations` rule + fixtures**
- FQN string 참조 패턴 (`"org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest"` 등) — compile 의존 없음
- `violations/slice/MixedSliceAnnotationsFixture.java` (positive control)
- `allowed/slice/SingleSliceWebMvcFixture.java` (over-block guard)
**Task 4 — `CleanArchitectureTest.java` 에 `@ArchTest` 추가 + fixtureleak fixtures**
- `@ArchTest static final ArchRule production_code_does_not_depend_on_test_fixtures`
- `violations/fixtureleak/LeakyProductionConsumerFixture.java` + `violations/fixtureleak/fixtures/LeakedTestFixture.java`
- meta-test: `TestTaxonomyArchitectureTest.fixture_leak_rule_fires_on_production_depending_on_fixture()``CleanArchitectureTest``@ArchTest` field 를 직접 참조해 evaluate
**Task 5 — 검증 결과**
| Command | Result |
|---|---|
| `./gradlew :app-bootstrap:test --tests '*TestTaxonomyArchitectureTest'` | 6/6 PASS |
| `./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest'` | PASS |
| `./gradlew verifyCleanArchitectureDependencies` | BUILD SUCCESSFUL |
| `./gradlew verifyEnvKeys` | OK (99 keys, 74 required placeholders covered) |
| `./gradlew :app-bootstrap:test` (full, `--rerun-tasks`) | **3회 연속 GREEN** (2026-06-20, application.yml fix + test 하드닝 후 — 직전 2-fail 은 §마주친 문제에서 해소) |
> 추가 변경 (2026-06-20, 본 세션): `application.yml` rate-limit default fix(production resource), `TestTaxonomyArchitectureTest` 하드닝, `taxonomyfixtures/TestcontainersUsingFixture` 신설, 슬라이스 fixture `public` 승격.
### 오류 기록 (본 feature 작업 중 발생)
- [[raw/errors/webmvctest-slice-configprops-enum-placeholder-no-default-2026-06-20]] — `@WebMvcTest` 슬라이스가 기본값 없는 enum placeholder 로 context load 실패(rate-limit `client-ip-mode`). 본 세션에서 root-cause + fix.
- [[raw/errors/archunit-importpackages-empty-vacuous-stale-build-2026-06-20]] — `importPackages(String)` static 코퍼스가 대형 스위트에서 빈 채로 반환 → positive-control flaky + clean-check vacuous-pass. importClasses + non-vacuity 가드로 해결.
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- [[raw/interviews/archunit-manual-importer-vs-analyzeclasses]] — `@AnalyzeClasses(importOptions=DoNotIncludeTests)` vs `new ClassFileImporter()` 를 언제 쓰나? test bytecode 를 rule 의 대상으로 삼고 싶을 때 왜 manual importer 가 필요한가? (2026-06-19 작성)
### Blog topics (이 작업에서 나온 글감)
- [[raw/blog-topics/test-taxonomy-archunit-enforcement-2026-06-19]] — 테스트 분류를 _문서_ 에서 _빌드가 강제하는 import-graph 규칙_ 으로 옮긴 사례(Testcontainers ban + slice-mixing ban + fixture-leak guard + manual-importer/positive-control). (2026-06-19 작성)
## 관련 일일 노트
- `[[raw/daily-notes/2026-06-19]]` — ca-implementer Phase C2 실 코드 작업 완료 일자
## 완료 후 wiki 추출 대상
- `wiki/projects/ca-tmpl/skeleton-governance-registry-verification-test-scorecard.md` 의 Test taxonomy(§29 G-G) canonical section + `wiki/projects/ca-tmpl/sample-fixture-and-adoption.md` 의 fixture 누수 방어 section. (구 경로 `wiki/projects/ca-skeleton-operational-contract.md` 는 현재 cluster 분리됨.)
## 완료 후 정리
> 머지/종료 시점에 채움.
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목:
- `locally-verified` 항목:
- `prod-verified` 항목:
- **추출하지 않을 항목** (planned / documented-only / abandoned):