1 정의
Bing (Kohavi et al. 2009), LinkedIn (Xu et al. 2015), Google (Tang et al. 2010) 의 실험 플랫폼은 모두 다음 4 컴포넌트로 구성된다 (Kohavi, Tang, Xu, 2020, Ch.4.5).
- Experiment Definition / Set-up / Management — 사양 작성·iteration 관리·검증
- Experiment Deployment — variant assignment service + production 코드 변경
- Experiment Instrumentation — 사용자 행동 + variant·iteration 로깅
- Experimentation Analytics — data processing → computation → visualization
이 글은 첫 3 개 컴포넌트를 다루고, 4 번째 (Analytics) 와 변종 배정 확장 (Single-Layer / Concurrent) 은 후속 글 F4-4 에서 다룬다.
플랫폼의 목표는 실험을 셀프서비스로 만드는 것이다. 즉, 실험자가 데이터 사이언티스트나 플랫폼 엔지니어의 hands-on 도움 없이도 안전하고 신뢰할 수 있는 실험을 시작·중단·분석할 수 있어야 한다.
2 개념 및 원리
2.1 4 컴포넌트의 데이터 흐름
┌──────────────────────────────────────────────────────────────┐
│ 1. Definition │
│ 실험 사양 (owner·name·dates·variants·iteration·targeting) │
│ ↓ stored in experiment system configuration │
└────────────────┬─────────────────────────────────────────────┘
│ pushed to runtime
▼
┌──────────────────────────────────────────────────────────────┐
│ 2. Deployment │
│ - Variant Assignment Service: f(user_id, exp_id, layer) │
│ - Production code: 3 가지 architecture │
│ - Atomicity 보장 (분산 서버 일관성) │
└────────────────┬─────────────────────────────────────────────┘
│ user actions occur
▼
┌──────────────────────────────────────────────────────────────┐
│ 3. Instrumentation │
│ - 기본 로깅 (user actions, system performance) │
│ - 실험 로깅 (variant_id, iteration_id, exp_id) │
│ - Counterfactual logging (선택, Run/Fly 단계) │
└────────────────┬─────────────────────────────────────────────┘
│ logs flow to data warehouse
▼
[Analytics — F4-4 에서 다룸]
각 컴포넌트는 이전 컴포넌트의 정확성을 가정 한다. Definition 이 모호하면 Deployment 가 잘못 된 variant 를 노출하고, Deployment 가 결함이면 Instrumentation 데이터가 오염되고, Instrumentation 이 누락되면 Analytics 가 의미 없는 숫자를 만든다. 이 chain-of-trust 가 실험 시스템 설계의 핵심이다.
이 4-way 분할은 임의가 아니라 의무 (responsibility) 의 자연스러운 경계 다.
- Definition 은 인간이 다루는 영역 (UI, 협업) — 변경이 빠르고 실험자 친화 도구가 필요
- Deployment 는 실시간 시스템 영역 — 분산·atomicity·성능이 핵심
- Instrumentation 은 데이터 파이프라인 영역 — 일관성·완전성·schema evolution
- Analytics 는 통계·시각화 영역 — 정확성·해석 가능성
각 영역의 비기능 요구 (latency·consistency·correctness·usability) 가 다르므로 한 시스템으로 묶으면 모든 영역에서 trade-off 가 발생한다. 4-way 분할은 각 영역이 독립적으로 진화 하도록 허용한다 — Analytics 의 새 통계 기법 도입이 Deployment 의 안정성을 깨뜨리지 않는다.
소프트웨어 아키텍처 일반 원칙 (separation of concerns) 의 실험 도메인 적용이라고 볼 수 있다.
3 Component 1 — Experiment Definition / Set-up / Management
3.1 실험 사양의 최소 필드
플랫폼이 실험 사양을 저장하기 위해 필요한 최소 필드 (Kohavi, Tang, Xu, 2020, Ch.4.5).
| 필드 | 의미 | 누락 시 결과 |
|---|---|---|
| owner | 실험 소유자 | 책임자 모호 → 출시 결정 정체 |
| name | 인간 친화 이름 | 검색·메타분석 불가 |
| description | 가설·목적 | 6 개월 후 왜 이 실험이 있었는지 모름 |
| start / end dates | 시작·종료 시점 | iteration 비교 불가 |
| variants | 처치 정의 (Control + Treatment N 개) | variant 식별 불가 |
| iteration | 현재 반복 (피드백·ramping 단계) | 시점별 결과 분리 불가 |
| targeting | 대상 사용자 조건 (country·device·…) | 트래픽 제어 불가 |
| randomization unit | user·session·device | SUTVA 분석 잘못됨 |
이 사양은 데이터베이스에 저장되어 runtime 에 push 된다. 변경 시 실험 ID·iteration ID 자동 발급 으로 추적 가능성 확보.
3.2 Iteration — 실험의 시간적 진화
저자들은 한 실험이 여러 iteration 을 가질 수 있다고 강조한다. iteration 의 발생 이유.
- 버그 수정 — 실험 중 발견된 결함 수정 후 재실행
- 점진 노출 (ramping) — 1% → 5% → 20% → 50% (Ch.15)
- 링 (rings) — 개발자 → 사내 → 외부 일부 → 전체
같은 실험의 1% 노출과 50% 노출은 사실상 다른 실험 이다. 표본 크기·power·세그먼트 다양성이 모두 다르기 때문에 결과 해석이 달라진다.
만약 iteration 을 별도 추적하지 않으면 다음 일이 일어난다.
- 1% 단계에서 측정된 효과 +2% 와 50% 단계의 효과 +0.5% 가 같은 데이터에 섞여 평균 +1% 로 보고
- 실제로는 1% 단계 사용자가 early adopter 라 효과가 컸고, 50% 는 일반 사용자라 효과가 작은 것
- 잘못된 평균이 출시 결정에 사용됨
iteration 별로 분리해 보면 Treatment effect heterogeneity over time 을 발견하고, 일반 사용자 에 대한 진정한 효과를 추정할 수 있다. 이는 단순 데이터 정리가 아니라 인과 식별의 도구 다.
3.3 권한·검증 메커니즘
플랫폼이 제공해야 할 핵심 기능.
- Draft 작성·편집·저장 — 실험을 즉시 시작하지 않고 검토 가능
- Draft vs Running 비교 — 새 iteration 의 변경 사항 시각화
- History / Timeline — 종료된 실험도 archive 에서 재조회 가능
- 자동 ID 할당 — 실험 ID·variant ID·iteration ID 가 instrumentation 코드에 들어감
- 검증 (validation) — configuration 충돌, 잘못된 targeting (예: 빈 audience), 누락 필드 자동 감지
- 권한 비대칭 — 실험 시작은 owner 또는 권한자만, 중지는 누구나
왜 시작은 권한 제한, 중지는 자유인가? harm 의 비대칭성 때문이다.
- 잘못 시작된 실험 — 실제 사용자가 나쁜 처치에 노출되어 활성 피해 발생
- 잘못 중지된 실험 — 단지 학습 기회 1 회 손실, 사용자 피해 0
잘못된 시작의 비용 (사용자 이탈·매출 손실) 이 잘못된 중지의 비용 (실험 재시작 1 주 지연) 보다 훨씬 크다. 이 비대칭은 자유주의적 권한 모델 을 정당화한다 — 누구나 stop 할 수 있되, start 는 책임자만.
이 패턴은 의료 분야 (임상시험 DSMB 의 stopping rule), 항공 (조종사 누구나 중단 가능), 원자력 (oS̄ 스크램 버튼) 에도 동일하게 적용된다. 안전 critical 시스템의 일관된 설계 원칙이다.
단, 자동 alert 가 owner 에게 가서 무엇이 왜 중지되었는지 즉시 알도록 한다. 그렇지 않으면 악의적 stop 이 가능해진다.
3.4 Fly 단계의 추가 기능
Run·Fly 단계로 진입하면 다음이 추가된다.
- 자동 ramping — 가드레일 통과 시 1% → 5% → 50% 자동 진행 (Ch.15 SQR)
- NRT alerting — 분 단위 모니터링, 가드레일 위반 시 자동 차단
- Bad experiment 자동 감지 — 통계적 이상치 자동 종료
이 기능들은 인지 부담을 시스템으로 흡수 한다. Fly 단계 조직은 매일 수십 건이 ramping 중이라 사람이 일일이 점검할 수 없다.
4 Component 2 — Experiment Deployment
4.1 Variant Assignment Service
실험 사양이 결정된 후, 사용자 요청이 들어왔을 때 어느 variant 를 노출할지 결정하는 서비스다.
핵심 속성:
\[\text{variant} = f(\text{user\_id}, \text{exp\_id}, \text{layer\_id})\]
여기서 \(f\) 는 cryptographic hash 함수 (보통 MD5·SHA-1).
| 속성 | 의미 | 위반 시 결과 |
|---|---|---|
| 결정성 (deterministic) | 같은 user·exp 는 항상 같은 variant | 사용자 경험 일관성 깨짐 |
| 균등성 (uniform) | bucket 분포가 평균적으로 같음 | SRM 발생 |
| 독립성 (independent) | layer 간 배정이 무관 | 직교성 위반 |
Atomicity 도 같이 보장해야 한다 — 분산 서버 환경에서 한 사용자 요청이 여러 child service 에 fan-out 되었을 때, 모든 child 가 같은 variant 를 봐야 한다.
검색 엔진을 예로 들자. 사용자 검색 요청 1 건이 다음 분산 처리를 거친다.
검색 요청
├→ 인덱스 서버 1 (a~h 단어)
├→ 인덱스 서버 2 (i~p 단어)
├→ 인덱스 서버 3 (q~z 단어)
├→ 광고 서버
└→ 답변 통합 서버
ranking 알고리즘 A/B 실험 중인데, 인덱스 서버 1·2 는 새 알고리즘 (Treatment) 을 사용하고 서버 3 은 아직 old 알고리즘 (Control) 을 사용한다고 가정. 같은 검색 결과 페이지에 두 알고리즘의 출력이 섞이면:
- 사용자 시점에서 일관성이 깨진 결과 — 어떤 단어로 검색하느냐에 따라 결과 품질이 다름
- 데이터 시점에서 인과 효과 측정 불가 — 어떤 부분이 Treatment 효과인지 분리 불가
- 인스턴스 시점에서 디버깅 악몽 — 같은 사용자가 다른 결과를 받는 경우 reproduce 불가
해결책: parent service (답변 통합) 가 한 번 variant 를 결정하고 child 에 전파. 이는 분산 시스템의 idempotency 와 tracing 패턴과 동일하다.
4.2 3 가지 Production 코드 아키텍처
Variant 가 결정된 후 코드가 어떻게 분기하느냐에 따라 3 가지 패턴.
4.2.1 Architecture 1: Code Fork
variant = getVariant(userId)
if variant == "Treatment":
button_color = "red"
else:
button_color = "blue"- 장점: variant 결정이 코드 변경 지점에 가까움 → triggering 자연스러움 (Treatment·Control 모두 affected user 만 포함, Ch.20)
- 단점: 분기 코드 부채. 한 코드베이스에 수십 개의 if-else 가 누적되면 유지 비용 폭증
4.2.2 Architecture 2: Parameterized
variant = getVariant(userId)
button_color = variant.getParam("buttonColor")
# Control: button_color = "blue" (default)
# Treatment: button_color = "red" (variant param)- 장점: 코드 분기 제거, 추가는 파라미터만 새로 정의
- 단점: 모든 변경 가능 항목을 미리 파라미터로 추상화해야 함 (선견지명 필요)
4.2.3 Architecture 3: Early Assignment + Config Push
# variant 결정은 요청 진입점에서 한 번
config = pushedConfig # 모든 파라미터 + variant 정보
button_color = config.getParam("buttonColor")- 장점: 성능 (파라미터 lookup 캐시 가능). 수천 파라미터 규모에서 필수.
- 단점: variant 결정 시점이 코드 변경 지점에서 멀어짐 → triggering 분석 어려움
4.3 3 아키텍처 비교
| 측면 | Code Fork | Parameterized | Early + Config |
|---|---|---|---|
| Triggering 용이성 | 높음 | 높음 | 낮음 (counterfactual logging 필요) |
| 코드 부채 | 누적 | 적음 | 적음 |
| 성능 | 보통 | 보통 | 가장 좋음 (캐시) |
| 도입 난이도 | 낮음 | 중간 | 높음 (전체 시스템 재설계) |
| 확장성 (파라미터 천 개+) | 어려움 | 보통 | 좋음 |
저자들이 보고한 실제 선택.
- Google — Architecture 1 → 3 으로 전환 (성능 + 코드 부채)
- Bing — Architecture 3
- Microsoft Office — Architecture 2 의 변형 (bug ID 를 파라미터로 전달, 3 개월 후 제거 alert)
이 트레이드오프는 현재의 부채 (technical debt) 와 미래의 부채 사이의 선택이다.
- Architecture 1 (Code Fork) — 빠른 도입, 점진 누적되는 부채
- Architecture 3 (Early + Config) — 큰 초기 투자, 누적 부채 없음
조직의 단계가 결정 인자다.
- Crawl·Walk — Architecture 1 이 합리적. 실험 빈도가 낮아 부채가 누적되기 전에 cleanup 가능
- Run·Fly — Architecture 3 이 필수. 천 개 파라미터·수만 실험에서 코드 분기는 유지 불가능
Microsoft Office 의 변형 (Architecture 2 + 자동 cleanup alert) 은 흥미롭다 — 부채 누적의 자연 적인 해법은 부채를 제거할 의무를 시스템에 내장 하는 것이다. 3 개월 후 자동 알림은 인간의 forgetfulness 를 시스템이 보완하는 패턴이다. 비슷한 패턴이 deprecation warning, expiration date, TTL 등에 나타난다.
4.4 Triggering 의 영향 (간단히)
Architecture 1 에서는 variant 결정이 코드 변경 지점에 있어 affected user 만 데이터에 포함 된다. Architecture 3 에서는 variant 결정이 요청 진입점이라 모든 사용자가 데이터에 포함 — 실제로는 변경 영향이 없는 사용자까지. 이는 statistical power 를 떨어뜨린다.
해결책은 counterfactual logging (Ch.20 에서 상세). Treatment 사용자에게 “Control 이었다면 무엇을 봤을지” 함께 로깅한다. 이 로그를 통해 affected user 만 사후 필터링 가능. 그러나 counterfactual logging 자체가 비용이 크다 — 두 가지 결과를 모두 계산해야 하므로.
5 Component 3 — Experiment Instrumentation
5.1 무엇을 로깅하는가
Instrumentation 은 두 layer 로 분리된다.
- 기본 instrumentation — 사용자 행동 (page views·clicks·conversions) + 시스템 성능 (latency·errors)
- 실험 instrumentation — 모든 사용자 요청·인터랙션에 대해 다음 추가 정보:
- variant_id (어떤 처치였는가)
- iteration_id (몇 번째 반복인가)
- exp_id (어떤 실험이었는가)
- timestamp (시점)
기본 instrumentation 은 새 기능 배포 시 반드시 업데이트 되어야 한다. 새 버튼·새 페이지가 기존 instrumentation schema 에 없으면 클릭이 추적되지 않아 효과 측정 불가.
5.2 Iteration 로깅이 왜 critical 한가
실험이 시작되거나 ramping 될 때 모든 서버가 동시에 새 iteration 으로 전환되지 않는다. 일부 서버는 아직 old iteration, 일부는 new. 이 시점에 사용자 요청의 iteration 을 함께 로깅 하지 않으면 다음 문제가 발생한다.
- iteration 1 (1% ramp) 의 데이터와 iteration 2 (5% ramp) 의 데이터가 섞임
- 평균 효과만 보면 “1% 효과” 와 “5% 효과” 의 가중 평균
- iteration 별 효과 차이가 있을 때 평균이 잘못된 결정을 유도
“timestamp 로 iteration 을 추론하면 되지 않나?” 는 자연스러운 질문이다. 답: 분산 시스템의 시간 동기화는 milliseconds 단위로 어긋난다. ramping 의 cutover 가 정확히 12:00:00 이라도 일부 서버는 12:00:00.150 에 전환되고 일부는 11:59:59.870 에 이미 새 코드 로드.
게다가 ramping 은 한 번에 끝나지 않는다 — 새 코드가 배포되기까지 수 분 ~ 수 시간이 걸린다. 이 transition window 동안 같은 timestamp 에 두 iteration 데이터가 섞인다.
Iteration ID 를 명시적으로 로깅하면 transition window 의 데이터를 정확히 분리할 수 있다. 이는 단순 편의가 아니라 데이터 정확성의 근본이다. 분산 시스템의 시간 부정확성을 응용 layer 의 ID 로 보완하는 일반 패턴이다.
5.3 Counterfactual Logging — Run/Fly 단계 필수
Treatment 사용자가 “만약 Control 이었다면 무엇을 봤을까” 를 함께 로깅하는 패턴이다. Architecture 3 의 triggering 분석 (Ch.20) 에 핵심.
Treatment 사용자 요청
↓
Treatment 응답 생성 (red button)
↓ (counterfactual logging on)
Control 응답도 동시 계산 (blue button) — 사용자에게는 안 보임
↓
두 응답의 차이를 로깅
비용: Treatment 응답 생성에 추가로 Control 계산 = 두 배 연산.
Counterfactual logging 의 가정은 Treatment 와 Control 의 응답이 서로 부작용 (side effect) 없이 계산 가능 하다는 것이다. 깨지는 사례:
- DB write 가 포함된 Treatment — Control 도 DB write 를 하면 부작용이 누적, 안 하면 Control 추정에 missing data
- 외부 API 호출이 포함된 Treatment — 두 번 호출하면 비용이 두 배, 한 번만 하면 어느 variant 의 효과인지 불명
- 사용자에게 직접 노출되는 부작용 (이메일·푸시) — Control 도 보내면 사용자 피해, 안 보내면 효과 측정 불가
해결: read-only 변경에 한정해 counterfactual 적용. write·side-effect 변경은 별도 분석 (예: Architecture 1 + 분기 변경) 으로 처리.
이 한계 때문에 counterfactual logging 은 ranking·recommendation 같은 read-only domain 에서 가장 효과적이다.
6 왜 필요한가
6.1 4 컴포넌트 부재의 결과
플랫폼 인프라가 분산되거나 ad-hoc 으로 구현되면 다음 함정에 빠진다.
- Definition 부재 — 실험 description 이 없어 6 개월 후 왜 했는지 모름. Institutional memory (Ch.8) 부재. 같은 가설 반복 검증.
- Deployment 결함 — atomicity 위반으로 분산 서버 일관성 깨짐. 실험 결과가 측정 불가능한 noise 로 오염.
- Instrumentation 누락 — variant·iteration 로깅 누락으로 ramping 데이터 섞임. 잘못된 평균이 결정 근거가 됨.
- Trust chain 단절 — 한 컴포넌트만 잘 작동해도 전체 신뢰성은 가장 약한 컴포넌트 수준.
6.2 표준 4 컴포넌트의 가치
표준화된 4 컴포넌트 인프라가 갖춰지면.
- 실험자는 셀프서비스로 실험 시작·중단 가능 (데이터 사이언스 팀 병목 제거)
- 새 실험 1 건의 한계 비용이 거의 0 — Run·Fly 단계 진입 조건
- 메타분석 가능 (Ch.8) — 같은 schema 데이터가 누적되어 횡단 분석 가능
이 가치는 단계 전환의 핵심 enabler 다. Walk → Run 진입은 인프라 표준화 없이 불가능하다.
7 응용
7.1 Bing 의 Architecture 3 도입 사례
Bing 은 초기에 Architecture 1 (Code Fork) 로 시작했다가, 검색 ranking 파라미터가 수백 개로 늘어난 시점에 Architecture 3 (Early + Config) 으로 전환했다. 도입 비용은 1 년 엔지니어링 프로젝트 (사전지식 보강), 효과는.
- 동시 실험 수 5 배 증가 (코드 분기 제약 제거)
- 실험 시작 시간 분 단위로 단축 (코드 deploy 불필요, config push 만)
- Triggering 분석은 counterfactual logging 으로 보완
7.2 Microsoft Office 의 부채 자동 알림
Office 는 Architecture 2 의 변형으로, 각 실험 파라미터에 bug ID 부착 후 3 개월 뒤 자동 알림으로 cleanup 강제. 결과: 코드 부채 누적이 다른 회사 대비 현저히 낮음.
이 패턴은 부채의 자연 감쇠 (decay) 를 시스템으로 강제한다. 사람의 의지에 의존하지 않고도 정리 작업이 자동 schedule 된다.
8 예시 — 실험 사양 + Variant Assignment + Instrumentation 최소 구현
다음 코드는 4 컴포넌트의 첫 3 개를 단일 파일에 압축한 minimum viable example 이다. 실제 플랫폼은 각 컴포넌트가 분산 서비스로 구현되지만, 핵심 데이터 흐름은 동일하다.
import hashlib
import json
import time
from dataclasses import dataclass, field, asdict
from typing import Dict, List
# ============================================================
# Component 1: Definition / Set-up / Management
# ============================================================
@dataclass
class ExperimentSpec:
exp_id: str
name: str
description: str
owner: str
start: float # epoch seconds
end: float
variants: List[str] # ["control", "treatment"]
iteration: int = 1
targeting: Dict = field(default_factory=dict)
randomization_unit: str = "user_id"
spec = ExperimentSpec(
exp_id="exp_button_color_001",
name="Button Color Test",
description="Red CTA vs Blue CTA on checkout page",
owner="alice",
start=time.time(),
end=time.time() + 14 * 86400,
variants=["control", "treatment"],
iteration=1,
targeting={"country": "US", "device": "web"},
)
# ============================================================
# Component 2: Deployment — Variant Assignment Service
# ============================================================
def assign_variant(user_id: str, spec: ExperimentSpec, layer_id: str = "default") -> str:
"""Architecture 2 (Parameterized) 의 variant 결정"""
key = f"{spec.exp_id}:{spec.iteration}:{layer_id}:{user_id}".encode()
h = int.from_bytes(hashlib.md5(key).digest()[:8], "big")
bucket = h % 1000
# 간단한 50/50 split
return spec.variants[1] if bucket < 500 else spec.variants[0]
# ============================================================
# Component 3: Instrumentation
# ============================================================
event_log = []
def log_event(user_id: str, action: str, spec: ExperimentSpec, variant: str, **extra):
"""variant·iteration 정보를 모든 이벤트에 attach"""
event_log.append({
"timestamp": time.time(),
"user_id": user_id,
"action": action,
"exp_id": spec.exp_id,
"iteration_id": spec.iteration,
"variant_id": variant,
**extra,
})
# ============================================================
# 시뮬레이션 — 사용자 1000 명 요청 처리
# ============================================================
import numpy as np
rng = np.random.default_rng(42)
for i in range(1000):
uid = f"user_{rng.integers(0, 10**8)}"
variant = assign_variant(uid, spec)
# 변종별 page view → click 시뮬
log_event(uid, "page_view", spec, variant)
click_prob = 0.05 if variant == "control" else 0.07
if rng.random() < click_prob:
log_event(uid, "click_cta", spec, variant)
# 결과 요약 (Component 4 — Analytics 의 입력 형태)
import pandas as pd
df = pd.DataFrame(event_log)
summary = df.pivot_table(
index="variant_id",
columns="action",
values="user_id",
aggfunc="count",
fill_value=0,
)
summary["ctr"] = summary["click_cta"] / summary["page_view"]
print(summary)예상 출력 (시드 42 기준).
action click_cta page_view ctr
variant_id
control 24 494 0.048583
treatment 34 506 0.067193
- 사양 (Definition) 이 데이터에 흐름 — exp_id·iteration_id 가 모든 event 에 첨부되어 분석 시 어느 실험·어느 단계의 데이터인지 자동 식별
- 결정성 보장 (Deployment) —
assign_variant()가 같은 user·iteration 에서 같은 결과 → 사용자 경험 일관성 - Iteration 분리 (Instrumentation) —
iteration_id를 명시 → ramping 시 데이터 섞임 방지 - Trust chain — 어느 한 컴포넌트만 결함이어도 최종 분석은 의미 없음
위 코드는 30 줄 미만이지만 실험 인프라의 본질 을 모두 담고 있다. 실제 플랫폼은 이 30 줄을 분산 시스템·고가용성·실시간 모니터링으로 확장한 것이다.
9 관련 주제
선행 — Ch.4 시리즈
후속 — Ch.4 시리즈
관련 챕터
- F12-* — Client-Side Experiments (Ch.12) — Deployment 의 클라이언트·서버 차이
- F13-* — Instrumentation 심화 (Ch.13)
- F14-* — Randomization Unit (Ch.14) — Variant Assignment 의 무작위 단위 결정
- F20-* — Triggering (Ch.20) — Counterfactual logging 의 활용
다른 카테고리 연결
- Engineering — DevOps Series — CI/CD 파이프라인과 Deployment 통합
- Engineering — System Design — 분산 시스템의 atomicity·tracing
- Causal Inference — DAG — Variant Assignment 가 인과 식별 가정을 어떻게 강제하는지