Files
llm-wiki/raw/branch-notes/feature-runtime-context-propagation-contract.md

359 lines
38 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: branch / feature-runtime-context-propagation-contract
source_type: branch-note
status: raw
branch: feature-runtime-context-propagation-contract
parent_branch:
related_projects: [ca-skeleton, ca-tmpl]
governing_docs: [raw/project-notes/ca-skeleton-operational-contract.md]
tags: [branch, observability, concurrency, virtual-threads, context-propagation]
created: 2026-06-09
target_merge:
status_label: in-progress
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-056
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-056
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-LANGUAGE-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-TEST-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-ARCHTEST-001@1]
refines: []
overrides: []
depends_on: [WI-CA-SKELETON-OPERATIONAL-CONTRACT-002, WI-CA-SKELETON-OPERATIONAL-CONTRACT-001, WI-CA-SKELETON-OPERATIONAL-CONTRACT-025, WI-CA-SKELETON-OPERATIONAL-CONTRACT-027, WI-CA-SKELETON-OPERATIONAL-CONTRACT-022]
contract_packet: 1
contract_packet_sha256: c4b5f766a11a0331d04ca0649fd795aa293d04ef0f05fb0e90b569a921481053
---
# branch: feature-runtime-context-propagation-contract
> Layer: `raw/branch-notes/` — 단일 브랜치의 **TODO·결정·진행 기록**. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출. 원본은 raw에 영구 보관.
> `status_label`: `in-progress` | `review` | `merged` | `abandoned`
> **계층 표기**: project 의 직접 자식 branch 는 `parent_branch:` 를 **비워두고** `related_projects` 만 채움.
<!-- section-id: branch-parent -->
## 부모 (필수)
> 이 branch 는 ca-skeleton 운영 계약 project 의 **직접 자식 branch** (project 분해표 §8.0 E영역 row 7). `parent_branch:` 비어있음.
- **Project 의 직접 자식 branch**: [[raw/project-notes/ca-skeleton-operational-contract]] (§8.0 E영역 priority 7: `feature-runtime-context-propagation-contract` — "virtual thread 활성화 + 도메인 context 전파 요구 시점 / Java 21 Scoped Values — boundary B6 의 도메인 확장")
이 branch 가 *확장* 하는 형제 branch (B6 baseline 의 도메인 확장이므로 강결합):
- [[raw/branch-notes/feature-boundary-validation-mapping-contract]] — **B6 (virtual-thread MDC propagation) baseline 의 owner** (D13). 본 branch 는 그 도메인 확장.
본 branch 가 *결정을 위임* 하는 형제 branch (Out of scope, §범위 참조):
- [[raw/branch-notes/feature-operational-error-observability-foundation]] — MDC key 카탈로그 + ID 의미 SSOT (D6/D8/D11/D19)
- [[raw/branch-notes/feature-distributed-tracing-contract]] — W3C trace context 전파 (D5/D7/D8)
- [[raw/branch-notes/feature-background-job-async-contract]] — `@Async` executor TaskDecorator MDC-copy (D5/D6)
- [[raw/branch-notes/feature-tenant-context-policy]] — `tenant_id` lifecycle/policy
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: runtime context capture·propagation·cleanup과 architecture/contract test가 명시된다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1` | Gradle multi-module에서 domain-core·application-core·adapter-*·shared-contract·app-bootstrap·sample-portfolio 책임을 분리한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-LANGUAGE-001@1` | application language는 Java 21 LTS다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1` | framework는 Spring Boot 3.5.14다 | 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]] |
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-ARCHTEST-001@1` | architecture test는 archunit-junit5 1.3.0을 사용한다 | 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 -->
## 목표
ca-skeleton 의 **진단(diagnostic) context 전파**`request_id` / `trace_id` / `correlation_id` 를 inbound filter 에서 MDC 에 심고 virtual thread 위에서 application layer 까지 전달 — 은 **이미 B6 (boundary-validation D13) 에서 구현 완료**(`actually-implemented`: `VirtualThreadMdcPropagationTest`, `VirtualThreadMdcE2ETest`, `no_inheritable_thread_local` ArchUnit rule).
본 branch 는 그 **도메인 확장**이다: 진단용 6개 MDC 키를 넘어서는 **도메인/비즈니스 context**(예: 도메인 식별자)를 virtual thread + structured concurrency(`StructuredTaskScope.fork()`) 경계에서 전파하는 **기본 구현 + 스왑 가능 추상화**를 제공한다.
> **2026-06-09 설계 전환 (baseline=nothing → 기본 구현 + 스왑)**: 초안은 "트리거 전까지 아무것도 선박 안 함(baseline=nothing)"이었으나, **이 skeleton 자신의 rate-limit 선례**(`RateLimiter` 포트 + `FixedWindowRateLimiter` 기본 + `RateLimitAlgorithm`/`Factory` 스왑)에 비춰 과소(under-ambitious)로 판정. rate-limit 의 교훈 = **메커니즘과 값을 분리** — 포트는 값(도메인 key)을 몰라도 추상화 가능. 따라서 *메커니즘*(경계 넘어 capture/restore)을 기본 구현(plain ThreadLocal)으로 선박하고, *값(key)*만 도메인이 등록하도록 전환. 불가능한 부분(`ScopedValue` 기본 — preview, `--enable-preview` 부재)과 도메인 고유 부분(어떤 key)만 deferred.
- **기본 제공**: `DomainContextPropagator` 포트 + `ThreadLocalDomainContextPropagator` 기본 구현 + `DomainContextStrategy` enum + `DomainContextPropagatorFactory` + `DomainContextProperties`(`ca-skeleton.domain-context.strategy`, 기본 `THREAD_LOCAL`). rate-limit 구조 1:1 미러.
- **스왑 가능**: `MICROMETER`(stable, 다중 key/Reactor)·`SCOPED_VALUE`(preview, `--enable-preview` 시) 는 enum 주석 + factory 확장점으로 예약.
- **트리거(값 활성화)**: 도메인 코드가 *비즈니스 식별자를 async/fork 경계 너머로* 요구하는 시점 (project note L2080) — 그때 `DomainContextKey` 상수를 도메인이 선언. seam 은 그 전까지 *동작하지만 전파할 값이 없음*(라우트 없는 `RateLimitKeyResolver` 와 동일).
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- **(S1) 통합 cross-boundary 전파 메커니즘 메타-계약** — virtual thread 활성화(`spring.threads.virtual.enabled=true`) 시 *어느 경계에서 어느 메커니즘이 적용되는지* 의 단일 위임 맵. 특히 background-job 의 `ThreadPoolTaskExecutor`+`TaskDecorator` 모델(pool)과 B6 의 `SimpleAsyncTaskExecutor`(virtual) 전환의 정합 — 현재 어느 형제도 소유하지 않는 seam.
- **(S2) 도메인 context 전파 메커니즘 추상화 + 기본 구현** — `DomainContextPropagator` 포트 + `ThreadLocalDomainContextPropagator` 기본(default) + `DomainContextStrategy` enum + factory 스왑. ✅ **구현됨**(2026-06-09). `ScopedValue`/Micrometer 는 예약 strategy.
- **(S3) fork 경계 명시적 capture/rebind 규칙** — 도메인 context 는 thread/`StructuredTaskScope` fork 마다 *명시적으로 재확립*해야 한다(묵시적 상속 금지). `InheritableThreadLocal` 금지의 도메인-context 판본. ✅ **구현됨**: `wrap(Runnable/Callable)` + `capture()/restore()` API + no-silent-inheritance 테스트.
- **(S4) ScopedValue 전용 신규 ArchUnit enforcement**(활성화 시) — 기존 `no_inheritable_thread_local`(B6 소유)을 cross-cite 하되, Scoped-Value 오용 차단 룰만 *신규 도입*. **deferred**(rule shape 미정, UNSUPPORTED). 단 `domain_context_propagation_primitives_stay_unshipped` 가드(preview API 차단)는 선박됨.
### 제외 범위
> 의도적으로 제외 — 각각 형제 branch 가 SSOT. 본 branch 의 §구현 가이드에 *재결정* 하지 않고 cross-cite 만 한다(CLAUDE.md §15.5 R3).
- **ID 의미 + MDC snake_case 키 카탈로그 + snake↔camel↔kebab 투영** → [[raw/branch-notes/feature-operational-error-observability-foundation]] D6/D8/D11/D19.
- **W3C `traceparent`/`tracestate` 전파, baggage allowlist, B3-forbidden, sampling/exporter** → [[raw/branch-notes/feature-distributed-tracing-contract]] D5/D7/D8.
- **`@Async`/executor `TaskDecorator` MDC-copy(4키) + pool sizing + graceful shutdown** → [[raw/branch-notes/feature-background-job-async-contract]] D5/D6/D7/D8.
- **B6 baseline(virtual-thread filter/MDC 안전성 probe) + `no_inheritable_thread_local` ArchUnit rule** → [[raw/branch-notes/feature-boundary-validation-mapping-contract]] D13(`CleanArchitectureTest.java:623` + `InheritableThreadLocalFixture`).
- **`tenant_id` lifecycle/policy** → `feature-tenant-context-policy`. 본 branch 는 `tenant_id`*consumer/예시* 로만 다룸.
## 근거 (필수, 최소 1개+)
> 도메인 context 메커니즘 결정(S2)의 근거가 되는 외부 자료. `/branch-spec` 자동조사(`wiki-decision-researcher`)가 N=3 alternatives × 공식문서+기술블로그로 생성.
| Source | 정당화하는 결정 |
|---|---|
| [[raw/official-docs/scoped-value-jep-446-506-openjdk]] | D2(ScopedValue) 공식 명세 — immutability / bounded lifetime / StructuredTaskScope inheritance |
| [[raw/company-tech-blogs/scoped-value-structured-concurrency-softwaremill]] | D2(ScopedValue) production 패턴 사례 (SoftwareMill, 2025-09) |
| [[raw/official-docs/micrometer-context-propagation-official]] | D3(Micrometer ContextSnapshot) 공식 API — capture/restore + ThreadLocalAccessor 등록 |
| [[raw/company-tech-blogs/micrometer-context-propagation-line-be-hase]] | D3(Micrometer) production 사례 (LINE / Ryosuke Hasebe, 2025-02) |
| [[raw/official-docs/threadlocal-virtual-threads-java21-oracle]] | D4(plain ThreadLocal) virtual thread 안전성 — per-virtual-thread 독립 copy |
| [[raw/company-tech-blogs/threadlocal-capture-restore-att-israel]] | D4(plain ThreadLocal) TaskDecorator capture-restore 패턴 사례 (AT&T Israel, 2022-04) |
> 형제 branch 결정(foundation D11, distributed-tracing D5/D7, background-job D5/D6, boundary-validation D13)은 외부 Source 가 아니라 *cross-contract 의존* 이므로 §엣지·실패·의존 + Decision Evidence Map 의 Supporting Claims 에 기재.
## TODO
각 항목 옆에 증거 등급 표기.
- [x] (S2) 도메인 context 메커니즘 추상화 + 기본 구현 — 등급: `locally-verified` (`shared-contract` `DomainContextPropagator`/`ThreadLocalDomainContextPropagator`/`DomainContextStrategy`/`DomainContextPropagatorFactory`/`DomainContextSnapshot` + `app-bootstrap` `DomainContextProperties`/`DomainContextConfig`. `:shared-contract:test --tests '*DomainContext*'` 11/11 green)
- [x] (S3) fork 경계 명시적 capture/rebind — 등급: `locally-verified` (`wrap()`/`capture()`/`restore()`; virtual-thread 전파 + no-silent-inheritance + finally-revert 테스트 통과)
- [x] (S1) 통합 boundary→mechanism 위임 맵 (cross-cite siblings, reference-only) — 등급: `documented-only` (`package-info.java` §S1)
- [ ] (S4) ScopedValue 전용 ArchUnit rule 신규 작성 (활성화 시) — 등급: `planned` (UNSUPPORTED_IMPL_DECISION — rule shape 미정, 지어내지 않음)
- [x] **(신규) preview-primitive 가드 — `SCOPED_VALUE` 전략 미선박 강제** — 등급: `locally-verified` (`domain_context_propagation_primitives_stay_unshipped` ArchUnit rule, `CleanArchitectureTest` 47/47 green; production 이 `ScopedValue`/`StructuredTaskScope` 참조 시 fail)
- [ ] 트리거 시점 결정: 도메인 context key 집합 명세 (tenantId? userId? …) — 등급: `needs-confirmation` (도메인이 `DomainContextKey` 상수 선언 시)
- [x] B6 baseline(virtual-thread MDC propagation) 사전확인 — 등급: `actually-implemented` (boundary-validation D13 소유, 본 branch 범위 밖)
## 진행 중 메모
- 2026-06-09 **설계 전환 + 구현 (선택지 B → 기본구현+스왑, SUPERSEDES 아래 baseline=nothing 메모)**: rate-limit 선례 대조에서 baseline=nothing 이 과소로 판정 → **기본 구현 + 스왑 추상화**로 승급 구현. 선박물: `shared-contract/.../concurrency/` 에 `DomainContextKey`·`DomainContextPropagator`·`DomainContextSnapshot`·`DomainContextStrategy`·`DomainContextPropagatorFactory`·`ThreadLocalDomainContextPropagator`(기본) + `app-bootstrap/.../concurrency/` 에 `DomainContextProperties`·`DomainContextConfig`. 테스트: `:shared-contract:test --tests '*DomainContext*'` **11/11 green**(set/get/clear, snapshot 불변, restore revert, virtual-thread `wrap()` 전파, no-silent-inheritance, finally-revert), `:app-bootstrap:test --tests '*CleanArchitectureTest'` **47/47 green**(shared-contract 순수성 + preview-primitive 가드 유지). `MICROMETER`/`SCOPED_VALUE` 는 예약 strategy(enum 주석+factory 확장점). `package-info` 는 baseline=nothing 서술에서 default+swap 서술로 재작성.
- 2026-06-09 **구현(선택지 B 채택, 위 메모로 대체됨)**: 사용자 지시("문서대로 구현, 하나도 빠짐없이")를 future-activation contract 의 `baseline=nothing`(D1)과 양립시키기 위해 **문서 계약 artifact + baseline 가드 테스트**만 선박. (1) `src/shared-contract/src/main/java/dev/caskeleton/shared/concurrency/package-info.java` — S1 위임 맵 + S2 선택 결정표 + S3 fork rebind 규칙 + S4 deferred 표기를 in-repo Javadoc 으로 인코딩(어노테이션 없는 package-info → `.class` 미생성, source-only 의도와 일치). (2) `CleanArchitectureTest` 에 `domain_context_propagation_primitives_stay_unshipped` 신규 ArchUnit rule — D1 강제(production 이 `ScopedValue`/`StructuredTaskScope` 참조 금지). D2/D3/D4 메커니즘·S4 활성화 룰은 **여전히 미선박**(planned/UNSUPPORTED). 검증: `:shared-contract:clean compileJava`, `:app-bootstrap:test --tests '*CleanArchitectureTest'`, `verifyCleanArchitectureDependencies` 모두 green.
- 2026-06-09 **검증 함정 기록**: 첫 arch 테스트 실행이 `:shared-contract:compileJava` UP-TO-DATE 로 새 package-info 가 classpath 에 없어 **stale green** 이었음. `:shared-contract:clean` 후 재실행으로 true green 확보. (preview API 라 위반 fixture 컴파일 불가 → fixture 대신 dormant defence-in-depth rule + 컴파일 게이트 이중방어로 문서화.)
- 2026-06-09: `ScopedValue` 는 ca-tmpl src 에 **0건** — 도메인 context 전파는 전적으로 greenfield/미선박. B6(진단 MDC)만 구현됨.
- 2026-06-09 **빌드 사실(C1 해소)**: ca-tmpl 빌드(`src/build.gradle`, Java 21 toolchain `JavaLanguageVersion.of(21)`)에 `--enable-preview` **없음**. `StructuredTaskScope` 도 0건. → **현재 D2(ScopedValue) 는 빌드 정책 변경 전까지 unavailable**; 트리거 도착 시 기본 후보는 D3/D4.
- `no_inheritable_thread_local` rule 은 `CleanArchitectureTest.java:623` 에 존재하고 본문에서 명시적으로 "feature-boundary-validation-mapping-contract B6" 를 cite — 본 branch 는 재소유 금지, cross-cite.
- background-job D5(`TaskDecorator`)는 src/main 에 **미구현**(`planned`). 즉 pool-vs-virtual 정합(S1 seam)은 *아직 코드로 충돌하지 않은* 미래 정합 대상.
## 결정 사항
> 각 결정 근거는 위 Sources 또는 형제 branch 결정을 가리킴. **2026-06-09 재구성**: D1 이 baseline=nothing → 기본구현+스왑으로 전환. D2/D3/D4 는 *트리거 시 택1* 이 아니라 *seam 뒤 strategy 옵션* — D4(ThreadLocal)가 선박된 기본, D2/D3 는 예약.
- 2026-06-09: (D1) **도메인 context 전파의 기본 구현 + 스왑 추상화를 선박**(SUPERSEDES baseline=nothing). / 이유: rate-limit 선례(메커니즘과 값 분리) — 포트는 도메인 값을 몰라도 추상화 가능하므로 *메커니즘*은 기본 구현(ThreadLocal)으로 선박하고 *값(key)*만 도메인이 등록. / 근거: [[raw/project-notes/ca-skeleton-operational-contract]] L2080(트리거는 이제 *값* 활성화에만 적용) + rate-limit 구조 선례(`RateLimiter`/`RateLimitAlgorithm`/`Factory`).
- 2026-06-09: (D4) **plain ThreadLocal capture-restore 를 기본(default) strategy 로 선박**`THREAD_LOCAL`. / 이유: virtual-thread 안전(per-thread copy) + zero dep + `InheritableThreadLocal`-free + 단순. / 근거: [[raw/official-docs/threadlocal-virtual-threads-java21-oracle]], [[raw/company-tech-blogs/threadlocal-capture-restore-att-israel]].
- 2026-06-09: (D3) **Micrometer Context Propagation 을 예약 strategy(`MICROMETER`)로** — 스왑 조건: stable-API + 다중 key/Reactor 확장. (enum 주석 + factory 확장점, 미선박. C7: virtual-thread 보장 확인 후 활성화.) / 근거: [[raw/official-docs/micrometer-context-propagation-official]], [[raw/company-tech-blogs/micrometer-context-propagation-line-be-hase]].
- 2026-06-09: (D2) **ScopedValue 를 예약 strategy(`SCOPED_VALUE`)로** — 스왑 조건: `--enable-preview` 수용(현재 부재, C1) + StructuredTaskScope 중심 + immutability. (preview API, `domain_context_propagation_primitives_stay_unshipped` 가드로 production 진입 차단.) / 근거: [[raw/official-docs/scoped-value-jep-446-506-openjdk]], [[raw/company-tech-blogs/scoped-value-structured-concurrency-softwaremill]].
- 2026-06-09: (D5) **통합 boundary→mechanism 위임 맵** — 각 경계의 전파는 형제 branch 가 소유; 본 branch 는 *consolidation view* 만 제공(reference-only). / 근거: foundation D11, distributed-tracing D5/D7, background-job D5/D6, boundary-validation D13.
- 2026-06-09: (D6) **fork 경계 명시적 capture/rebind 규칙** — 도메인 context 는 thread/`StructuredTaskScope` fork 마다 명시적 재확립; 묵시적 상속 금지(`InheritableThreadLocal` ban 의 도메인 판본). / 근거: boundary-validation D13(no-silent-inheritance) + `scoped-value-jep-446-506-openjdk#SV-C2`(StructuredTaskScope 내 자동 상속은 *scope 안* 에 한정).
- 2026-06-09: (D7) **ScopedValue 전용 신규 ArchUnit enforcement** — 활성화 시. 구체적 rule shape 는 근거 없음(`UNSUPPORTED_DECISION`). / 기존 `no_inheritable_thread_local`(B6) cross-cite.
## 결정-근거 매핑
> Supporting Claims: 외부 raw 는 `raw/<slug>.md#<ClaimID>`, cross-contract 의존은 형제 branch 의 `D<n>`.
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | 도메인 context **기본 구현 + 스왑 추상화 선박** (포트+default+factory) — ✅ 구현됨 | 항상(기본 제공). 트리거는 *값(도메인 key)* 활성화에만 적용 — 도메인이 `DomainContextKey` 선언 시. | [[raw/project-notes/ca-skeleton-operational-contract]] L2080(값 트리거) + rate-limit 구조 선례(`RateLimiter`/`Factory`) | `governing + repo-precedent` | seam 은 동작하나 도메인 key 0개면 전파 값 없음(라우트 없는 rate-limit 와 동일, 정상) |
| D4 | **plain ThreadLocal capture-restore = 선박된 기본 strategy(`THREAD_LOCAL`)** — ✅ 구현됨 | 기본값. 다중 key/Reactor 면 D3, preview 수용 시 D2 로 스왑. | `raw/official-docs/threadlocal-virtual-threads-java21-oracle.md#TL-VT-C1`, `raw/company-tech-blogs/threadlocal-capture-restore-att-israel.md#ATT-TL-C1` | `official-vendor-doc + company-case-study + locally-verified(11 tests)` | `finally`-clear 는 `wrap()`/`restore()` 가 try-with-resources 로 강제(규율 위험 해소). key 증가 시 D3 권고 |
| D3 | **Micrometer Context Propagation = 예약 strategy(`MICROMETER`)** — 미선박(enum 주석+factory 확장점) | 스왑: stable-API + 다중 key/Reactor 확장. | `raw/official-docs/micrometer-context-propagation-official.md#MCP-C3`, `#MCP-C4`, `raw/company-tech-blogs/micrometer-context-propagation-line-be-hase.md#LN-MCP-C1` | `official-vendor-doc + company-case-study` | 공식 문서가 virtual thread 명시 보장 없음(C7 — 활성화 전 확인); 추가 의존성 |
| D2 | **ScopedValue = 예약 strategy(`SCOPED_VALUE`)** — 미선박(preview 차단) | 스왑: `--enable-preview` 수용(현재 부재 C1) + StructuredTaskScope 중심 + immutability. | `raw/official-docs/scoped-value-jep-446-506-openjdk.md#SV-C1`, `#SV-C2`, `raw/company-tech-blogs/scoped-value-structured-concurrency-softwaremill.md#SM-SV-C1` | `official-standard + company-case-study` | Java 21 **preview**(`--enable-preview` 필요); `domain_context_propagation_primitives_stay_unshipped` 가드가 production 진입 차단 |
| D5 | 통합 boundary→mechanism 위임 맵 (consolidation only, reference-only) | 항상 — 본 branch 는 경계별 전파를 *재결정 안 하고* 위임 맵만 제공 | foundation `D11`, distributed-tracing `D5`/`D7`, background-job `D5`/`D6`, boundary-validation `D13` | `cross-contract (sibling decisions)` | background-job `TaskDecorator`(pool) ↔ B6 `SimpleAsyncTaskExecutor`(virtual) 정합 seam 미소유 — S1 핵심 리스크 |
| D6 | fork 경계 명시적 capture/rebind 규칙 (묵시 상속 금지) | 항상 (도메인 context 활성화 시) | boundary-validation `D13` (no-silent-inheritance), `raw/official-docs/scoped-value-jep-446-506-openjdk.md#SV-C2` | `cross-contract + official-standard` | StructuredTaskScope *안* 자동상속과 *밖* 수동재확립의 경계가 개발자에게 혼동 가능 |
| D7 | ScopedValue 전용 신규 ArchUnit rule (활성화 시) | 도메인 context 활성화 + ScopedValue(D2) 채택 시 | `UNSUPPORTED_DECISION` — 구체 rule shape 권고하는 raw 없음. 기존 `no_inheritable_thread_local`(boundary-validation D13) cross-cite | `none (unsupported)` | rule 부재 시 미래 개발자가 도메인 context 를 ThreadLocal 로 오용/누수 |
## 구현 가이드
> 본 branch 는 **기본 구현 + 스왑 추상화**(rate-limit 패턴)를 선박한다(2026-06-09). §2 의 포트/기본구현/factory 는 `locally-verified`(11 tests); 도메인 key·`MICROMETER`/`SCOPED_VALUE` strategy·S4 활성화 룰만 `planned`/예약.
### 1. Boundary → Mechanism 위임 맵 (D5 — REFERENCE ONLY)
> **Trace**: D5. 각 행의 *실제 호스팅 = sibling branch*. 본 branch 는 consolidation view 만 — 코드 위치는 sibling.
>
> - **UNSUPPORTED_IMPL_DECISION**: (1) `domain-context` 행의 메커니즘은 D2/D3/D4 트리거 선택에 종속 — 트리거 전까지 미정(trade-off: 조기 확정 시 YAGNI 위반). (2) `pool↔virtual 전환 정합` seam 행은 mechanism·owner 모두 미정 — *의도적 deferred*: background-job D5(`TaskDecorator`) 구현 완료 + virtual executor 전환이 동시 성립할 때만 활성화(trade-off: 지금 정하면 미구현 D5 에 대한 근거 없는 가정).
| 경계 (boundary) | 전파 대상 | 메커니즘 | 소유 branch (actual location) | 본 branch 관계 |
|---|---|---|---|---|
| inbound HTTP filter | `request_id`/`correlation_id`/`trace_id` MDC | SLF4J 2.0+ MDC (virtual-thread aware) | boundary-validation D13 (`RequestLoggingFilter.java`) | cross-cite (Out of scope) |
| outbound HTTP / message | W3C `traceparent`/`tracestate`, baggage(`tenant_id`,`request_id`) | Micrometer Tracing | distributed-tracing D5/D7/D8 | cross-cite (Out of scope) |
| `@Async` `ThreadPoolTaskExecutor` (pool) | MDC 4키(`request_id`/`trace_id`/`correlation_id`/`tenant_id`) | `TaskDecorator` copy (`planned`, 미구현) | background-job D5/D6 | cross-cite (Out of scope) |
| virtual-thread carrier (`SimpleAsyncTaskExecutor`) | 진단 MDC | SLF4J 2.0+ MDC, `InheritableThreadLocal` 금지 | boundary-validation D13 (`no_inheritable_thread_local` `CleanArchitectureTest.java:623`) | cross-cite (Out of scope) |
| **`StructuredTaskScope.fork()` / thread handoff** | **도메인 context** | **`DomainContextPropagator.wrap()`/`capture()` (기본 `THREAD_LOCAL`)** ✅ 선박 | **본 branch (S2/S3)** | **in scope — 구현됨** |
| pool↔virtual 전환 정합 (`TaskDecorator` semantics when executor is not a pool) | — | — | **미소유 seam** | **본 branch (S1) 신규** |
### 2. 도메인 context 추상화 + 기본 구현 (D1/D4 ✅ 구현
> **Trace**: D1(기본구현+스왑) + D4(`THREAD_LOCAL` 기본). Supporting: `TL-VT-C1`, `ATT-TL-C1` + rate-limit 선례. 예약 strategy 근거: `SV-C1/SV-C2`(D2), `MCP-C3/MCP-C4`(D3).
>
> **선박된 코드** (`:shared-contract:test --tests '*DomainContext*'` 11/11 green):
>
> | 요소 | 클래스 | 위치 |
> |---|---|---|
> | 포트 | `DomainContextPropagator` | `shared-contract/.../concurrency/` |
> | 기본 구현 | `ThreadLocalDomainContextPropagator` (plain ThreadLocal) | 〃 |
> | strategy enum | `DomainContextStrategy` (`THREAD_LOCAL` 기본; `MICROMETER`/`SCOPED_VALUE` 주석) | 〃 |
> | factory(확장점) | `DomainContextPropagatorFactory` (switch) | 〃 |
> | 키(도메인 확장점) | `DomainContextKey<T>` | 〃 |
> | hand-off | `DomainContextSnapshot` + `wrap()`/`capture()`/`restore()` | 〃 |
> | Spring 와이어링 | `DomainContextProperties`(`ca-skeleton.domain-context.strategy`) + `DomainContextConfig` | `app-bootstrap/.../concurrency/` |
>
> - **여전히 planned/예약**: 도메인이 선언할 `DomainContextKey` 상수(C5), `MICROMETER` strategy(C7 확인 후), `SCOPED_VALUE` strategy(`--enable-preview` 시 C1), S4 활성화 룰(UNSUPPORTED).
strategy 스왑 규칙 (`DomainContextStrategy` / factory):
```text
IF (build 가 --enable-preview 수용) AND (StructuredTaskScope 중심) AND (context immutable)
THEN ScopedValue # D2 — fork 자동상속(scope 내) + immutability
ELIF (stable-API only) AND (Reactor 확장 가능성 OR 다중 domain key)
THEN Micrometer ContextSnapshot # D3 — ThreadLocalAccessor 등록 1회 + captureAll()
ELSE plain ThreadLocal + capture-restore wrapper # D4 — 1~2 key, 최소 추상화
# 2026-06-09 빌드 사실(C1): ca-tmpl 빌드에 --enable-preview 없음 + StructuredTaskScope 0건
# → 현재 IF(D2) 가지는 빌드 정책 변경 전까지 dead. 트리거 시 ELIF/ELSE 부터 평가.
# C5(domain key 수) 미정 시 폴백 순서: 기본 D4(plain TL, 1~2 key) → key 증가/Reactor 도입 시 D3.
```
### 3. fork 경계 명시적 capture/rebind 규칙 (D6 — planned)
> **Trace**: D6. Supporting: boundary-validation D13(no-silent-inheritance, `actually-implemented`) + `scoped-value-jep-446-506-openjdk.md#SV-C2`.
- 도메인 context 는 **thread/`StructuredTaskScope` fork 를 넘을 때 명시적으로 재확립**한다. 묵시적 상속(`InheritableThreadLocal`)은 금지 — B6 가 이미 `no_inheritable_thread_local`(`CleanArchitectureTest.java:623`)로 차단(cross-cite, 재작성 금지).
- 단 ScopedValue(D2)는 *`StructuredTaskScope` scope 안* fork 에서는 자동 상속(SV-C2) — 이 한 경우만 예외이며 scope *밖* fork 는 여전히 명시적 재확립 필요.
- 실패 동작: capture/rebind 누락 시 도메인 context 유실 → 계약 위반(테스트로 감지, S4).
### 4. ScopedValue 전용 ArchUnit enforcement (D7 — UNSUPPORTED_IMPL_DECISION
> **Trace**: D7. **UNSUPPORTED_IMPL_DECISION**: 구체적 rule shape(무엇을 noClasses/should 로 차단할지)를 권고하는 raw 없음. 활성화 시 신규 작성 대상이며, 그 전까지 *기존* `no_inheritable_thread_local`(B6, boundary-validation D13)만 유효. trade-off: 지금 rule 을 지어내면 근거 없는 결정.
- REFERENCE ONLY: `no_inheritable_thread_local` (actual location: `app-bootstrap` `CleanArchitectureTest.java:623`, owner=boundary-validation D13).
- 신규(활성화 시 본 branch host): 도메인 context 를 `ThreadLocal` 로 오용/누수 차단하는 rule — *형식 미정*.
## 엣지·실패·의존
- **실패·엣지 경로**:
- `--enable-preview` 가 CI/build 정책에서 거부됨 → D2(ScopedValue) 불가, D3/D4 로 강등.
- ca-tmpl 이 `StructuredTaskScope` 를 전혀 사용하지 않음(현재 grep 0건) → D2 의 fork 자동상속 이점 소멸, D3/D4 와 동등.
- D4 의 `finally`-clear 누락 → 같은 virtual thread 내 후속 단계에서 stale 도메인 context 읽기.
- ScopedValue ↔ OTel `ContextStorage`(attach/detach) 비호환 → distributed-tracing 의 trace context 와 도메인 context 공존 시 충돌 가능. ⚠️ **근거 raw 미등록** — 자동조사 시 secondary 로만 언급된 별도 SoftwareMill OTel 아티클. 활성화(D2 채택) 전 `raw/company-tech-blogs/` 에 정식 등록 후 이 엣지를 설계 제약으로 승격할 것(미등록 상태로 설계 판단에 사용 금지).
- **다른 계약 의존**:
- [[raw/branch-notes/feature-boundary-validation-mapping-contract]] 의 `D13` (B6 baseline + `no_inheritable_thread_local`) 에 의존 — 본 branch 는 그 위에 도메인 확장만 얹음. B6 가 바뀌면(예: MDC 위임 대상 변경) 본 branch S3 규칙 영향.
- [[raw/branch-notes/feature-operational-error-observability-foundation]] 의 `D11` (MDC 키 카탈로그) 에 의존 — 도메인 context 는 이 6키와 *별도 채널* 임을 전제.
- [[raw/branch-notes/feature-background-job-async-contract]] 의 `D5`/`D6` (TaskDecorator, pool) 와 *seam* — pool↔virtual 전환 정합(S1)이 본 branch 신규 결정 영역.
- [[raw/branch-notes/feature-distributed-tracing-contract]] 의 `D8` (baggage allowlist=`tenant_id`,`request_id`) 에 의존 — 도메인 context 를 baggage 로 전파하려면 이 allowlist 와 충돌하지 않아야 함.
- [[raw/branch-notes/feature-tenant-context-policy]] — `tenant_id` 는 그 branch 소유. 본 branch 는 consumer/예시로만.
## 검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| (C1) Java 21 LTS 에서 `ScopedValue``--enable-preview` 없이 컴파일 불가 | preview API 여부가 빌드 정책을 좌우(D2 선결조건) | `ca-tmpl` `build.gradle.kts` compileJava options + JEP 446/506 직접 확인 | `needs-confirmation` |
| (C2) ca-tmpl 이 `StructuredTaskScope` 를 도메인 경로에서 사용/계획 | D2 fork 자동상속 이점의 전제 | `grep -r StructuredTaskScope src` (현재 0건) + 도메인 온보딩 계획 확인 | `planned` |
| (C3) `io.micrometer:context-propagation` 이 Spring Boot 3.5.x starter 로 classpath 에 transitive 존재 | D3 의 추가 의존성 여부 | `./gradlew dependencies` 의존성 트리 grep | `needs-confirmation` |
| (C4) plain ThreadLocal + `TaskDecorator``SimpleAsyncTaskExecutor`(virtual) 에서 동작 | AT&T 사례는 2022(Loom GA 이전) — virtual 미검증 | **D4 채택 결정 전 사전 spike 의무**: virtual thread executor 통합 테스트 작성 | `planned` |
| (C5) 트리거 시점의 *도메인 context key 집합*(tenantId? userId? …) | 미정 — key 수가 D3 vs D4 선택을 가름 | 도메인 온보딩 시 use case 별 필요 식별자 명세 | `needs-confirmation` |
| (C6) pool(`TaskDecorator`)↔virtual(`SimpleAsyncTaskExecutor`) 전환 시 context-copy semantics 정합(S1 seam) | 어느 형제도 미소유; background-job D5 미구현 | background-job TaskDecorator 구현 후 virtual 전환 통합 테스트 | `planned` |
| (C7) Micrometer `ContextSnapshot` `captureAll()`/`setThreadLocals()` 가 virtual thread 환경에서 안전 | 공식 문서가 virtual thread 명시 보장 없음(plain TL 간접 지지뿐) | Spring Boot 3.3+ 릴리즈 노트 / Micrometer CHANGELOG 의 virtual thread 호환성 명시 raw 등록, 또는 D3 채택 전 통합 테스트 | `needs-confirmation` |
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
> `/coverage` 가 채우는 생성물(손유지 금지, 실행 시마다 재생성). governing 문서(`raw/project-notes/ca-skeleton-operational-contract.md`)가 요구하는 관심사 커버리지. 기준: `rules/coverage-gate.md`.
> 2026-06-09 coverage-auditor 판정: **Covered** (Blocking 0 / Should-fix 0 / Advisory 1).
| 관심사 (governing doc 출처) | 상태 | owner | 심각도 | 근거 |
|---|---|---|---|---|
| 도메인 context 전파 메커니즘 선택 계약 (§8.0 E row 7) | covered-here | — | — | D1 + D2/D3/D4 조건부 + §구현 §2 선택표 |
| virtual thread + fork 경계 명시적 capture/rebind 규칙 (§8 async boundary, §15) | covered-here | — | — | D6 |
| 통합 boundary→mechanism 위임 맵 (§8 전 경계 전파) | covered-here | — | — | D5 (§구현 §1 표) |
| 기본 구현 + 스왑 추상화 (§8.0 E row 7, rate-limit 패턴) | covered-here | — | — | D1 (포트+default+factory, L2080 값 트리거) |
| pool↔virtual 전환 정합 seam (§15/§18 미소유 신규) | covered-here | — | — | D5 S1 + C6 |
| ScopedValue 전용 ArchUnit enforcement (§15) | covered-here | — | Advisory | D7 (UNSUPPORTED_DECISION 라벨) |
| MDC key 카탈로그 + snake↔camel↔kebab (§8/§21) | delegated | [[raw/branch-notes/feature-operational-error-observability-foundation]] (D6/D8/D11/D19) | OK | §범위 Out of scope + §엣지 의존 |
| W3C traceparent/baggage/sampling (§8 Distributed Tracing) | delegated | [[raw/branch-notes/feature-distributed-tracing-contract]] (D5/D7/D8) | OK | §범위 Out of scope + §엣지 의존 |
| @Async TaskDecorator MDC-copy + pool sizing (§15/§18) | delegated | [[raw/branch-notes/feature-background-job-async-contract]] (D5/D6/D7/D8) | OK | §범위 Out of scope + §구현 §1 표 |
| B6 baseline virtual-thread MDC + no_inheritable_thread_local rule (§15) | delegated | [[raw/branch-notes/feature-boundary-validation-mapping-contract]] (D13) | OK | §목표 + 코드 `CleanArchitectureTest.java:623` |
| tenant_id lifecycle/policy (§19 Tenant Policy) | delegated | [[raw/branch-notes/feature-tenant-context-policy]] | OK | §범위 Out of scope + §엣지 의존 |
| ScopedValue↔OTel ContextStorage 비호환 (§8 공존) | covered-here | — | Advisory | §엣지 open risk (근거 raw 미등록 — 활성화 전 등록 의무) |
## 마주친 문제
- (없음)
## 묶음 (이 branch에서 파생된 자료)
<!-- GENERATED: sources:start -->
- [[raw/company-tech-blogs/micrometer-context-propagation-line-be-hase]]
- [[raw/company-tech-blogs/scoped-value-structured-concurrency-softwaremill]]
- [[raw/company-tech-blogs/threadlocal-capture-restore-att-israel]]
- [[raw/official-docs/micrometer-context-propagation-official]]
- [[raw/official-docs/scoped-value-jep-446-506-openjdk]]
- [[raw/official-docs/threadlocal-virtual-threads-java21-oracle]]
<!-- GENERATED: sources:end -->
<!-- GENERATED: blog-topics:start -->
- [[raw/blog-topics/java21-context-propagation-strategy-virtual-threads-2026-07-02]]
<!-- GENERATED: blog-topics:end -->
### Sub-branches (세부 작업)
- (없음)
### 오류 기록 (이 branch 작업 중 발생)
- (별도 raw/errors 파일 불필요 — 진행 중 메모에 인라인 기록) 2026-06-09 "stale green": Gradle `:shared-contract:compileJava` UP-TO-DATE 로 새 package-info 가 ArchUnit classpath 에 미반영되어 첫 실행이 가짜 green. 교훈 = 새 소스 추가 후 arch 테스트는 해당 모듈 `clean` 후 재실행. 재사용 가치 낮아 derived 파일 생성 안 함.
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- (후보) "virtual thread 에서 MDC/context 가 왜 안 깨지는가, InheritableThreadLocal 은 왜 금지했는가" — B6 + 본 branch 도메인 확장
### 강의 (이 작업을 위해 학습한 강의)
- (없음)
### job-posting tie-ins (이 작업에서 파생된 글감)
- (후보) "Java 21 ScopedValue vs Micrometer Context Propagation vs ThreadLocal — virtual thread 시대의 context 전파 선택"
- derived blog: 생성 전
### 외부 근거 자료 (Sources — 자동조사 생성)
- [[raw/official-docs/scoped-value-jep-446-506-openjdk]]
- [[raw/company-tech-blogs/scoped-value-structured-concurrency-softwaremill]]
- [[raw/official-docs/micrometer-context-propagation-official]]
- [[raw/company-tech-blogs/micrometer-context-propagation-line-be-hase]]
- [[raw/official-docs/threadlocal-virtual-threads-java21-oracle]]
- [[raw/company-tech-blogs/threadlocal-capture-restore-att-israel]]
## 관련 일일 노트
- (없음)
## 완료 후 정리
> 머지/종료 시점에 채움. `/ingest`가 이 섹션 기준으로 wiki/projects/에 추출.
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **머지 결과 / 배포 환경**: 로컬 검증 완료(`:shared-contract:test --tests '*DomainContext*'` 11/11, `:app-bootstrap:test --tests '*CleanArchitectureTest'` 47/47). prod 미배포.
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목: B6 baseline 은 boundary-validation 소유(본 branch 추출 대상 아님)
- `locally-verified` 항목: (D1) 도메인 context 기본구현+스왑 추상화 — `DomainContextPropagator` 포트 + `ThreadLocalDomainContextPropagator` 기본 + `DomainContextStrategy`/`Factory` 스왑 + `wrap()/capture()/restore()`(S3) + Spring 와이어링 + `domain_context_propagation_primitives_stay_unshipped` 가드. rate-limit 패턴 미러.
- `prod-verified` 항목:
- **추출하지 않을 항목** (planned / documented-only / abandoned): 도메인 `DomainContextKey` 상수(C5, 도메인 몫), `MICROMETER`/`SCOPED_VALUE` 예약 strategy(D3/D2 미선박), S4 활성화 룰(D7 UNSUPPORTED), D5(reference-only consolidation)