428 lines
49 KiB
Markdown
428 lines
49 KiB
Markdown
---
|
|
title: branch / feature-contract-verification-test-suite
|
|
source_type: branch-note
|
|
status: raw
|
|
branch: feature-contract-verification-test-suite
|
|
parent_branch:
|
|
related_projects: [ca-skeleton]
|
|
governing_docs: [raw/project-notes/ca-skeleton-operational-contract]
|
|
tags: [branch, ca-skeleton, test, contract, verification]
|
|
created: 2026-05-21
|
|
target_merge:
|
|
status_label: in-progress
|
|
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-010
|
|
kind: project-work-item
|
|
project: ca-skeleton-operational-contract
|
|
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-010
|
|
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-OPENAPI-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-TEST-001@1]
|
|
refines: []
|
|
overrides: []
|
|
depends_on: []
|
|
contract_packet: 1
|
|
contract_packet_sha256: 15121967b54182a52d343b3a87b21d692dc3bb7a28edb3f75d306f2b09e74d63
|
|
---
|
|
|
|
# branch: feature-contract-verification-test-suite
|
|
|
|
> Layer: `raw/branch-notes/` — 운영 계약을 테스트로 강제하는 통합 검증 기준을 정의합니다.
|
|
|
|
<!-- 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 의 운영 계약 중 해당 영역 (§12 Test Contract · §13 API Contract Surface · §16 Schema/Serialization · §18 CI Quality Gates) 의 결정/근거/금지 사항을 정제한다.
|
|
|
|
<!-- GENERATED: branch-contract:start -->
|
|
<!-- section-id: branch-contract-packet -->
|
|
## 브랜치 계약 패킷
|
|
|
|
- **생성 시 프로젝트 개정**: `1`
|
|
- **패킷 스키마**: `contract_packet: 1`
|
|
- **완료 조건**: release-blocking contract suite가 OpenAPI drift를 검출한다
|
|
|
|
<!-- section-id: inherited-project-decisions -->
|
|
### 상속한 프로젝트 결정
|
|
|
|
| Decision Ref | Project Summary | Branch Application | Source |
|
|
|---|---|---|---|
|
|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-OPENAPI-001@1` | verification suite가 OpenAPI drift의 release-blocking 판정권을 소유한다 | 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별 기준이 문서에만 있으면 쉽게 깨집니다. 공통 contract verification suite로 response, log, env, boundary, repository capability, adapter failure mapping을 강제합니다.
|
|
|
|
- 이슈:
|
|
- PR:
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
- structured response contract test.
|
|
- validation field error contract test.
|
|
- raw exception leakage test.
|
|
- structured log field test.
|
|
- PII/token/body log forbidden test.
|
|
- retryable classification test.
|
|
- env profile matrix smoke test.
|
|
- repository capability violation test.
|
|
- adapter failure mapping test.
|
|
|
|
### 제외 범위
|
|
|
|
- business use case acceptance test.
|
|
- load test.
|
|
- provider integration E2E test.
|
|
|
|
## 근거 (필수, 최소 1개+)
|
|
|
|
> 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
|
|
|
|
| Source | 정당화하는 결정 |
|
|
|---|---|
|
|
| [[raw/official-docs/verification-approvaltests-snapshot-official]] | ApprovalTests JSON snapshot (ca-tmpl 채택 |
|
|
| [[raw/official-docs/verification-pact-cdc-official]] | Pact 공식이 "consumer-known subset만 검증" 직접 인정 → single-team에서 snapshot 우위 |
|
|
| [[raw/official-docs/verification-spring-restdocs-official]] | test-driven docs, docs quality 강점이나 contract 검증 weak |
|
|
| [[raw/official-docs/verification-spring-cloud-contract-official]] | stub-runner 강점이나 stub 정의 별도 작성 부담 |
|
|
| [[raw/official-docs/openapi-spec-3-1-0]] | OpenAPI Specification v3.1 — OpenAPI drift gate 의 SSOT 가 되는 machine-readable HTTP API contract 표준 (D5/D6 OpenAPI drift release-blocking 결정의 normative 근거) |
|
|
| [[raw/official-docs/spring-framework-test-enabledif-jupiter-annotation]] | D3: @EnabledIf 가 Spring Environment property placeholder 를 읽어 true 일 때만 테스트를 실행 (그 외 SKIPPED) — optional adapter contract test 를 adapter enabled env matrix 에서만 실행하는 공식 근거 |
|
|
| [[raw/official-docs/junit5-conditional-env-variable-user-guide]] | D3 보강: JUnit 5 공식 `@EnabledIfEnvironmentVariable` / `@DisabledIfEnvironmentVariable` — OS 환경 변수 undefined 시 DISABLED(SKIPPED, never FAILED) 보장, named+matches regex 속성, 5.6+ repeatable |
|
|
|
|
## 외부 근거 / 대안 조사 (2026-05-22 — Group G-G: Contract Verification Test Suite)
|
|
|
|
본 branch의 11 release-blocking gates + JSON snapshot (approvaltests) + Pact CDC out-of-scope 결정에 대한 외부 source.
|
|
|
|
- **채택 결정 (snapshot test + OpenAPI drift)**:
|
|
- [[raw/official-docs/verification-approvaltests-snapshot-official]] — ApprovalTests JSON snapshot (ca-tmpl 채택)
|
|
- **검토한 대안**:
|
|
- **대안 1: Pact CDC (consumer-driven contract)** — [[raw/official-docs/verification-pact-cdc-official]] (Pact 공식이 "consumer-known subset만 검증" 직접 인정 → single-team에서 snapshot 우위)
|
|
- **대안 2: Spring REST Docs** — [[raw/official-docs/verification-spring-restdocs-official]] (test-driven docs, docs quality 강점이나 contract 검증 weak)
|
|
- **대안 3: Spring Cloud Contract** — [[raw/official-docs/verification-spring-cloud-contract-official]] (stub-runner 강점이나 stub 정의 별도 작성 부담)
|
|
- **대안 4: Hoverfly / WireMock service virtualization** — 외부 의존성 mock, contract 검증 자체는 아님
|
|
- **비교 핵심**: snapshot(full schema) + OpenAPI drift는 single-team skeleton에서 합당. CDC는 외부 consumer 등장 시점이 도입 임계점 — ca-tmpl out-of-scope 결정은 Pact 공식 입장과 정합. Spring REST Docs는 docs quality 강점이나 contract 위반 검증력 약함.
|
|
- **D3 (optional adapter 조건부 실행) 대안 비교 (2026-06-15 자동조사)**: ① JUnit 5 `@EnabledIfEnvironmentVariable` (primary — env undefined → SKIPPED 공식 보장, Gradle 버전 무관, JUnit XML `<skipped>` 집계 가능) ② Spring `@EnabledIf` SpEL/property-placeholder (보완 — env+profile AND 복합 조건 / Spring Environment 바인딩 필요 시; JUnit 5.7+ 동명 어노테이션 import 충돌 주의) ③ `@Tag` + Gradle `includeTags` 태스크 분리 (보류 — Gradle 9.0 커스텀 Test 태스크 includeTags regression [gradle#35907], CI step skip 이라 JUnit 리포트에 SKIPPED 미집계). 권고: Alt1 primary + Alt2 보완.
|
|
|
|
## TODO
|
|
|
|
> TODO drained 2026-05-22 — 통과 기준은 아래 "결정 사항" / "판정 기준" / "Verification Ownership Matrix" / "테스트 계약" 참조. **11개 release-blocking gates** = 9 base contract tests + OpenAPI drift + sample removal smoke. (base 9개: response schema, validation exposure, raw exception leakage, log field, PII/token/body forbidden, retryable, env matrix, repository capability, adapter failure. 추가 2개: OpenAPI drift, sample removal smoke.)
|
|
>
|
|
> 잔존 미해결 TODO (retain):
|
|
- ~~PII/token/body log forbidden 구현 메커니즘~~ closed 2026-05-22: structured field whitelist + Logback masking 이중 layer.
|
|
- Layer 1 (Logback): custom `%mask` converter가 PatternLayout 단계에서 `(?i)(token|password|authorization|cookie|secret|key)\s*[=:]\s*[^*\s]+` regex 매칭 시 `****`로 치환.
|
|
- Layer 2 (Jackson): DTO field에 `@JsonSerialize(using=MaskingSerializer.class)` 명시. 미명시 PII field가 ObjectMapper로 serialize되면 archetype test fail.
|
|
- Verification test: JUnit + Logback ListAppender로 모든 log event capture. 다음 2 assertion: (a) capture된 log line에 위 regex 매칭 0건. (b) structured log JSON의 field name이 `mdc-keys.yaml`의 `log type별 allowed fields` 외 값 0건. 위반 시 fail.
|
|
- request body capture filter: default `spring.web.body-capture.enabled=false`. true로 활성화하려면 `allowed-content-types` 명시 + endpoint allowlist 필수.
|
|
|
|
## 진행 중 메모
|
|
|
|
- 이 branch는 모든 branch의 마지막 safety net입니다.
|
|
|
|
## 결정 사항 (decisions)
|
|
|
|
- 2026-05-21: 문서 기준은 테스트로 강제되어야 canonical 승급 대상이 됨.
|
|
- 2026-05-22: contract violation은 CI에서 release-blocking failure로 취급.
|
|
- 2026-05-22: optional adapter contract test는 adapter enabled env matrix에서만 실행.
|
|
- 2026-05-22: sample-portfolio fixture는 boundary/repo/transaction/error/log contract의 기준 fixture로 사용.
|
|
- 2026-05-22: OpenAPI/schema drift release-blocking 집행권은 이 branch가 단일 owner. API/schema/compatibility branch는 snapshot producer 또는 compatibility rule producer.
|
|
- 2026-05-22: verification suite는 **11개 release-blocking gates** = 9 base contract tests + OpenAPI drift + sample removal smoke. (base 9개: response schema, validation exposure, raw exception leakage, log field, PII/token/body forbidden, retryable, env matrix, repository capability, adapter failure. 추가 2개: OpenAPI drift, sample removal smoke.)
|
|
- 2026-06-15: D3 조건부 실행 메커니즘을 JUnit 5 `@EnabledIfEnvironmentVariable` primary + Spring `@EnabledIf` 보완으로 확정 (자동조사 근거 archive). `@Tag`+Gradle 분리는 Gradle 9.0 regression 으로 보류.
|
|
- 2026-06-20: (A) ArchUnit contract-isolation rule (`ContractSuiteIsolationArchTest`) 구현 완료. manual-importer 패턴, PACKAGE_DRIFT 해소 (`dev.caskeleton` 기준 `..` wildcard), 3-method: clean-check + positive-control + over-block guard. (B) `ContractSuiteCompletenessTest` 구현 완료 — 9 base contract class 를 `Class.forName` release-blocking enumerate. `actually-implemented`, `locally-verified` (Gradle :app-bootstrap:test PASS, 4 test methods).
|
|
- 2026-06-20: **suite 전체 구현 완료** (`actually-implemented`, `locally-verified` — `./gradlew check` BUILD SUCCESSFUL 1m28s, ca-architect-sentinel PASS 0 blocking). 사용자 확정 결정 2건: ① OpenAPI drift gate = **committed-snapshot 동등 비교** (`openapiCheckSnapshot` task + `-PapproveOpenApiChange` refresh, `verifyPublicPathSnapshot` 패턴 미러; 의미론적 additive/breaking 분류는 api-compatibility branch 레이어로 유지). ② delegated 경계 = **이 branch 검증물만** (sample `@ConditionalOnProperty` wiring · `.github` CI yaml · trace-propagation test 는 타 branch 소유 — 미구현, skip-not-pass/assert-core-green 으로 부재에 robust). 신규: `EnvelopeContractTest`(approvaltests 3 snapshot), `StructuredLogFieldContractTest`, `PiiTokenBodyForbiddenContractTest`(Logback ListAppender), `EnvProfileMatrixContractTest`, `OptionalAdapterConditionalExecutionContractTest`(6 composed `@EnabledIf*` + EngineTestKit SKIP proof), `SampleRemovalSmokeContractTest`, `OpenApiDriftContractTest`(sample-portfolio, servers block strip 으로 RANDOM_PORT 비결정성 제거). 도구: `approvaltests-java:31.0.0` + `junit-platform-testkit` (app-bootstrap testImpl).
|
|
- 2026-06-20: approvaltests 스냅샷 파일을 test 소스 옆이 아닌 전용 `contract/approved/` 하위폴더로 격리. 메커니즘 = `dev.caskeleton.bootstrap.contract.PackageSettings` 클래스의 `public static String UseApprovalSubdirectory = "approved"` (approvaltests 의 `org.packagesettings` 라이브러리가 package 계층을 따라 `PackageSettings` 를 찾아 필드를 읽음). **`.approvaltests.json` 은 approvaltests-java 에서 동작하지 않음** (raw/errors 후보 — .NET 포트의 config 와 혼동 주의; Java 는 `PackageSettings` 클래스 필드 방식).
|
|
- 2026-06-20: **§7 Layer 2 (Jackson `MaskingSerializer`) 미구현 — 아키텍처 제약**. masking SSOT `LogMaskingPatterns` 는 `app-bootstrap` 소재인데 DTO 가 사는 `adapter-web` 는 `app-bootstrap` 의존 금지(역방향). `shared-contract` 는 Jackson-free. 따라서 clean Layer-2 serializer 는 masking SSOT 를 `shared-contract` 로 relocate(타 branch production 변경, verification-only scope 밖)하거나 regex 중복(SSOT 훼손) 없이는 불가. gate #5 의 **검증**(Layer-1 런타임 masking + body-capture-disabled)은 `PiiTokenBodyForbiddenContractTest` 로 완료. ca-architect-sentinel 이 이 omission 이 아키텍처적으로 옳음을 독립 확인. → Layer-2 production serializer 는 log-management/boundary branch 의 후속 결정으로 이관.
|
|
|
|
## 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` 승급 위치 |
|
|
|
|
## 결정-근거 매핑
|
|
|
|
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. `Decision ID` 는 이 branch-note 안에서 안정적으로 유지.
|
|
|
|
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|
|
|---|---|---|---|---|
|
|
| D1 | 문서 기준은 테스트로 강제되어야 canonical 승급 대상이 됨 | UNSUPPORTED_DECISION (내부 governance 결정 — 외부 source 직접 증명 없음) | `team-policy` | ca-tmpl 운영 계약 자체의 원칙 |
|
|
| D2 | contract violation은 CI에서 release-blocking failure로 취급 | `raw/official-docs/test-taxonomy-practical-pyramid-fowler.md#TPP-FOWLER-C4` (CDC workflow의 책임 분배 — provider 가 contract test 를 green 으로 유지) | `engineering-blog` | Fowler 인용은 워크플로우 정의일 뿐, "release-blocking" 강도까지 직접 보장 안 함. release-blocking CI 배선 자체의 owner 는 `feature-ci-quality-gates-contract` (delegated) |
|
|
| D3 | optional adapter contract test는 adapter enabled env matrix에서만 실행 (skipped, not failed) | primary (Alt 1): `raw/official-docs/junit5-conditional-env-variable-user-guide.md#JUNIT5-ENV-C1` (`@EnabledIfEnvironmentVariable` named+matches regex 일치 시만 enabled), `#JUNIT5-ENV-C2` (env var undefined → DISABLED = SKIPPED, never FAILED). 보완 (Alt 2): `raw/official-docs/spring-framework-test-enabledif-jupiter-annotation.md#SPRING-ENABLEDIF-C1` (`@EnabledIf` 표현식 true 일 때만 실행), `#SPRING-ENABLEDIF-C2` (Spring Environment property placeholder gate) | `official-vendor-doc` (JUnit 5 + Spring Framework) | adapter enabled property key ↔ annotation 매핑은 구현 단계 검증 필요 (SPRING-ENABLEDIF-C2 Does-not-prove: property 소스 우선순위 미명시). Alt 3(@Tag+Gradle includeTags)은 Gradle 9.0 regression(gradle#35907)로 보류 |
|
|
| D4 | sample-portfolio fixture는 boundary/repo/transaction/error/log contract의 기준 fixture로 사용 | UNSUPPORTED_DECISION (ca-tmpl 내부 fixture 관례) | `team-convention` | sample fixture 의 prod leakage 방지 (test taxonomy branch D8 와 cross-link). flag(`APP_SAMPLE_ENABLED`)+adoption owner 는 `feature-sample-removal-adoption-contract` (delegated) — 본 branch 는 removal smoke 만 verify |
|
|
| D5 | OpenAPI/schema drift release-blocking 집행권은 이 branch가 단일 owner | (조직 ownership 결정 — 외부 표준이 owner 분리를 강제하지 않음) supporting: `raw/official-docs/openapi-spec-3-1-0.md#OPENAPI31-C2` (OAS = HTTP API 의 standard, machine-readable contract — drift 의 diff 대상이 표준화된 spec 임을 corroborate), `#OPENAPI31-C3` (OAS document 의 single vs split 구조 — drift gate 가 spec 파일을 다루는 근거). 정합: project §25 SSOT Owner Map ("OpenAPI / schema drift" owner = 본 branch) | `official-standard` (drift 대상 spec 자체) + `team-policy` (owner 분리) | 외부 표준은 OAS 가 drift 대상으로 적절함을 보장할 뿐, "single owner" governance 자체는 ca-tmpl 운영 결정. API/schema compatibility branch 와의 책임 경계 명확화 필요 |
|
|
| D6 | verification suite는 11개 release-blocking gates (9 base contract + OpenAPI drift + sample removal smoke) | `raw/official-docs/verification-approvaltests-snapshot-official.md#AT-OFFICIAL-C2` (complex object 비교 패턴) + `raw/official-docs/verification-approvaltests-snapshot-official.md#AT-OFFICIAL-C3` (approve workflow) + `raw/official-docs/openapi-spec-3-1-0.md#OPENAPI31-C1` (OAS normative keyword 해석 BCP 14), `#OPENAPI31-C2` (OAS = HTTP API contract 의 표준), `#OPENAPI31-C4` (Data Type = JSON Schema 2020-12 base — drift diff 의 type 어휘 표준화), `#OPENAPI31-C7` (Schema Object = JSON Schema 2020-12 superset) | `official-vendor-doc` (snapshot 도구) + `official-standard` (OAS drift gate 의 spec SSOT) | 11개 gate 의 정확한 enumeration 자체는 ca-tmpl 내부 결정. OpenAPI drift gate 도구 (openapi-diff / oasdiff) 의 OAS 3.1 호환성은 별도 검증 필요 (OPENAPI31-C7 Does-not-prove: JSON Schema 2020-12 의 모든 keyword 가 OAS 에서 동작하는 것은 아님) |
|
|
| D7 | contract test 도구 = JSON snapshot test (`approvaltests-java`) — envelope/error/log/env shape 검증 | `raw/official-docs/verification-approvaltests-snapshot-official.md#AT-OFFICIAL-C1`, `raw/official-docs/verification-approvaltests-snapshot-official.md#AT-OFFICIAL-C2`, `raw/official-docs/verification-approvaltests-snapshot-official.md#AT-OFFICIAL-C3` | `official-vendor-doc` | ApprovalTests 공식은 일반 complex object 만 언급 — envelope/error/log shape 시나리오 적합성은 추가 검증 필요. **ground truth: approvaltests-java 는 현재 ca-tmpl 미의존 (planned)** — §Audit & Findings 참조 |
|
|
| D8 | Pact CDC 는 out-of-scope (boundary 외부 통합 시만 도입) | `raw/official-docs/verification-pact-cdc-official.md#PACT-OFFICIAL-C4` (consumer-known subset 만 검증), `raw/official-docs/verification-pact-cdc-official.md#PACT-OFFICIAL-C5` (provider-only 한계 — multi-consumer 맥락) | `official-vendor-doc` (Pact 자체가 single-team subset 한계를 명시) | 외부 partner consumer 등장 시 도입 임계점은 ca-tmpl 별도 판단 |
|
|
| D9 | Spring Cloud Contract 도 동일 사유 out-of-scope | `raw/official-docs/verification-spring-cloud-contract-official.md#SCC-OFFICIAL-C1` (CDC umbrella project), `raw/official-docs/verification-spring-cloud-contract-official.md#SCC-OFFICIAL-C3` (Stub Runner = consumer-side 도구) | `official-vendor-doc` (CDC 정체성 자체가 multi-consumer 가정) | Spring REST Docs (`SRD-C1`, `SRD-C2`, `SRD-C3`) 는 docs 품질 도구로 별도 분류 — drift gate 책임 다름 |
|
|
|
|
## 구현 가이드
|
|
|
|
> *결정 (Decisions)* 이 "*무엇*" 이라면 본 §는 "*어디에 어떻게* 구현될 것인가" 의 사전 명세 — 다음 구현자가 되묻지 않고 코드를 작성할 수 있는 수준.
|
|
>
|
|
> **ground truth 정합 주의**: §2 ca-tmpl 코드 대조 결과 본 suite 는 대부분 **planned** 상태(자세히는 §Audit & Findings). 아래 표의 `as-built` 열은 `/home/donghyeon/workspace/ca-tmpl` 실 코드 grep 기반이며, `status` = `exists`(코드에 있음) / `partial`(도메인 특화 테스트로 일부) / `planned`(미구현). 명칭/glob 은 코드 확인 전까지 `planned`.
|
|
|
|
### 1. Contract test 디렉터리 배치 + 도메인 격리 강제
|
|
|
|
> **Trace**: D1(테스트 강제) + D7(snapshot 도구) ← `AT-OFFICIAL-C1`. 테스트 계약 §1(ArchUnit isolation) 의 구현 사전명세.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: ArchUnit regex-negation rule 형태(`..contract..` should-not depend-on `..features.(?!sample)..`) + `features.sample` allowlist 는 사용자 임의 trade-off — ApprovalTests/ArchUnit 공식은 "레이어 격리" 원칙만 권고, 정확한 glob 은 권고하지 않음. trade-off: regex 부정으로 sample 만 예외 허용 vs allowlist 명시 나열(유지보수 ↑, 명시성 ↑).
|
|
|
|
| 항목 | planned 명세 | as-built (ca-tmpl) | status |
|
|
|---|---|---|---|
|
|
| 디렉터리 | 각 module `src/test/**/contract/` | `app-bootstrap/.../contract/` 만 populated; `adapter-web`/`adapter-outbound`/`shared-contract` 의 `contract/` 는 `.gitkeep` 빈 placeholder | partial |
|
|
| 격리 rule | ArchUnit `noClasses().that().resideIn("..contract..").should().dependOnClassesThat().resideInAPackage("..features.(?!sample).+..")` | **`ContractSuiteIsolationArchTest` 구현됨** (`app-bootstrap/.../architecture/ContractSuiteIsolationArchTest.java`). 3 @Test: clean-check (non-vacuity guard + eval), positive-control, over-block guard. PACKAGE_DRIFT 해소: `..` wildcard 로 base-package-agnostic. NOTE: ArchUnit 이 regex negation 미지원이므로 `resideInAPackage("..features..").and(resideOutsideOfPackage("..features.sample.."))` 로 compose. | **exists** (`actually-implemented`, `locally-verified`) |
|
|
|
|
> ⚠️ **PACKAGE_DRIFT**: 테스트 계약 §1 의 glob 은 `com.example.caskeleton.features.*` 를 가정하나 ca-tmpl 실 base package 는 `dev.caskeleton`. 구현 시 glob 을 `dev.caskeleton..features..` 기준으로 정정. (사용자 작성 결정 영역이므로 본 §은 정합 권고만; 자동 rewrite 안 함 — §Audit & Findings.)
|
|
|
|
### 2. 9 base contract test class 인벤토리 + as-built 매핑
|
|
|
|
> **Trace**: D6(11 gates) ← `AT-OFFICIAL-C2`/`C3`. 테스트 계약 §2(9 base enumeration) 의 구현 사전명세.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: 각 test class 의 정확한 명칭(`EnvelopeContractTest` 등) + "9개를 단일 `contract/` 디렉터리로 묶는" 구조는 사용자 임의 명명 — 공식 근거는 snapshot 패턴만 권고. trade-off: generic 단일 suite(중복 ↓, 응집 ↑) vs adapter-specific 분산(이미 일부 존재, 재사용).
|
|
|
|
| # | base contract | planned suite class | as-built (ca-tmpl) | status |
|
|
|---|---|---|---|---|
|
|
| 1 | envelope/response schema | `EnvelopeContractTest` | `adapter-web/.../envelope/EnvelopeBodyAdviceTest`, `EnvelopeMetaIntegrationTest` (NOT in `contract/`, 명칭 다름) | planned(generic) / partial(behavior) |
|
|
| 2 | validation exposure | (planned) | `BusinessRuleValidationContractTest` (category 매핑 일부) | partial |
|
|
| 3 | raw exception leakage | (planned) | `BusinessRuleValidationContractTest#no_client_safe_message_leaks_sql_constraint_or_internals` | partial |
|
|
| 4 | structured log field | (planned) | generic contract 없음 (adapter-specific logger test 만: `RequestLoggingFilterTest` 등) | planned |
|
|
| 5 | PII/token/body forbidden | (planned) | `outbox/EventPayloadPiiContractTest`(ArchUnit) + `SqlLoggingForbiddenContractTest` (generic body/token 없음) | partial |
|
|
| 6 | retryable classification | (planned) | `PersistenceFailureMappingContractTest`, `LockFailureClassificationContractTest` | exists |
|
|
| 7 | env profile matrix | (planned) | `runtime/StartupSafetyValidatorTest` (contract/ 아닌 곳에 misplaced) | partial |
|
|
| 8 | repository capability | (planned) | `RepositoryAccessCapabilityRegistryTest` | exists |
|
|
| 9 | adapter failure mapping | (planned) | `PersistenceFailureMappingContractTest` (persistence side) | exists |
|
|
|
|
### 3. Snapshot 도구 + 검증 대상 shape
|
|
|
|
> **Trace**: D7(approvaltests-java) ← `AT-OFFICIAL-C1`/`C2`/`C3`.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: approval `.approved`/`.received` 파일 명명 규약 + approve workflow(누가 승인) + JSON 정규화 직렬화기 위치 + scrub 대상 field source 는 사용자 임의 — 공식은 패턴만 권고. trade-off ①(scrub 지점): Jackson ObjectMapper mixin/custom serializer 단계 scrub(타입 안전, 재사용) vs `Approvals.verify` 직전 string regex post-process(단순, 도구 무관). trade-off ②(scrub 대상): non-deterministic field 목록을 registry(`mdc-keys.yaml`) 참조(SSOT 정합) vs test-fixture hardcoded list(독립, drift 위험). 기본 대상: `timestamp`/`trace_id`/`request_id`/`correlation_id`/`span_id`/`duration_ms` + ULID id. trade-off ③(도구 위치): test-fixtures 공유 vs module 별 중복.
|
|
|
|
- 도구: `approvaltests-java` (`Approvals.verify(...)`). **as-built: 미의존** — build.gradle/version catalog grep 0건, `Approvals.verify` 사용 0건 → `planned`. 구현 시 test 의존성 추가.
|
|
- 검증 4 shape: ① envelope(success/data/meta) ② error(code/category/message/retryable/details) ③ structured log JSON ④ env profile 별 effective config. (Claims To Verify #1 이 4 shape 적합성 검증.)
|
|
|
|
### 4. optional adapter 조건부 실행 메커니즘
|
|
|
|
> **Trace**: D3 ← `JUNIT5-ENV-C1`/`C2` (primary) + `SPRING-ENABLEDIF-C1`/`C2` (보완).
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: test 별 Alt1 vs Alt2 선택 + composed annotation 명명(`@EnabledIfKafkaEnabled` 등) 은 사용자 임의 — 공식은 두 메커니즘을 모두 제공할 뿐 선택을 권고하지 않음. trade-off: Alt1(OS env 직접, Spring context 불필요, Gradle 무관) vs Alt2(Spring Environment 바인딩/profile AND 표현 가능, 5.7+ import 충돌 주의).
|
|
|
|
- **primary (Alt 1)** — `@EnabledIfEnvironmentVariable(named="<flag>", matches="true", disabledReason="...")`. env undefined → SKIPPED(never FAILED, `JUNIT5-ENV-C2`).
|
|
- **보완 (Alt 2)** — Spring `@EnabledIf("#{environment['...'] == 'true'}")` 또는 property-placeholder, env+profile **AND** 또는 Spring Environment override 반영 필요 시.
|
|
- env enable flag (registry `env-keys.yaml` 확인): `APP_MESSAGING_KAFKA_ENABLED`(:1257), `APP_CACHE_REDIS_ENABLED`(:1187), `APP_NOTIFICATION_SLACK_ENABLED`(:1287), `APP_NOTIFICATION_GOOGLE_EMAIL_ENABLED`(:1301), `APP_OUTBOUND_HTTP_RETRY_ENABLED`(:529), `APP_OUTBOUND_HTTP_CIRCUIT_BREAKER_ENABLED`(:585).
|
|
- profile 선택자 = `SPRING_PROFILES_ACTIVE` (allowed: local/dev/staging/prod/sample). ⚠️ **`APP_PROFILE` 사용 금지** — registry 에서 제거됨(env-keys.yaml D6 2026-06-06).
|
|
|
|
### 5. OpenAPI drift gate 메커니즘
|
|
|
|
> **Trace**: D5(drift 집행 단일 owner) + D6 ← `OPENAPI31-C2`/`C3`. project §25 SSOT Owner Map: "OpenAPI / schema drift" owner = 본 branch, producer = api-baseline.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: diff 도구(openapi-diff vs oasdiff), committed snapshot 파일 경로, gradle task 명(`openapiCheckSnapshot`), escape-hatch label 명(`intent:breaking-change-approved`), **snapshot baseline 생성/갱신 절차** 는 사용자 임의 — OAS 표준은 diff 대상 spec 만 표준화. trade-off ①(도구): oasdiff(CLI, breaking-change 분류 내장) vs openapi-diff(Java lib, gradle 통합 쉬움). trade-off ②(baseline 갱신): springdoc `/v3/api-docs` 출력을 commit 된 fixture 로 두고, 첫 baseline + 의도적 변경 승인 시 `./gradlew openapiCheckSnapshot --write` 류 explicit refresh task 로만 갱신(수동 commit 방지) vs 매 빌드 자동 재생성(drift 무력화 위험 — 채택 금지). 첫 baseline 은 수동 commit 후 review.
|
|
|
|
- producer: [[raw/branch-notes/feature-api-contract-baseline]] D10 (springdoc `adapter-web/build.gradle:11`). 본 branch 는 그 runtime spec 을 committed snapshot 과 diff 하여 release-blocking 판정.
|
|
- **as-built: planned** — `sample-portfolio/.../openapi/OpenApiSnapshotTest` 가 `/v3/api-docs` 제공만 검증하고 **drift gate 는 명시적으로 본 branch 로 defer**(`OpenApiSnapshotTest.java:35-36`). committed snapshot 파일 없음, `openapiCheckSnapshot` task 없음, oasdiff/openapi-diff 의존 없음.
|
|
|
|
### 6. sample-removal smoke 메커니즘
|
|
|
|
> **Trace**: D4(sample fixture) + D6(11 gates). flag/adoption owner = `feature-sample-removal-adoption-contract` (delegated) — 본 branch 는 smoke verify 만 own.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: smoke 실행 gradle task 명 + sample bean gating 방식(`@ConditionalOnProperty`)은 adoption branch 소유 — 본 branch 는 결과(core green)만 assert. trade-off: 별도 gradle task vs 기존 test 에 profile param.
|
|
|
|
- flag: `APP_SAMPLE_ENABLED` (registry `env-keys.yaml:1398`, default true, `prod_profile_must_be_false`, owner `feature-sample-removal-adoption-contract`).
|
|
- smoke: `APP_SAMPLE_ENABLED=false` 로 core app/context/contract test 실행 → 모두 green assert (Claims To Verify #4).
|
|
- **as-built: planned** — flag 는 registry 에만 존재, 코드 wiring(`@ConditionalOnProperty(...sample)`) 0건, smoke test/task 없음.
|
|
|
|
### 7. PII/token/body forbidden 검사 메커니즘
|
|
|
|
> **Trace**: D6(11 gates 중 PII/token/body forbidden) + TODO closed(2026-05-22 이중 layer). field whitelist authoritative = registry `mdc-keys.yaml`(snake_case).
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: mask regex 패턴 + capture 수단(Logback ListAppender vs Spring `OutputCaptureExtension`) + async appender 경로 커버리지 는 사용자 임의 — 공식 근거 없음. trade-off: ListAppender(동기 event 직접 capture) 는 async/custom appender 우회 가능(Claims #5 needs-confirmation).
|
|
|
|
- Layer 1 (Logback): `%mask` converter regex `(?i)(token|password|authorization|cookie|secret|key)\s*[=:]\s*[^*\s]+` → `****`.
|
|
- Layer 2 (Jackson): PII DTO field `@JsonSerialize(using=MaskingSerializer.class)`; 미명시 시 archetype test fail.
|
|
- verify: JUnit + Logback ListAppender 로 (a) masked regex 매칭 0건 (b) log JSON field ∈ `mdc-keys.yaml` allowed.
|
|
- **as-built: partial** — `SqlLoggingForbiddenContractTest` + `outbox/EventPayloadPiiContractTest` 존재; generic body/token forbidden contract 는 `planned`.
|
|
|
|
## 엣지·실패·의존
|
|
|
|
> R4(깊이 게이트) 캡처용. 정상 경로 외에 구현 중 부딪힐 실패/엣지/다른 계약 의존.
|
|
|
|
- **실패·엣지 경로**:
|
|
- **async / custom Logback appender 우회**: ListAppender 가 동기 event 만 capture → async appender 로 흐른 PII/token 미탐지. 기대: async appender 도 capture 경로에 포함하거나 별도 assert (Claims To Verify #5, `needs-confirmation`).
|
|
- **springdoc dynamic-routing 누락**: runtime introspection 이 일부 dynamic route 를 OpenAPI spec 에 미반영 → drift snapshot false-negative (실제 envelope 변경을 못 잡음). 기대: 의도적 schema 변경 PR 로 gate exit code 검증 (Claims #3).
|
|
- **snapshot non-deterministic field**: timestamp/traceId/requestId/correlationId/ULID 가 매 실행 변동 → snapshot diff false-positive churn. 기대: 정규화 scrubber 로 변동 field mask 후 비교.
|
|
- **env key 오탈자 → silent SKIP**: `@EnabledIfEnvironmentVariable` 가 undefined env 를 SKIPPED 처리(`JUNIT5-ENV-C2`)하므로, CI matrix 가 flag 명을 오타내면 "의도적 skip" 과 구분 불가. 기대: `disabledReason` 명시 + CI 의 SKIPPED 항목 review.
|
|
- **sample-portfolio prod leak**: fixture 가 test 외 의존성으로 prod classpath 에 누출 (D4 open risk). 기대: sample-removal smoke 가 leak 을 build 실패로 감지.
|
|
- **Gradle daemon env 미반영**: daemon 캐싱이 env 변경을 stale 반영(gradle#17461) → 조건부 테스트 오작동. 기대: CI 에서 `--no-daemon` 또는 daemon 재시작.
|
|
- **다른 계약 의존**:
|
|
- [[raw/branch-notes/feature-api-contract-baseline]] D10 (OpenAPI/springdoc producer) — drift gate 가 이 producer 의 runtime spec 을 diff. producer surface 가 바뀌면 본 gate snapshot 갱신 필요.
|
|
- [[raw/branch-notes/feature-api-compatibility-deprecation-contract]] — breaking-change catalog 를 openapi-diff gate 가 consume (additive vs breaking 분류).
|
|
- [[raw/branch-notes/feature-schema-serialization-contract]] — JSON field/type/date/money schema 가 serialization snapshot 의 대상. 직렬화 정책 변경이 snapshot 을 깨뜨림.
|
|
- [[raw/branch-notes/feature-ci-quality-gates-contract]] — release-blocking CI 배선(11 gates 의 `needs:` 의존성)의 owner. 본 branch 는 gate(test)를 produce, CI wiring 은 CI branch 가 consume (delegated).
|
|
- [[raw/branch-notes/feature-sample-removal-adoption-contract]] — `APP_SAMPLE_ENABLED` flag + sample bean gating 의 owner. 본 branch 는 removal smoke 만 verify (delegated).
|
|
- [[raw/branch-notes/feature-operational-error-observability-foundation]] D10/D19 — envelope `error.category` enum(10, `Category.java`) + log field snake_case(`mdc-keys.yaml`) 가 contract test assertion 의 기준값.
|
|
- [[raw/branch-notes/feature-distributed-tracing-contract]] — requestId/traceId/correlationId **propagation 테스트** owner (`DistributedTracingContractTest`, registry `required_test = contract-verification:trace-propagation`). §12 propagation 관심사는 본 branch 가 아니라 tracing branch 가 소유 → 본 suite 는 그 결과를 중복 검증하지 않음 (delegated).
|
|
- [[raw/branch-notes/feature-repository-access-permission-contract]] — repository capability enum(7, `capabilities.yaml`) 이 repository capability contract test 의 기준.
|
|
|
|
## Audit & Findings (ca-tmpl ground-truth 대조, 2026-06-15)
|
|
|
|
> §2 절차로 `/home/donghyeon/workspace/ca-tmpl` 실 코드/registry 를 grep 대조한 결과. 사용자 작성 결정 영역(테스트 계약 등)은 **자동 rewrite 하지 않고 정합 권고만** 기록(CLAUDE.md §11). Claims To Verify 가 이미 `planned`/`needs-confirmation` 으로 정직히 표기하므로 본 §은 그 ground truth 근거를 보강.
|
|
|
|
| 라벨 | finding | 근거(file:line) | 권고 |
|
|
|---|---|---|---|
|
|
| `STALE_TEST_NAME` | 테스트 계약 §2 가 `EnvelopeContractTest` 명시하나 실 구현은 `EnvelopeBodyAdviceTest`+`EnvelopeMetaIntegrationTest` (위치도 `adapter-web/.../envelope/`, `contract/` 아님) | grep `class.*ContractTest` 에 Envelope 없음; `EnvelopeBodyAdviceTest.java` | generic envelope contract test 신설 or 기존 envelope 테스트를 `contract/` 승격 + 명칭 정합 |
|
|
| `PACKAGE_DRIFT` | 테스트 계약 §1 ArchUnit glob 이 `com.example.caskeleton.features.*` 가정, 실 base package 는 `dev.caskeleton` | `src/shared-contract/.../dev/caskeleton/...` | glob 을 `dev.caskeleton..features..` 로 정정 |
|
|
| `APP_PROFILE_REMOVED` | env matrix 결정이 `APP_PROFILE` 가정 가능하나 registry 에서 제거됨 | `env-keys.yaml:38-39` (D6 2026-06-06 제거) | `SPRING_PROFILES_ACTIVE` 로 정합 |
|
|
| `PLANNED_NOT_IMPLEMENTED` | approvaltests-java 미의존 / OpenAPI drift gate 미구현(producer 가 본 branch 로 defer) / sample-removal smoke 미구현(flag 만 존재) / ~~ArchUnit contract-isolation rule 미구현~~ / CI 부재 | grep `approvaltests`=0; `OpenApiSnapshotTest.java:35-36`; `APP_SAMPLE_ENABLED` in `src/**.java`=0; `find .github`=∅. **2026-06-20 부분 해소**: contract-isolation rule → `ContractSuiteIsolationArchTest` (`actually-implemented`, `locally-verified`); 9-base enumeration → `ContractSuiteCompletenessTest` (`actually-implemented`, `locally-verified`). 잔존 미구현: approvaltests-java, OpenAPI drift gate, sample-removal smoke, CI 배선 | Claims To Verify 가 정직 표기 — 본 branch 착수 = 이들 구현 |
|
|
| `OWNERSHIP_CLARIFY` | sample flag/adoption owner = `feature-sample-removal-adoption-contract`; release-blocking CI wiring owner = `feature-ci-quality-gates-contract` | `env-keys.yaml:1398` owner_branch; project §25 Owner Map | 본 branch 는 verification(smoke/test) produce, flag·CI wiring 은 delegated (§엣지·실패·의존 의존 링크) |
|
|
|
|
## 판정 기준
|
|
|
|
| 구분 | 기준 |
|
|
| --- | --- |
|
|
| Decision | 계약은 문서가 아니라 테스트로 강제 |
|
|
| Allowed | optional adapter는 enabled profile에서만 테스트 |
|
|
| Forbidden | contract violation을 warning-only로 처리 |
|
|
| Required tests | response schema, validation exposure, raw exception leakage, log field, PII/token/body forbidden, retryable, env matrix, repository capability, adapter failure, OpenAPI drift, sample removal smoke |
|
|
| Failure condition | 위 계약 중 하나라도 깨졌는데 build가 성공하면 실패 |
|
|
|
|
## Verification Ownership Matrix
|
|
|
|
| produced by | artifact | verified here by |
|
|
| --- | --- | --- |
|
|
| API baseline | OpenAPI snapshot | drift check against runtime response/envelope |
|
|
| API compatibility | breaking change catalog | openapi-diff release-blocking gate |
|
|
| schema serialization | JSON field/type/date/money schema | serialization snapshot |
|
|
| sample fixture | sample-portfolio scenarios | contract fixture run |
|
|
| sample removal | no-sample profile | sample removal smoke |
|
|
| registry governance | registry tables/artifacts | registry usage scan |
|
|
|
|
## 테스트 계약
|
|
|
|
- skeleton-level 실행 가능성: contract test class는 `src/test/**/contract/` 디렉터리에 위치하고 import statement에 도메인-specific package(`com.example.caskeleton.features.{도메인}.`)를 사용하지 않아야 함. 단, `features.sample.`는 fixture로 허용. 측정 방법: ArchUnit `noClasses().that().resideIn("..contract..").should().dependOnClassesThat().resideInAPackage("..features.(?!sample).+..")` (regex 부정). 위반 시 fail.
|
|
- 9 base contract test enumeration: response schema test (`EnvelopeContractTest`), validation exposure test, raw exception leakage test, log field test, PII/token/body forbidden test, retryable classification test, env matrix test, repository capability test, adapter failure mapping test — 9개 test class가 `src/test/**/contract/`에 존재하고 모두 PR단위 release-blocking. 측정 방법: 9개 file 존재 verify + CI gate 명시.
|
|
- optional adapter는 enabled env에서만 관련 contract test를 실행.
|
|
- OpenAPI snapshot과 실제 response envelope가 drift되면 build 실패.
|
|
- sample-portfolio 제거 profile에서 core app/context/contract tests가 실패하면 build 실패.
|
|
|
|
## 검증해야 할 주장
|
|
|
|
> 공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다. 구현 전/중/후에 실제로 검증해야 하는 주장을 분리.
|
|
|
|
| Claim | Why uncertain | How to verify | Status |
|
|
|---|---|---|---|
|
|
| ApprovalTests JSON snapshot 이 envelope/error/log/env 4가지 shape 모두에 적합 | 공식 (`AT-OFFICIAL-C2`) 는 일반 complex object 만 언급, 4가지 사용처 별 패턴 검증 부재 | 각 4영역마다 PoC test 작성 + snapshot diff 가 의도된 변화만 감지하는지 확인. ✓ envelope/error 는 approvaltests 3 snapshot (`EnvelopeContractTest`, scrub 후 stable) 로 검증; log shape 는 field-membership (`StructuredLogFieldContractTest`), env 는 registry 제약 (`EnvProfileMatrixContractTest`) 로 검증 — full-snapshot 보다 robust 하다는 판단(Claims #1 결론: approvaltests 는 envelope/error 에 적합, log/env 는 targeted assertion 이 우위) | `locally-verified` |
|
|
| 9개 base contract test class 가 모두 `src/test/**/contract/` 에 존재하고 release-blocking | 본 branch 의 "테스트 계약" 에 enumeration 있으나 실제 코드 부재 | 9개 file 존재 verify + CI workflow 의 `needs:` 의존성에 모두 포함 verify. **`ContractSuiteCompletenessTest` 구현됨** (`app-bootstrap/.../contract/ContractSuiteCompletenessTest.java`) — `Class.forName(fqcn, false, loader)` 로 9 base class 검증. `:app-bootstrap:test` PASS (1/1 method green). | `locally-verified` |
|
|
| OpenAPI snapshot vs runtime response envelope drift 가 build 단계에서 잡힘 | springdoc 의 runtime introspection (`CIOS-C1`) 은 dynamic routing 일부 누락 가능 | 의도적 envelope schema 변경 PR → `openapiCheckSnapshot` exit code != 0 verify. ✓ `OpenApiDriftContractTest`(sample-portfolio) committed snapshot 동등 비교; compare-mode 2회(`--rerun-tasks`) green, `servers` block strip 으로 RANDOM_PORT 비결정성 제거. dynamic-routing 누락 가능성은 잔존(springdoc introspection 한계) | `locally-verified` |
|
|
| sample-portfolio 제거 profile 에서 core app/context/contract tests 가 모두 통과 | sample-portfolio 이 fixture 외에 의존성으로 leak 되어 있을 가능성 | `APP_SAMPLE_ENABLED=false` profile 로 test suite 실행 + core test green verify. ✓ `SampleRemovalSmokeContractTest`: (a) 모든 production module 이 sample-portfolio 를 test-only 로만 참조(삭제 가능 보장), (b) `APP_SAMPLE_ENABLED` registry `prod_profile_must_be_false`. 실제 bean-gating(`@ConditionalOnProperty`)+no-sample boot 은 feature-sample-removal-adoption-contract 위임 — 본 branch 는 검증물만 | `locally-verified` (smoke); full no-sample boot `delegated` |
|
|
| Logback ListAppender 기반 PII/token/body forbidden 검사가 모든 log path 를 capture | custom appender / async appender 가 별도 경로로 leak 가능 | 의도적 PII log 코드 추가 → contract test fail verify; async logging 도 capture 되는지 확인. ✓ `PiiTokenBodyForbiddenContractTest`: 동기 ListAppender 로 capture→`LogMaskingPatterns.mask()` 후 `UNMASKED_SECRET` 매칭 0건 (6 secret shape + Bearer scheme false-positive 방지 possessive quantifier). async/custom appender 경로는 미검증 잔존. §7 Layer 2 (Jackson MaskingSerializer)는 아키텍처 제약으로 이관(결정 참조) | `locally-verified` (sync); async path `needs-confirmation` |
|
|
| `intent:breaking-change-approved` label escape hatch 가 의도된 PR 에만 적용 | label 추가 권한 정책 부재 시 누구나 우회 가능 | GitHub branch protection + CODEOWNERS 로 label 추가 권한 제한 + audit log 점검 | `planned` |
|
|
| optional adapter test 가 disabled env 에서 FAILED 아닌 SKIPPED 로 보고됨 | `JUNIT5-ENV-C2`/`SPRING-ENABLEDIF-C1` 는 공식 보장이나 ca-skeleton 의 실 annotation 적용·CI 리포트 집계는 미검증 | 각 adapter flag=false 로 test 실행 → JUnit XML `<skipped>` 생성 + build green verify. ✓ `OptionalAdapterConditionalExecutionContractTest`: 6 composed `@EnabledIf*` annotation (현행 registry flag 명: REDIS/HTTP_RETRY/HTTP_CIRCUIT_BREAKER=`true`, MESSAGING_BROKER/SLACK/EMAIL provider=`.+`), default env 에서 6 skipped; EngineTestKit 으로 disabled→skipped(1)/failed(0)/started(0) 독립 증명 | `locally-verified` |
|
|
|
|
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
|
|
|
> `/coverage` 가 채우는 **생성물** — 손으로 유지하지 않는다. governing 문서(frontmatter `governing_docs` = `raw/project-notes/ca-skeleton-operational-contract`, §12 Test Contract · §13 API Contract Surface · §16 Schema/Serialization · §18 CI Quality Gates)가 요구하는 관심사를 이 브랜치가 빠짐없이 덮는지의 결과. 기준: `rules/coverage-gate.md`.
|
|
> 상태: `covered-here`(이 브랜치 결정) / `delegated`(다른 owner 브랜치) / `missing`(아무도 안 맡음 → Blocking).
|
|
|
|
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
|
|--------|------|-------|--------|------|
|
|
| §12 structured error response schema (envelope shape) | covered-here | — | — | D6 (9 base #1), §테스트계약 |
|
|
| §12 validation details exposure policy | covered-here | — | — | D6 (9 base #2), §구현 가이드 §2 #2 |
|
|
| §12 raw exception leakage 방지 | covered-here | — | — | D6 (9 base #3), §구현 가이드 §2 #3 |
|
|
| §12 structured log field 존재 | covered-here | — | — | D6 (9 base #4), §구현 가이드 §2 #4 |
|
|
| §12 PII/token/body 미기록 | covered-here | — | — | D6 (9 base #5), §구현 가이드 §7 |
|
|
| §12 retryable classification | covered-here | — | — | D6 (9 base #6), `PersistenceFailureMappingContractTest` (exists) |
|
|
| §12 requestId/traceId/correlationId propagation | delegated | [[raw/branch-notes/feature-distributed-tracing-contract]] | OK (linked) | tracing §테스트계약 + `DistributedTracingContractTest` (exists) — §엣지·실패·의존 의존 링크 보유 |
|
|
| §12 env profile matrix smoke test | covered-here | — | — | D6 (9 base #7), §구현 가이드 §4 |
|
|
| §12 repository capability violation detection | covered-here | — | — | D6 (9 base #8), `RepositoryAccessCapabilityRegistryTest` (exists) |
|
|
| §12 adapter failure mapping | covered-here | — | — | D6 (9 base #9), `PersistenceFailureMappingContractTest` (exists) |
|
|
| §13 OpenAPI schema ↔ 실제 응답 일치 검증 | covered-here | — | — | D5 (단일 owner), §구현 가이드 §5 (planned) |
|
|
| §16 OpenAPI schema drift 테스트 감지 | covered-here | — | — | D5/D6; schema-serialization 이 집행권 본 branch 위임 |
|
|
| §18 CI gate 분리 (format/lint/test/contract/drift/security) | delegated | [[raw/branch-notes/feature-ci-quality-gates-contract]] | OK (linked) | CI 배선 owner = ci-quality-gates D1/D3; §엣지·실패·의존 의존 링크 보유 |
|
|
| §18 contract violation not warning-only (CI 배선) | delegated | [[raw/branch-notes/feature-ci-quality-gates-contract]] | OK (linked) | 본 branch 는 test produce(D2), CI 강제 wiring 은 ci-quality-gates owner |
|
|
| §18 optional adapter test = enabled matrix only | covered-here | — | — | D3 (`@EnabledIfEnvironmentVariable` primary), §구현 가이드 §4 |
|
|
| sample removal smoke (§18 연계) | covered-here | — | — | D4/D6 (smoke verify 소유); flag/wiring 은 feature-sample-removal-adoption-contract (delegated, §엣지 link) |
|
|
|
|
## 마주친 문제
|
|
|
|
- 아직 없음.
|
|
|
|
## 묶음
|
|
|
|
<!-- GENERATED: sources:start -->
|
|
- [[raw/official-docs/junit5-conditional-env-variable-user-guide]]
|
|
- [[raw/official-docs/openapi-spec-3-1-0]]
|
|
- [[raw/official-docs/spring-framework-test-enabledif-jupiter-annotation]]
|
|
- [[raw/official-docs/verification-approvaltests-snapshot-official]]
|
|
- [[raw/official-docs/verification-pact-cdc-official]]
|
|
- [[raw/official-docs/verification-spring-cloud-contract-official]]
|
|
- [[raw/official-docs/verification-spring-restdocs-official]]
|
|
<!-- GENERATED: sources:end -->
|
|
|
|
<!-- GENERATED: blog-topics:start -->
|
|
- [[raw/blog-topics/contract-verification-suite-release-gates-2026-07-02]]
|
|
<!-- GENERATED: blog-topics:end -->
|
|
|
|
> 2026-06-20 첫 실 구현 완료: ContractSuiteIsolationArchTest + ContractSuiteCompletenessTest.
|
|
|
|
### 오류 기록 (본 feature 작업 중 발생)
|
|
|
|
- (T5) IDE 진단의 transient indexer "cannot resolve import" 경고 — 신규 파일 인덱싱 지연, Gradle 컴파일에서 정상 해소.
|
|
- **OpenAPI snapshot RANDOM_PORT 비결정성** (raw/errors 추출 후보): `OpenApiDriftContractTest` 가 committed snapshot 과 compare 시 매 실행 실패. 원인 = springdoc `/v3/api-docs` 의 `servers` block 이 `@SpringBootTest(RANDOM_PORT)` 의 `http://localhost:<random>` 를 담아 매 run 변동. 두 generation diff 로 단 1줄(`url`) 차이 확인 → canonicalize 단계에서 `servers` 키 제거(drift gate 는 API surface: paths/components/schemas 만 추적, base URL 은 harness noise). 재현/교훈: snapshot gate 는 환경 의존 필드(포트/호스트/타임스탬프/ULID)를 반드시 scrub.
|
|
- **PII masking 검증 regex 의 possessive-quantifier backtracking false-positive** (raw/errors 추출 후보): `UNMASKED_SECRET` detector 가 이미 masked 된 `authorization: Bearer ****` 를 위반으로 오탐. 원인 = optional auth-scheme group `(?:bearer|basic|negotiate\s+)?` 가 lookahead `(?!\*{4})` 실패 시 backtrack 하여 "Bearer" 자체를 secret value 로 재매칭. 해결 = possessive `?+` (`(?:...)?+`) 로 scheme 을 give-back 불가하게. 교훈: "이미 마스킹됐는지" 판정 regex 는 optional prefix 의 backtracking 을 possessive 로 차단해야 함.
|
|
|
|
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
|
|
|
- ArchUnit manual-importer 패턴을 선택한 이유: `@AnalyzeClasses` + `DoNotIncludeTests` suite 가 test 클래스를 볼 수 없어서 `ClassFileImporter` 직접 사용 필수.
|
|
- ArchUnit 에서 "regex negation" 을 사용할 수 없는 경우 복합 predicate 로 표현하는 방법 (`resideInAPackage("..features..").and(resideOutsideOfPackage("..features.sample.."))`).
|
|
- non-vacuity guard 가 필요한 이유: 빈 corpus 스캔 시 rule 이 silently 통과하는 문제 방지.
|
|
- snapshot/golden-master 테스트에서 비결정성(포트/타임스탬프/trace_id/ULID)을 어떻게 다루나 — scrub vs strip, 그리고 "무엇을 계약으로 볼 것인가"(API surface vs 환경 메타) 경계 판단.
|
|
- optional adapter 테스트를 enabled env 에서만 실행하면서 disabled 시 FAILED 아닌 SKIPPED 를 어떻게 보장·검증하나 (`@EnabledIfEnvironmentVariable` + EngineTestKit 으로 skipped/failed 통계 단언).
|
|
- masking 같은 cross-cutting 메커니즘의 SSOT 가 상위 모듈(app-bootstrap)에 있을 때, 하위 모듈(adapter-web) 직렬화 레이어에서 재사용하려면 왜 SSOT relocate 또는 중복이 강제되는가 (의존성 방향 제약).
|
|
|
|
### 블로그·채용공고 연계 글감
|
|
|
|
- ArchUnit 에서 테스트 클래스를 검사할 때 manual-importer 패턴이 필요한 이유 (잠재적 블로그 글감).
|
|
- "violations-as-data" 픽스처 패턴: ArchUnit 규칙의 positive-control + over-block guard 를 명시적 픽스처 클래스로 구조화하는 접근.
|
|
- "계약을 문서가 아니라 테스트로 강제하기": 11 release-blocking gate 를 approvaltests snapshot + registry-drift + ArchUnit isolation + OpenAPI committed-snapshot 으로 묶은 verification suite 설계.
|
|
- regex 로 "이미 마스킹됐는지" 판정할 때 backtracking 함정과 possessive quantifier (PII 로그 마스킹 검증 사례).
|
|
|
|
## 관련 일일 노트
|
|
|
|
- (해당 spec 패스에서 단독 daily-note 추출 없음. 진행은 §결정 사항 + §Audit & Findings 에 직접 기록.)
|
|
|
|
## 완료 후 정리
|
|
|
|
- PR 링크:
|
|
- 리뷰 메모:
|
|
- 머지 결과 / 배포 환경:
|
|
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
|
- `actually-implemented` 항목:
|
|
- `locally-verified` 항목:
|
|
- `prod-verified` 항목:
|
|
- **추출하지 않을 항목** (planned / documented-only / abandoned):
|