fix: 하네스 제거 및 keycloak 문서 보강
This commit is contained in:
@@ -1 +0,0 @@
|
||||
../../vault/10-projects/ca-skeleton-operational-contract/branch-notes/feature-runtime-context-propagation-contract.md
|
||||
@@ -0,0 +1,358 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user