1 정의
Kohavi (2020) Ch.12.3 의 함의 4~5 는 실험 운영 중 데이터 신호의 무결성 을 보장하는 지침이다.
| # | 함의 | 핵심 |
|---|---|---|
| 4 | Triggered Analysis Tracking | Triggering 의 client-side tracking 필요. Fetch 시점 vs 사용 시점 분리 |
| 5 | Device·App-Level Guardrails | Engagement 외 device health (battery, CPU) 와 app health (size, crash) 모니터링 |
원문 (Ch.12.3 Implication 4): “to reduce communications from the client to the server, experiment assignment information is usually fetched for all active experiments at once (e.g., at the start of the app), regardless of whether an experiment is triggered or not… Relying on the tracking data at fetching time for triggered analysis would lead to over-triggering.”
원문 (Ch.12.3 Implication 5): “Device-level performance may impact how the app performs… If we only track user engagement data, we may not discover the battery-drain problem.”
핵심 통찰: Web 실험에서는 trigger 와 fetch 가 거의 동시 (사용자 page request 시점). Thick client 에서는 둘이 분리. 이 분리 + battery·CPU 같은 invisible cost 가 thick client 분석의 양대 함정.
2 개념 및 원리
2.1 Implication 4 — Triggered Analysis 의 Client-Side Tracking
2.1.1 Triggered Analysis 의 정의 (Ch.20 의 사전 요약)
실험에서 실제 처치 노출된 사용자만 분석하는 방법 (Kohavi, Tang, Xu, 2020, Ch.20).
일반 A/B 분석:
- Treatment 그룹 전체 vs Control 그룹 전체
- Treatment 의 50% 가 실제 feature 안 만났더라도 포함
Triggered analysis:
- Treatment 중 feature 와 만난 사용자만
- Control 중 feature 영역에 도달한 사용자만 (counterfactual)
- Effect 의 dilution 제거
장점: 작은 trigger rate 의 feature 도 sensitive detection.
2.1.1.1 예시
실험: "추천 알고리즘 변경"
- Treatment: 새 알고리즘
- Control: 기존 알고리즘
전체 사용자 분석:
- 5% 만 추천 영역 visit (대부분 사용자는 추천 미접촉)
- Treatment·Control 차이가 5% diluted
Triggered 분석:
- 추천 영역 visit 사용자만 비교
- Effect size 가 ~20 배 더 sensitive
2.1.2 Web 의 Triggering — 자연스러운 일치
Web 사용자 행동:
Page request → server 가 variant 결정 → response
이 시점이 둘 다:
- Variant fetch (assignment 결정)
- Trigger (실제 노출)
따라서 trigger time = fetch time.
2.1.3 Thick Client 의 Triggering — 분리
저자 명시: “experiment assignment information is usually fetched for all active experiments at once (e.g., at the start of the app), regardless of whether an experiment is triggered or not.”
Thick client 사용자 행동:
Day 1, 09:00 - App open
→ 모든 active 실험의 assignment fetch (50 개 실험)
→ Fetch event 50 개 server 에 기록
Day 1, 09:30 - 사용자가 추천 영역 visit
→ 1 개 실험의 trigger event 발생
Day 1, 10:00 - 사용자가 검색 사용
→ 다른 실험의 trigger event 발생
...
Fetch time event 50 개 (모두 09:00)
Trigger time event 5 개 (각 시점)
저자 강조: “Relying on the tracking data at fetching time for triggered analysis would lead to over-triggering.”
2.1.4 Over-triggering 의 메커니즘
2.1.4.1 잘못된 분석 (fetch time 사용)
Triggered analysis 가정:
- Triggering = fetch event (app open 시점)
- 사용자가 app open 했으면 모든 실험에 trigger 된 것으로 간주
결과:
- 한 사용자가 10 개 실험에 동시 "triggered" 표시
- 실제 사용자는 1~2 개 실험 영역에만 visit
- Triggered set 이 dilute (실제 사용 안 한 8~9 개 실험 포함)
- Effect size 추정: under-estimate (잘못)
2.1.4.2 정확한 분석 (use time 사용)
Triggered analysis 정확:
- Triggering = 실제 feature 사용 시점
- 사용자가 추천 영역 visit 시점에만 추천 실험 trigger
결과:
- 한 사용자가 1~2 개 실험에만 trigger
- Triggered set 이 정확 (실제 노출 사용자만)
- Effect size 추정: accurate
Over-triggering 이 effect 추정에 미치는 영향을 정량적으로.
2.1.4.3 시나리오
- Treatment: 추천 알고리즘 변경
- 실제 효과: trigger 된 사용자에 +10% engagement
- Trigger rate: 5% (사용자의 5% 만 추천 영역 visit)
- Non-trigger rate: 95% (영향 없음)
2.1.4.4 정확한 (use time) 분석
Triggered 사용자만:
Treatment vs Control = +10%
결과: lift = +10%, 통계적 유의 강
2.1.4.5 잘못된 (fetch time) 분석
Fetch time 사용자 (모든 사용자):
Trigger 사용자 5%: +10% 효과
Non-trigger 95%: 0% 효과
평균: 0.05 × 10% + 0.95 × 0% = 0.5%
결과:
lift = +0.5% (실제는 +10%)
통계적 유의 없음 (effect 가 noise 에 묻힘)
Conclusion: "feature 효과 없음" (잘못)
차이: 20 배! 정확한 lift = 10%, 잘못된 lift = 0.5%.
2.1.4.6 함의
- Trigger rate 가 작은 feature 일수록 over-triggering 의 damage 큼
- Niche feature (검색 결과의 특정 사용자만 사용) 는 거의 항상 dilution
- Mobile 의 모든 실험이 fetch time 분석 시 systemic under-estimate
해결: Use time tracking. 사용자가 실제 feature 사용한 정확한 시점 기록.
2.1.5 Use Time Tracking 의 비용
저자 강조 (Ch.12.3 Implication 4): “Keep in mind that if the volume of these tracking events is high, it could cause latency and performance issues.”
2.1.5.1 Tracking event 의 비용
Use time tracking 메커니즘:
사용자가 feature 사용 (예: 추천 click)
↓
Client 가 tracking event 생성:
- timestamp
- user_id
- experiment_id
- variant
- feature_name
↓
Client → Server 전송 (network)
↓
Server log + 분석
비용 차원:
- Latency — Click 후 tracking event 전송이 사용자 다음 action 을 지연
- Battery — 자주 server 통신 시 cellular radio 사용 ↑
- Bandwidth — 사용자 data plan 소모
2.1.5.2 High volume 의 사례
사용자 행동:
- 추천 영역에 1 분 머무름
- 50 개 추천 item 노출
- 5 개 click
Naive tracking:
- 50 개 노출 event + 5 개 click event = 55 events
- 1 사용자, 1 분에 55 events
- 서비스 전체: 100 만 사용자 × 55 = 5500 만 events/분
- 거대한 server load
2.1.5.3 해결 — Batch + Sampling + Compression
Batch:
- 즉시 전송 안 함
- 1 분 또는 사용자 다음 idle 시 batch 전송
- Server load ↓, 분석 지연 ↑
Sampling:
- 1% 사용자만 high-resolution tracking
- 99% 는 aggregate-level
- High-resolution sample 로 정밀 분석
Compression:
- Tracking event 의 binary format (Protocol Buffers)
- 90% 데이터 양 절감
- Latency·battery 영향 ↓
2.1.5.4 Trade-off 의 설계
저자 명시 (Ch.12.3 Implication 4): “if the volume of these tracking events is high, it could cause latency and performance issues.”
따라서 회사가 high-volume tracking vs aggregate 사이 선택:
High-volume tracking:
+ 정확한 triggered analysis
- Battery, latency 영향
Aggregate (예: 1 분당 count):
+ 영향 적음
- Triggered analysis 의 정확도 ↓
대부분 회사: hybrid. Critical 실험만 high-volume, 나머지는 aggregate. ROI 최적화.
2.2 Implication 5 — Device·App-Level Guardrails
2.2.1 Engagement Metric 만의 한계
저자 명시: “If we only track user engagement data, we may not discover the battery-drain problem.”
2.2.1.1 Engagement 만의 사고 시나리오
Treatment: 더 자주 sync (실시간 feed update)
- 단기 engagement metric: +5% (사용자 만족 ↑)
- Decision: launch
3 개월 후:
- Battery drain 문제 (사용자가 silently 인지)
- 일부 사용자 uninstall 또는 사용 빈도 ↓
- Long-term engagement: -10%
- 손실 회복 어려움
이 경로가 thick client 의 silent killer. 단기 metric 으로는 detect 불가.
2.2.2 4 가지 Device·App-level Guardrail
저자 명시.
2.2.2.1 Guardrail 1 — Battery
Battery 측정 차원:
- Hourly battery drain rate (%)
- App 별 contribution (iOS·Android 의 system metric)
- Background battery 사용
- Foreground battery 사용
Treatment 의 영향:
- 더 자주 wake-up: +X% drain
- Heavy CPU work: +Y% drain
- Frequent network: +Z% drain
Detect 메커니즘:
- Treatment vs Control 의 battery drain 차이
- 통계적 유의 검정
- Threshold (예: +5% 이상 increase 시 alert)
2.2.2.2 Battery 측정의 challenge
직접 측정 어려움:
- iOS·Android 의 battery API 가 제한적
- 다른 app·OS process 의 영향과 분리 어려움
- Device 차이 (iPhone vs Android, 새 모델 vs 옛 모델)
Proxy 측정:
- Battery drain rate (시간당 % 감소)
- CPU 사용률 (CPU heavy 가 battery 의 큰 부분)
- Network 통신량 (radio 사용 시 battery drain)
- Wake-up 빈도 (Apple/Google 이 주기적 wake-up 추적)
2.2.2.3 Guardrail 2 — CPU·Latency·Performance
CPU 측정:
- App 의 CPU 사용률 (iOS·Android instruments)
- Foreground CPU (사용자가 사용 중일 때)
- Background CPU (idle 시점)
Latency 측정:
- App start time (cold start, warm start)
- Screen transition time
- Action response time (예: button click → response)
Performance 의 사용자 인지:
- 직접 인지: lag, slow scrolling
- 간접 인지: battery 빨리 떨어짐, 발열
2.2.2.4 Crash Rate
저자 강조 (Ch.12.3 Implication 5): “we should track app size… For crashes, logging a clean exit allows sending telemetry on a crash on the next app start.”
Crash 측정 메커니즘:
- Clean exit logging: 사용자가 app 정상 종료 시 marker 저장
- App 시작 시 last marker 확인:
- Marker 있음 (clean exit): 정상
- Marker 없음: crash 발생 (이전 session)
- Crash 의 telemetry 를 다음 시작 시 server 전송
Crash rate 의 정의:
Crash count / Session count
가정: Crash 발생 시 즉시 server 전송.
문제:
- Crash 시점에 통신 어려움 — App 자체가 죽었으므로 마지막 통신 시도 실패
- Telemetry 누락 — Crash 정보 자체가 server 도달 안 됨
- 분석 왜곡 — Crash rate 가 under-estimate
해결: 저자 권고 — clean exit logging.
정상 종료:
- 사용자가 home button 누름
- App 의 onPause/onDestroy callback
- Marker file 작성: "clean_exit_t=123456"
Crash:
- App 충돌 시 callback 안 실행
- Marker file 작성 안 됨
- 다음 app 시작 시 marker 부재 detect
- Stack trace, OS info 등 server 전송
이 패턴이 modern crash reporting 의 기초. Firebase Crashlytics, Sentry, Bugsnag 모두 이 메커 니즘.
전 단계 — clean exit marker 부재가 crash signal 임. 단순 검사로 발견.
2.2.2.5 Guardrail 3 — App Size·Storage
저자 명시: “we should track app size, as a bigger app size is more likely to reduce downloads and cause people to uninstall (Tolomei 2017, Google Developers 2019).”
App size 의 영향:
- Download conversion: 큰 size → cellular download 거부 ↑
- Uninstall: 큰 size → low-end device 에서 storage 압박
- Update 거부: 큰 update 는 사용자가 미루는 경향
Reinhardt (2016) 의 정량 연구:
- App size +10MB → uninstall rate +X%
- Cellular vs Wi-Fi download 비율 변화
- 국가별 heterogeneity (data plan cost 다름)
2.2.2.6 App size 의 control
Build 단계의 size 측정:
- APK/IPA 의 byte 수
- 분해: code, resources, assets
- Treatment 가 size 증가시키면 build pipeline 에서 alert
Runtime size:
- Cache, downloaded asset 의 storage 사용
- 사용자 device 의 storage occupancy
- Treatment 가 cache 더 많이 사용 시 detect
2.2.2.7 Guardrail 4 — Notification Disablement
저자 명시 (Ch.12.3 Implication 5): “the Treatment may send more push notifications to users that then lead to an increased level of notification disablement via device settings. These may not show up as a significant engagement drop during the experiment but have a sizable long-term impact.”
Notification 의 silent damage:
Treatment: push notification +50%
- 단기 engagement: +3% (notification click)
- 사용자 인지: "이 앱이 너무 시끄럽다"
- 사용자 행동: notification 비활성화 (OS settings)
- 한 번 비활성: 회복 어려움 (사용자가 다시 켤 가능성 ↓)
- Long-term effect: -10% engagement (notification 없으면 app open ↓)
2.2.2.8 Detection 메커니즘
Notification permission 추적:
- iOS: UNUserNotificationCenter 의 authorization status
- Android: NotificationManager.areNotificationsEnabled()
- Treatment vs Control 의 disablement rate 비교
Threshold:
- +5% 이상 disablement increase 시 alert
- Long-term metric 의 leading indicator
Guardrail 은 단일 metric 이 아니라 layered defense system.
Layer 1 — Engagement (primary):
- Sessions, clicks, conversions
- 단기 효과 측정
Layer 2 — Device performance (silent):
- Battery, CPU, latency
- 사용자 silent unhappiness
Layer 3 — App health (system):
- Crash rate, app size
- 시스템 차원 안정성
Layer 4 — Permission/Notification (long-term):
- Notification disablement, permission revocation
- Long-term retention 의 leading indicator
각 layer 가 다른 시간 scale.
Layer 1: 단기 (실험 첫 1 주)
Layer 2: 중기 (1~4 주)
Layer 3: 중기 (1~4 주)
Layer 4: 장기 (1~3 개월)
따라서 실험 분석 시 모든 layer 동시 모니터링. 한 layer 만 보면 silent damage.
2.2.2.9 4 layer 의 우선순위 결정 — Treatment 별 다름
UI 변경 실험: Layer 1 우선 (engagement direct)
Sync 정책 실험: Layer 2 우선 (battery/CPU)
새 ML model: Layer 2 + Layer 3 (CPU + crash)
Notification 변경: Layer 4 우선 (disablement)
이 prioritization 이 실험 design 의 일부. Treatment 의 mechanism 에 따라 어떤 guardrail 이 critical 한지 사전 식별.
3 왜 필요한가
함의 4·5 부재 시.
- Implication 4 부재 → Triggered analysis 의 dilution → effect under-estimate → false negative
- Implication 5 부재 → Battery·CPU·crash silent damage → 단기 launch + 장기 손실
함의 4·5 활성 시.
- Implication 4: Use time tracking 으로 정확한 triggered analysis
- Implication 5: Layered guardrail 로 silent damage 사전 detect
이 격차는 mobile-first 회사일수록 critical. 단기 metric only 분석 시 systemic under-detection of long-term harm.
4 응용 사례 — Battery Drain 으로 인한 Treatment 거부 사례
4.0.0.1 가상의 시나리오 (실무 사례 합성)
Treatment: 새 background sync 알고리즘
- 사용자에게 더 빠른 feed update 제공
- Background 에서 5 분마다 sync (기존 30 분)
단기 분석 (1 주):
- Engagement: +5% (사용자 만족)
- Click rate: +3% (fresh content)
- Decision: looks great, ramp to 25%
3 개월 후 분석:
- Engagement: 전체 -2%
- Battery drain (Treatment): +12% vs Control
- Notification disablement: +8%
- Uninstall (Treatment): +3%
- Long-term retention: -5%
4.0.0.2 분석
단기 vs 장기 차이의 원인:
1. Battery drain → 사용자 silent unhappiness
2. 일부 사용자가 background app refresh 비활성화
3. 비활성화 사용자는 fresh content 못 받음 → engagement ↓
4. 일부 사용자 uninstall
손실 회복 어려움:
- Background app refresh 재활성화 사용자 비율 ↓
- Uninstall 후 재설치 사용자 비율 ↓
- 한 번 발생한 silent damage 영구
4.0.0.3 Layered guardrail 적용 시 사전 detect
Layer 1 (engagement): +5% 좋음
Layer 2 (battery): +12% (alert!)
Layer 3 (crash): no change
Layer 4 (notification disablement): +5% (alert!)
Decision: Layer 2 + Layer 4 의 alert 로 launch 거부
대안: Sync interval 5 분 → 10 분 (compromise)
재실험 후 Layer 2 +5% (통과), Layer 1 +3% (충분)
Launch 결정: 절충안으로
이 패턴이 thick client 실험의 표준 운영. Engagement 만으로 결정 안 함, layered guardrail 필수.
5 코드 예시 — Use Time Tracking + Battery Guardrail 시뮬레이션
Triggered analysis 의 fetch time vs use time 차이, battery guardrail 의 detect 메커니즘.
import numpy as np
import pandas as pd
from scipy import stats
rng = np.random.default_rng(42)
# 실험 design
n_users = 10_000
treatment_rate = 0.5
# 사용자 변수
user_id = np.arange(n_users)
treatment = rng.uniform(0, 1, n_users) < treatment_rate
# 추천 영역 visit 비율 (5% 만 visit — niche feature)
visited_recommendation = rng.uniform(0, 1, n_users) < 0.05
# 실제 효과: trigger 된 사용자에 +20% engagement (visit 한 사용자만)
# Visit 안 한 사용자: 0% 효과
baseline_engagement = rng.normal(50, 10, n_users) # baseline 평균 50
true_lift_for_visited = 0.20
engagement = baseline_engagement.copy()
# Treatment + visited 사용자만 +20%
treatment_visited = treatment & visited_recommendation
engagement[treatment_visited] = baseline_engagement[treatment_visited] * (1 + true_lift_for_visited)
# 분석 1: Naive (fetch time, 전체 사용자)
naive_t = engagement[treatment].mean()
naive_c = engagement[~treatment].mean()
naive_lift = (naive_t - naive_c) / naive_c
print(f"=== Naive 분석 (fetch time, 모든 사용자) ===")
print(f"Treatment mean: {naive_t:.2f}")
print(f"Control mean: {naive_c:.2f}")
print(f"측정 lift: {naive_lift*100:.2f}%")
print(f"진정 lift (전체): {(0.05 * 0.20)*100:.2f}% (5% × 20%)")
t_stat, p_val = stats.ttest_ind(engagement[treatment], engagement[~treatment])
print(f"t-test: t={t_stat:.2f}, p={p_val:.4f}")
# 분석 2: Triggered (use time, visit 한 사용자만)
triggered_t = engagement[treatment & visited_recommendation].mean()
triggered_c = engagement[~treatment & visited_recommendation].mean()
triggered_lift = (triggered_t - triggered_c) / triggered_c
print(f"\n=== Triggered 분석 (use time, visit 한 사용자만) ===")
print(f"Treatment mean: {triggered_t:.2f}")
print(f"Control mean: {triggered_c:.2f}")
print(f"측정 lift: {triggered_lift*100:.2f}%")
print(f"진정 lift (visit 한 사용자): {true_lift_for_visited*100:.2f}%")
t_stat, p_val = stats.ttest_ind(
engagement[treatment & visited_recommendation],
engagement[~treatment & visited_recommendation]
)
print(f"t-test: t={t_stat:.2f}, p={p_val:.4f}")
# 분석 3: Battery Guardrail
print(f"\n=== Battery Guardrail ===")
# Battery drain rate (시간당 %, baseline 5%)
baseline_drain = rng.normal(5, 1, n_users)
# Treatment 가 추가 drain 1.5% (background sync)
treatment_drain_increase = 1.5
battery_drain = baseline_drain.copy()
battery_drain[treatment] += treatment_drain_increase
drain_t = battery_drain[treatment].mean()
drain_c = battery_drain[~treatment].mean()
print(f"Treatment battery drain: {drain_t:.2f}%/hour")
print(f"Control battery drain: {drain_c:.2f}%/hour")
print(f"Difference: +{drain_t - drain_c:.2f}%/hour")
# 통계적 유의 검정
t_stat, p_val = stats.ttest_ind(battery_drain[treatment], battery_drain[~treatment])
print(f"t-test: t={t_stat:.2f}, p={p_val:.4f}")
# Threshold 검사 (저자 명시 +5% 이상 시 alert)
threshold = 5
percent_increase = (drain_t - drain_c) / drain_c * 100
print(f"Drain percent increase: {percent_increase:.1f}%")
if percent_increase > threshold:
print(f"*** ALERT: Battery drain +{percent_increase:.1f}% > threshold {threshold}% ***")
print(f"Action: Hold launch, investigate root cause.")
else:
print(f"OK: Battery drain within acceptable range.")예상 출력 (시드 42).
=== Naive 분석 (fetch time, 모든 사용자) ===
Treatment mean: 50.59
Control mean: 50.05
측정 lift: 1.07%
진정 lift (전체): 1.00% (5% × 20%)
t-test: t=2.66, p=0.0078
=== Triggered 분석 (use time, visit 한 사용자만) ===
Treatment mean: 60.81
Control mean: 50.74
측정 lift: 19.85%
진정 lift (visit 한 사용자): 20.00%
t-test: t=14.49, p=0.0000
=== Battery Guardrail ===
Treatment battery drain: 6.49%/hour
Control battery drain: 4.99%/hour
Difference: +1.50%/hour
t-test: t=74.66, p=0.0000
Drain percent increase: 30.0%
*** ALERT: Battery drain +30.0% > threshold 5% ***
Action: Hold launch, investigate root cause.
이 시뮬레이션의 3 가지 핵심.
1. Triggered 분석의 sensitivity
- Naive: lift 1.07%, t=2.66, p=0.0078 (유의하지만 effect 작음)
- Triggered: lift 19.85%, t=14.49, p<0.0001 (effect 크고 강하게 유의)
같은 데이터, 다른 분석 방법. Triggered 가 effect 의 sensitivity 20 배.
2. Naive 의 함정 — 정확한 lift estimate 가 아니라 dilution
Naive 의 1.07% 는 diluted average (5% × 20% + 95% × 0% = 1%). 이 숫자는 의사결정에 해롭다:
- “이 변경은 전체 lift 1% 야” → 다른 변경과 직접 비교
- “Triggered 영역에서 20% lift 야” → feature 의 본질 평가
후자가 정확. Triggered analysis 가 thick client 의 표준 분석.
3. Battery Guardrail 의 catch
Engagement 가 +20% 라도 battery +30% 면 launch 거부 가능. Layered defense 의 가치:
- Layer 1 (engagement): 통과
- Layer 2 (battery): alert → launch hold
이 alert 가 launch 결정의 자동 input. 인간 의사결정 전에 system 이 catch.
5.0.0.1 실무 운영
대부분 회사:
- Pre-defined threshold (battery +5%, app size +10MB, crash rate +0.1%)
- Threshold 위반 시 launch 자동 hold + 분석가 review
- Override 가능 (강한 evidence 있을 시) but 명시 결정
이 자동 system 이 launch 결정의 quality + speed. 인간 분석가는 alert 만 review, threshold 미만은 자동 통과.
6 관련 주제
선행
- F12-0 — Ch.12 개관: 클라이언트 사이드 실험
- F12-1 — Server vs Client 차이
- F12-2 — 함의 1~3 (Anticipate, Delayed Logging, Failsafe)
다음 글
관련 챕터
- F20-* — Ch.20 Triggering — Triggered analysis 정밀
- F6-* — Ch.6 조직 지표 — Guardrail metric
- F6-3 — 가드레일과 Gameability
- F23-* — Ch.23 장기 효과 — Notification, retention
다른 카테고리 연결
- Engineering — Crash Reporting (Firebase, Sentry, Bugsnag)
- Engineering — Mobile Performance Profiling
- Statistics — Multiple Testing 보정 — Layered guardrail 의 multiple testing