Files
llm-wiki/raw/branch-notes/feature-contract-verification-test-suite.md
T

49 KiB

title, source_type, status, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, contract_packet_sha256
title source_type status branch parent_branch related_projects governing_docs tags created target_merge status_label id kind project work_item inherits refines overrides depends_on contract_packet contract_packet_sha256
branch / feature-contract-verification-test-suite branch-note raw feature-contract-verification-test-suite
ca-skeleton
raw/project-notes/ca-skeleton-operational-contract
branch
ca-skeleton
test
contract
verification
2026-05-21 in-progress BR-CA-SKELETON-OPERATIONAL-CONTRACT-010 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-010
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-OPENAPI-001@1
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-TEST-001@1
1 15121967b54182a52d343b3a87b21d692dc3bb7a28edb3f75d306f2b09e74d63

branch: feature-contract-verification-test-suite

Layer: raw/branch-notes/ — 운영 계약을 테스트로 강제하는 통합 검증 기준을 정의합니다.

부모 (필수)

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) 의 결정/근거/금지 사항을 정제한다.

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: release-blocking contract suite가 OpenAPI drift를 검출한다

상속한 프로젝트 결정

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

브랜치 지역 결정

기존 branch-local 결정은 아래 ## Decision Evidence Map / 결정-근거 매핑의 D-row가 소유하며 이 packet에서 복제하지 않는다.

Decision ID Decision Relation Supporting Claims Status

선언한 예외

Override ID Overrides Reason Approval Status

목표

이 skeleton의 핵심은 기능이 아니라 계약입니다. branch별 기준이 문서에만 있으면 쉽게 깨집니다. 공통 contract verification suite로 response, log, env, boundary, repository capability, adapter failure mapping을 강제합니다.

  • 이슈:
  • PR:

범위

포함 범위

  • 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):
  • 검토한 대안:
  • 비교 핵심: 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.yamllog 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 LogMaskingPatternsapp-bootstrap 소재인데 DTO 가 사는 adapter-webapp-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-contractcontract/.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: plannedsample-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: partialSqlLoggingForbiddenContractTest + 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 재시작.
  • 다른 계약 의존:

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)

마주친 문제

  • 아직 없음.

묶음

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-docsservers 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):