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 |
|
|
|
2026-05-21 | in-progress | BR-CA-SKELETON-OPERATIONAL-CONTRACT-010 | project-work-item | ca-skeleton-operational-contract | WI-CA-SKELETON-OPERATIONAL-CONTRACT-010 |
|
1 | 15121967b54182a52d343b3a87b21d692dc3bb7a28edb3f75d306f2b09e74d63 |
branch: feature-contract-verification-test-suite
Layer:
raw/branch-notes/— 운영 계약을 테스트로 강제하는 통합 검증 기준을 정의합니다.
부모 (필수)
- 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) 의 결정/근거/금지 사항을 정제한다.
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
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):
- 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@EnabledIfSpEL/property-placeholder (보완 — env+profile AND 복합 조건 / Spring Environment 바인딩 필요 시; JUnit 5.7+ 동명 어노테이션 import 충돌 주의) ③@Tag+ GradleincludeTags태스크 분리 (보류 — 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
%maskconverter가 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 필수.
- Layer 1 (Logback): custom
진행 중 메모
- 이 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
@EnabledIfEnvironmentVariableprimary + 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.forNamerelease-blocking enumerate.actually-implemented,locally-verified(Gradle :app-bootstrap:test PASS, 4 test methods). - 2026-06-20: suite 전체 구현 완료 (
actually-implemented,locally-verified—./gradlew checkBUILD SUCCESSFUL 1m28s, ca-architect-sentinel PASS 0 blocking). 사용자 확정 결정 2건: ① OpenAPI drift gate = committed-snapshot 동등 비교 (openapiCheckSnapshottask +-PapproveOpenApiChangerefresh,verifyPublicPathSnapshot패턴 미러; 의미론적 additive/breaking 분류는 api-compatibility branch 레이어로 유지). ② delegated 경계 = 이 branch 검증물만 (sample@ConditionalOnPropertywiring ·.githubCI 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 SSOTLogMaskingPatterns는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.sampleallowlist 는 사용자 임의 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(타입 안전, 재사용) vsApprovals.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 파일 없음,openapiCheckSnapshottask 없음, 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(registryenv-keys.yaml:1398, default true,prod_profile_must_be_false, ownerfeature-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):
%maskconverter 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.yamlallowed. - 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 재시작.
- async / custom Logback appender 우회: ListAppender 가 동기 event 만 capture → async appender 로 흐른 PII/token 미탐지. 기대: async appender 도 capture 경로에 포함하거나 별도 assert (Claims To Verify #5,
- 다른 계약 의존:
- 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_ENABLEDflag + sample bean gating 의 owner. 본 branch 는 removal smoke 만 verify (delegated). - raw/branch-notes/feature-operational-error-observability-foundation D10/D19 — envelope
error.categoryenum(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, registryrequired_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 만 존재) / |
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로 허용. 측정 방법: ArchUnitnoClasses().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 문서(frontmattergoverning_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) |
마주친 문제
- 아직 없음.
묶음
- 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
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의serversblock 이@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_SECRETdetector 가 이미 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+DoNotIncludeTestssuite 가 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):