함의 1~3 — Parameterize · Delayed Logging · Failsafe

Windows 10 검색 박스 사례 · Effective Starting Time · OEM First-Run 메커니즘

Kohavi (2020) Ch.12.3 의 첫 3 함의를 깊게 다룬다. 모든 variant 사전 ship + feature flag, Windows 10 search box text 의 millions-of-dollars 사례, delayed logging 과 effective starting time 의 selection bias, offline·startup 시 default variant 와 stable randomization ID 의 failsafe 설계를 코드와 사례로 풀이한다.

Experimentation
A/B Test
저자

Kwangmin Kim

공개

2026년 05월 08일

1 정의

정의: Implications 1~3 — 사전 준비와 robustness

Kohavi (2020) Ch.12.3 의 첫 3 함의는 실험 시작 전 준비 단계의 지침이다.

# 함의 핵심
1 Anticipate Changes Early and Parameterize 모든 variant 를 미리 ship + server-side flag 로 통제
2 Expect Delayed Logging and Effective Starting Time 실험 활성화 시점이 사용자별 다름 + selection bias
3 Create Failsafe for Offline/Startup Offline·startup 시 default variant + stable random ID

원문 (Ch.12.3 Implication 1): “all experiments, including all variants for each of these experiments, need to be coded and shipped with the current app build. Any new variants, including bug fixes on any existing variant, must wait for the next release.”

핵심 통찰: Web 실험은 “변경 → 즉시 실험” 1 step. Thick client 실험은 “전체 variant 사전 설계 → ship → 활성화 → 활성화 시점 분석 → failsafe” 의 5 step. 사전 설계의 비용 이 thick client 실험의 정체적 ROI 결정자.

2 개념 및 원리

2.1 Implication 1 — Anticipate Changes Early and Parameterize

2.1.1 Mindset Shift — 모든 variant 의 사전 ship

저자 명시: “all experiments, including all variants for each of these experiments, need to be coded and shipped with the current app build.”

2.1.1.1 Web 실험 vs Thick Client 실험의 mindset
Web (thin):
  Day 1: 새 variant idea
  Day 2: server 에 코드 배포
  Day 3: 실험 시작
  → idea-to-experiment cycle: 3 일

Thick Client (mobile):
  Day 1: 새 variant idea
  Day 2~30: client 코드에 variant ship + 다음 release
  Day 31: app store 심사 신청
  Day 35: 심사 통과
  Day 36~50: 사용자 update 도달
  Day 51: 실험 시작
  → idea-to-experiment cycle: 50 일+
2.1.1.2 해결책 — 사전 ship + feature flag
Anticipate 의 운영:
  - 다음 release 에 ship 할 모든 variant 를 미리 코드화
  - 각 variant 에 feature flag (default OFF)
  - Server-side 에서 flag on/off 결정
  - 실험 시작은 server-side flag toggle 만으로

이 결과: idea-to-experiment cycle 이 다음 release 의 50 일 + server toggle 의 1 일. Variant 가 다음 release 에 미리 ship 되어 있으면 server 결정만으로 실험 가능.

2.1.2 3 가지 실용 패턴

저자 명시 3 가지.

2.1.2.1 패턴 1 — Dark Features (Feature Flag)
정의: Dark Feature

Build 에 ship 되었으나 default OFF 된 feature. Server 의 결정으로 동적 활성화.

Build 에 ship:
  - Code: 새 feature 의 모든 코드
  - Default: OFF (사용자에 노출 안 됨)

Server 결정:
  - feature_flag_X = OFF → 모든 사용자에 미노출
  - feature_flag_X = (50% Treatment, 50% Control) → 실험
  - feature_flag_X = ON → 모두에 노출 (launch)
2.1.2.2 Dark feature 의 활용
Use case 1 — 미완성 feature:
  - 다음 release 까지 완성 어려움
  - 일부만 ship + dark feature 로 숨김
  - Server 가 ready 되면 활성화

Use case 2 — Server-dependent feature:
  - Client code 는 ship
  - Server-side service 가 ready 되면 활성화
  - 두 작업의 release timing 분리

Use case 3 — Safety net:
  - 모든 feature 에 flag
  - 문제 발견 시 즉시 OFF
  - 새 release 기다리지 않고 rollback
가정 — Dark Feature 없이 운영 시

가정: 모든 feature 가 build 에 ship 되면 즉시 사용자에 활성.

결과:

  1. Bug 발견 시 rollback 어려움 — 새 build + 심사 + 사용자 update 까지 weeks

    • 그동안 사용자는 buggy feature 노출
    • Crash, data loss, 사용자 이탈 risk
  2. Server-dependent feature 의 timing 충돌 — Client ship 후 server 미완성 시 사용자가 broken feature 만남

  3. A/B 실험 자체 불가 — 모든 사용자에 ship 되면 100% 노출. 50/50 비교 어려움

해결: Dark feature + feature flag 가 thick client 실험의 prerequisite. 거의 모든 modern app 가 이 패턴 사용 (Facebook 의 Gatekeeper, Microsoft 의 Office Feature Flags, Google 의 Phenotype).

2.1.2.3 패턴 2 — Server-side Configurable Features

저자 명시: “More features are built so they are configurable from the server side.”

2.1.2.4 메커니즘
Client code:
  feature_X.enabled = config.get("feature_X.enabled")  # server 에서 fetch
  feature_X.variant = config.get("feature_X.variant")  # A/B/C/...

Server:
  - 사용자 ID 별 config 결정
  - 실험 design 변경 시 server-side 만 수정
  - Client code 변경 불필요
2.1.2.5 A/B 실험과 safety net 양 효과
A/B 실험:
  Server: user_id % 2 == 0 ? "A" : "B"
  실험 결과 분석 후 결정

Safety net:
  Server: feature_X.enabled = true
  문제 발견 시: feature_X.enabled = false  ← 즉시
  사용자 영향: 다음 fetch 부터 즉시 OFF

저자 강조: “If a feature does not perform well, we can instantly revert by shutting down the feature (the variant in the controlled experiment) without having to go through a lengthy client release cycle.”

이 즉시성이 thick client 실험의 가장 강력한 운영 advantage. 모든 변경에 safety net 내장.

2.1.2.6 패턴 3 — Fine-grained Parameterization

저자의 핵심 사례 (Ch.12.3 Implication 1).

2.1.2.7 Windows 10 검색 박스 사례
Windows 10 출시 시 (~2015):
  task bar 의 검색 박스 text 가 hardcoded
  → 변경하려면 Windows update (수 개월 cycle)

Parameterize 적용:
  검색 박스 text 를 server-side parameter 로 추출
  → "Search Windows" 또는 "Type here to search" 등
  → server 가 결정

1 년 후 (~2016):
  실험 시작 (이미 parameter 가 있으므로 별도 release 없이)
  여러 text variant 비교
  Winner: user engagement ↑ + Bing revenue ↑ "millions of dollars"
직관 — Parameterize 의 힘

Windows 10 사례의 메시지: release 시점에 모든 가능한 실험 변수를 미리 parameterize 하면 미래 실험 가능.

원본 hardcoded 코드:

# 출시 시 (변경 어려움)
search_box.text = "Search Windows"

Parameterized 코드:

# 출시 시 (변경 가능 — server 에서 결정)
search_box.text = config.fetch("task_bar.search_box.text",
                                default="Search Windows")

차이:

  • 코드 line 수: 거의 동일
  • 출시 시점 비용: 0
  • 미래 실험 능력: hardcoded 0, parameterized infinite

Parameterize first 가 thick client 의 신조. “이 값을 미래에 변경할 가능성이 있나?” 질문에 “있을 수도” 면 parameterize. Default 만 잘 설정하면 사용자 영향 0.

2.1.2.8 ML model 의 server-side parameterization

저자 명시: “update machine learning model parameters from the server, so that a model can be tuned over time.”

Client (mobile):
  ml_model = load_local_model()  # build 와 함께 ship
  weights = config.fetch("ml_model.weights")  # server fetch
  ml_model.set_weights(weights)
  prediction = ml_model.predict(input)

Server:
  - 새 데이터로 재학습
  - 새 weight 를 client 에 push
  - Client 는 자동 적용

이 패턴이 modern mobile ML 의 표준. 모델 architecture 만 ship, weight 는 server-side update. 새 데이터에 적응 + A/B 실험 가능.

2.1.2.9 App Store Dark Feature Policy 주의

저자 강조: “there may be limitations imposed by app stores on which features can be shipped dark. We suggest carefully reading app store policies and appropriately disclosing dark features.”

Apple App Store policy (요약):
  - 심사 시 모든 feature 가 표시 가능해야
  - "Hidden feature" 는 reject 가능
  - Disclosure 필요 (release notes, privacy policy)

Google Play policy:
  - 일부 dark feature 허용 (A/B 실험 포함)
  - Privacy/policy 위반 dark feature 는 reject

함의: Dark feature 사용 시 app store policy 준수 필요. 특히 Apple 은 보수적 → 미리 disclose 권장.

2.2 Implication 2 — Delayed Logging and Effective Starting Time

2.2.1 실험 시작의 본질적 지연

저자 명시: “even then, the experiment is not fully active because:”

2.2.1.1 지연 원인 1 — Device 의 offline 또는 low bandwidth
실험 시작 t0:
  Server-side: flag X = (50% T, 50% C)

사용자 device 도달:
  - Online + Wi-Fi: 즉시 (분~시간)
  - Online + Cellular: 1~2 일 (Wi-Fi 대기)
  - Offline (occasional): 다음 online 까지 (~1 일)
  - Offline (frequent): 며칠
2.2.1.2 지연 원인 2 — App open 시에만 fetch

저자 명시: “If the new experiment configuration is fetched only when a user opens the app, the new assignment may not take effect until the next session as we do not want to change a user’s experience after they have started the current session.”

Heavy user (다 daily):
  - Day 1: morning open → config fetch → 즉시 적용
  - Light user (weekly):
  - Day 1~6: app 안 염
  - Day 7: open → config fetch → 처음 적용
  → 1 주 지연
2.2.1.3 지연 원인 3 — Old version 사용자

저자 명시: “There can be many devices with old versions without the new experiment code, particularly right after the new app release.”

Old version:
  - 새 실험 코드 자체가 ship 안 됨
  - Update 받기 전까지 control variant 와 동등 (또는 default)
  - Update 도달 시점부터 실험 참여

저자 추정: “the initial adoption phase takes about a week to reach a more stable adoption rate.”

2.2.2 Selection Bias 의 메커니즘

저자 명시: “signals at the beginning of the experiment would appear to be weaker (smaller sample size), and also have a strong selection bias towards frequent users and Wi-Fi users who tend to be early adopters.”

2.2.2.1 Selection 의 차원
실험 1 주차 sample 의 편향:
  - Frequent user 비중 ↑ (daily user 가 즉시 receive)
  - Wi-Fi user 비중 ↑ (Wi-Fi only telemetry)
  - 도시 거주 user 비중 ↑ (Wi-Fi access 좋음)
  - 선진국 user 비중 ↑ (mobile 인프라 좋음)
  - 기술 enthusiastic user 비중 ↑ (자동 update 활성화)

이 sample 이 모집단 대표 못 함.

2.2.2.2 Effective Starting Time

저자 명시: “Treatment and Control variants may have a different effective starting times.”

2.2.2.3 메커니즘
Server flag toggle (t0):
  flag = "C only" → "A/B 실험"

Treatment (variant A):
  - t0 + small delay 부터 활성화
  - Light user 는 t0 + 1 주에야 적용

Control (variant B):
  - 만약 shared control 이면 t0 이전부터 활성 (이전 실험의 control 그대로)
  - 새 control 이면 Treatment 와 같은 timing

Shared control 의 함정:
  - Control 사용자가 t0 이전부터 cache warm
  - Server response 가 빠름 (cache hit ↑)
  - 이 latency 차이가 metric 에 영향
  - 따라서 "Treatment 가 effective start time 비대칭" 으로 분석 왜곡
가정 — Delayed Logging 무시 시

가정: 실험 시작 직후부터 분석 시작. 모든 사용자가 즉시 활성화로 간주.

결과:

  1. 1 주차 false positive — Selection bias 로 frequent user 효과만 감지. 일반 user 효과와 다름.

    • 예: 새 feature 가 frequent user 에 +10%, 일반 +0% 효과
    • 1 주차 sample 의 frequent 비중 ↑ → 측정 +5%
    • 4 주차 (모집단) → 측정 +2%
    • 1 주차 결과로 launch 결정 → over-estimate
  2. Sample ratio mismatch (SRM) — Treatment·Control adoption 비대칭으로 N 비율 깨짐. 50/50 설계인데 1 주차에 48/52 → 통계 power 부족

  3. Cache warming bias — Shared control 의 cache hit ↑ → 빠른 response → engagement metric ↑. Control 자체 효과가 아닌 cache 효과.

해결:

  • 분석 시점 연기 — 1 주~2 주 후 stable adoption 상태에서만 분석
  • Sample weighting — 1 주차 sample 의 편향 차원 (frequent, Wi-Fi) 을 모집단 비중으로 reweight
  • Cache 분리 — Treatment·Control 모두 fresh cache 로 시작

이 보정이 모든 thick client 분석의 표준. Web 분석에서는 거의 없는 차원.

2.2.2.4 Cache Warming Bias 의 detail

저자 강조 (Ch.12.3 Implication 2): “if the Control runs earlier, the caches are warmed up so responses to service requests are faster, which may introduce additional bias.”

2.2.2.5 메커니즘
Cache hit/miss 의 latency:
  - Cache hit:  10ms (즉시 response)
  - Cache miss: 200ms (DB query)

Shared control (이전 실험에서 사용 중):
  - Cache 가 warm
  - 평균 latency: 10ms

Treatment (새로 시작):
  - Cache cold
  - 평균 latency: 200ms (처음)
  - 점차 warm 됨

비교 시점:
  실험 1~2 일차: Treatment latency ↑↑ (cold cache)
  실험 1 주차: Treatment latency 정상 (cache warm 됨)

함의: 1~2 일차 분석 시 Treatment 가 latency-sensitive metric 에서 inferior 처럼 보임 (실은 cache 효과).

2.2.2.6 해결
  1. Fresh cache 시작 — Treatment·Control 모두 cache 비우고 시작
  2. 첫 1 주 제외 — Cache warm 후 분석
  3. Cache hit rate 별 segment — Cache hit/miss 사용자 분리 분석

2.3 Implication 3 — Failsafe for Offline/Startup

2.3.1 사용자가 offline 일 때의 시나리오

저자 명시: “When users open an app, their device could be offline.”

2.3.1.1 Offline 의 빈도
사용자 분포의 offline 비율:
  - 항상 online: 50%
  - 주로 online + 가끔 offline: 30%
  - 자주 offline (지하·터널·약전계): 15%
  - 항상 offline (개도국 일부): 5%

App open 시 offline 확률: ~10% (전체 평균)

10% 가 적은 비율이 아님. 매 100 만 사용자 중 10 만 명이 offline 시점에 app open. 이 사용자에 대한 처리가 critical.

2.3.2 3 가지 Failsafe 메커니즘

저자 명시.

2.3.2.1 Failsafe 1 — Cache Experiment Assignment
Online 시:
  Server fetch → variant assignment → cache 저장

Offline 시 next open:
  Cache 에서 last assignment 사용 → 일관성 유지
2.3.2.2 일관성의 중요

저자 강조: “For consistency reasons, we should cache experiment assignment in case the next open occurs when the device is offline.”

Cache 없으면:
  1 회: variant A 할당
  Offline 후 next open: variant 결정 불가 → default
  사용자 경험: variant A → default → A → default ...
  → User confusion

Cache 있으면:
  1 회: variant A 할당 + cache
  Offline 후 next open: cache 의 A 사용
  사용자 경험: 일관 A
2.3.2.3 Failsafe 2 — Default Variant for Server Unresponsive

저자 명시: “if the server is not responding with the configuration needed to decide on assignment, we should have a default variant for an experiment.”

2.3.2.4 Server unresponsive 의 시나리오
원인:
  - Server 일시적 down (deploy, outage)
  - Network timeout (사용자 약전계)
  - Service 자체 fail

처리:
  - 5 초 timeout
  - Default variant 사용 (보통 control)
  - 다음 open 시 재시도
2.3.2.5 Default 선택의 dilemma
Default = Control:
  - Conservative
  - 효과 측정 시 sample size 부족 가능 (server unreachable user 가 control 에 더해짐)

Default = Treatment:
  - Aggressive (사용자 더 많이 노출)
  - 미검증 feature 에 노출 → risk

Default = "old behavior":
  - 새 실험 도입 전 동작 그대로
  - 가장 안전

대부분 회사: Default = Control 또는 old behavior. 특히 새 feature 는 default OFF.

2.3.2.6 Failsafe 3 — OEM First-Run Experience

저자 명시 (Ch.12.3 Implication 3): “Some apps are also distributed as original equipment manufacture (OEM) agreements. In these cases, experiments must be properly set up for a first-run experience.”

2.3.2.7 OEM 의 시나리오
OEM agreement:
  - App 이 device 에 미리 설치됨 (Samsung Galaxy 의 일부 앱)
  - 사용자가 device 산 직후 첫 실행
  - Internet 미연결 가능 (첫 setup 전)

First-run 의 도전:
  - Server 와 통신 못 함
  - User ID 도 없음 (sign-up 전)
  - 그러나 사용자에게 first impression 제공
2.3.2.8 First-run failsafe 설계
Pre-sign-up:
  - Stable randomization ID: device-level (UUID 또는 device ID)
  - Variant 결정: device ID 의 hash 로 deterministic
  - Cache: 다음 open 까지 유지

Post-sign-up:
  - User ID 로 transition
  - Pre-sign-up variant 와 user ID variant 의 일관성
  - User ID hash 로 결정 시 device hash 와 다를 수 있음
  - 해결: device ID → user ID 매핑 + 일관 변환
직관 — Stable Randomization ID 의 핵심

저자 강조 (Implication 3): “a stable randomization ID before and after users sign up or log in.”

이것이 thick client 실험의 가장 미묘한 함정.

2.3.2.9 시나리오 — Sign-up 전후의 ID 변화
Day 1 (sign-up 전):
  Device ID: abc123
  Hash(abc123) % 2 = 0 → variant A
  사용자 경험: variant A

Day 2 (sign-up 후):
  User ID: kim@example.com
  Hash(kim@example.com) % 2 = 1 → variant B
  사용자 경험: variant B (변경됨)

Day 3:
  같은 사용자가 Day 1 의 A 와 Day 2 의 B 모두 보았다
  → User confusion
  → Analytics 에서도 한 사용자의 variant assignment 가 일관 안 됨

해결:

Stable ID 설계:
  if not signed_in:
    rand_id = device_id
  else:
    rand_id = user_id

    # 그러나 sign-up 직후 device → user 변환 시 일관성 유지
    if has_device_id_history:
      rand_id = first_device_id  # 처음 device ID 유지

또는:

Hash-based 일관성:
  variant = hash(experiment_id + first_id) % num_variants

  - first_id: 사용자가 처음 만든 ID (device 또는 user)
  - 영구 보존 (device 변경 시도 user ID 가 first_id 가 됨)

이 일관성이 A/A 실험 (Ch.19) 의 sample ratio 검증에서 자주 깨짐. SRM detect 시 first_id 일관성 점검이 첫 step.

3 왜 필요한가

3 함의 부재 시.

  • Implication 1 부재 → 실험 cycle 50 일+, web 의 1/10 속도
  • Implication 2 부재 → 1 주차 false positive, over-estimate
  • Implication 3 부재 → Offline 시 user confusion, OEM first-run 무한 응답

3 함의 활성 시.

  • Implication 1: idea-to-experiment cycle 1 일 (server toggle 만)
  • Implication 2: stable adoption 후 trustworthy 분석
  • Implication 3: offline·startup·OEM 모두 graceful handle

이 격차는 thick client 실험의 첫 mindset shift. Web 사고에서 thick 으로 전환 시 이 3 함의 학습 이 가장 큰 ROI.

4 응용 사례 — Microsoft Office 의 monthly release with hundreds of features

저자 인용: “in a typical monthly release, Microsoft Office ships with hundreds of features that rollout in a controlled manner.”

4.0.0.1 운영 detail
Monthly release cycle (Microsoft Office):
  Day 1~28:
    - 100s of features 개발
    - 각 feature 에 feature flag (default OFF)
    - Build 에 모두 ship

  Day 28: Build release
  Day 30~50: 사용자 update 도달

  Day 51 이후 (실험 phase):
    - Server-side flag 으로 단계적 활성화
    - 1% → 5% → 25% → 100%
    - 각 단계 metric 모니터링
    - 문제 시 즉시 OFF

이 운영이 Implication 1·2·3 모두 통합. Build 는 monthly cycle, but 실험 자체는 daily 운영.

4.0.0.2 Failsafe 의 운영 사례
Office 의 한 feature 가 server-side flag toggle:
  10:00:  flag = 1% Treatment
  10:30:  Crash report 급증 (Treatment 사용자만)
  10:31:  flag = 0% (즉시 OFF)
  10:32:  Server 가 client 에 새 config push
  10:33~: 사용자 device 점차 OFF (Wi-Fi 시)

전체 incident 영향:
  - 30 분 동안 1% 사용자가 buggy feature
  - 그 외 사용자: 영향 없음
  - 빠른 detection + rollback

이 즉시성이 thick client 실험의 가장 큰 advantage. Build 가 monthly cycle 이라도 server flag 는 minutely cycle.

5 코드 예시 — Stable Randomization ID 구현

Sign-up 전후의 일관 variant assignment.

import hashlib
import json
import os
from typing import Optional

class ExperimentAssignment:
    """ Thick client 실험의 stable randomization ID 관리. """

    def __init__(self, cache_path: str):
        self.cache_path = cache_path
        self.cache = self._load_cache()

    def _load_cache(self) -> dict:
        if os.path.exists(self.cache_path):
            with open(self.cache_path) as f:
                return json.load(f)
        return {}

    def _save_cache(self):
        with open(self.cache_path, "w") as f:
            json.dump(self.cache, f)

    def get_variant(self, experiment_id: str, device_id: str,
                    user_id: Optional[str] = None,
                    num_variants: int = 2) -> str:
        """ 실험의 variant 결정 + cache. """

        # Cache key: 실험 ID 만으로 cache (한 device 내 일관)
        if experiment_id in self.cache:
            return self.cache[experiment_id]

        # Stable ID 결정:
        # - 1 회: device_id 사용
        # - 후속: 첫 결정 시 사용한 ID 유지 (cache 의 의미)
        # - User ID 가 있어도 첫 결정의 ID 따른다 (consistency)
        stable_id = device_id  # device 단위 stable

        # Hash-based assignment
        hash_input = f"{experiment_id}:{stable_id}".encode()
        hash_val = int(hashlib.md5(hash_input).hexdigest(), 16)
        variant_idx = hash_val % num_variants
        variant = chr(ord("A") + variant_idx)  # A, B, C, ...

        # Cache 저장 (offline 대비)
        self.cache[experiment_id] = variant
        self._save_cache()

        return variant

    def get_variant_with_failsafe(self, experiment_id: str,
                                   device_id: str,
                                   server_response: Optional[dict] = None,
                                   default: str = "A") -> str:
        """ Server response failsafe + cache + default variant. """

        # 1. Server response 있으면 사용
        if server_response and experiment_id in server_response:
            variant = server_response[experiment_id]
            self.cache[experiment_id] = variant
            self._save_cache()
            return variant

        # 2. Server unresponsive: cache 사용
        if experiment_id in self.cache:
            return self.cache[experiment_id]

        # 3. Cache 도 없음: deterministic hash + default
        try:
            return self.get_variant(experiment_id, device_id)
        except Exception:
            # 4. 모두 실패 시 default
            return default


# 사용 예시
import tempfile

with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
    cache_path = f.name

assignment = ExperimentAssignment(cache_path)

# 시나리오 1: 정상 server response
result1 = assignment.get_variant_with_failsafe(
    experiment_id="exp_search_box_text",
    device_id="device_abc123",
    server_response={"exp_search_box_text": "B"}
)
print(f"시나리오 1 (정상): {result1}")

# 시나리오 2: Server unresponsive (cache 사용)
result2 = assignment.get_variant_with_failsafe(
    experiment_id="exp_search_box_text",
    device_id="device_abc123",
    server_response=None
)
print(f"시나리오 2 (offline, cache hit): {result2}")

# 시나리오 3: 새 실험, server unresponsive (deterministic hash)
result3 = assignment.get_variant_with_failsafe(
    experiment_id="exp_button_color",  # 새 실험
    device_id="device_abc123",
    server_response=None
)
print(f"시나리오 3 (offline, 새 실험, hash): {result3}")

# 시나리오 4: 같은 device, 같은 실험 = 같은 variant (consistency)
result4 = assignment.get_variant_with_failsafe(
    experiment_id="exp_button_color",
    device_id="device_abc123",
    server_response=None
)
print(f"시나리오 4 (consistency): {result4}")
print(f"  → 시나리오 3 과 동일? {result3 == result4}")

# 정리
os.unlink(cache_path)

예상 출력.

시나리오 1 (정상): B
시나리오 2 (offline, cache hit): B
시나리오 3 (offline, 새 실험, hash): A
시나리오 4 (consistency): A
  → 시나리오 3 과 동일? True
직관 — Failsafe Hierarchy

이 코드의 핵심은 failsafe priority.

1. Server response → 가장 신뢰할 수 있음 (실험 design 반영)
2. Cache → 일관성 보장 (이전 결정 유지)
3. Deterministic hash → 새 실험에서도 일관 (같은 device 항상 같은 variant)
4. Hardcoded default → 모든 게 fail 했을 때 안전망

각 단계가 fall-through. 상위 단계 fail 시 하위로 자동 이동.

5.0.0.1 핵심 invariant: idempotency

같은 device + 같은 실험 → 항상 같은 variant. Network·cache·server 상태와 무관.

이 invariant 가 사용자 경험의 일관성 보장. Variant 가 실행 시점마다 바뀌면 confusion + analytic 혼란.

5.0.0.2 Edge case — Sign-up 후의 ID 전환

위 코드는 device_id 만 사용. 실제 운영에서는:

Sign-up 시점:
  - Device_id 의 cache 데이터를 user_id 로 migrate
  - 새 user_id 로 cache 재구성
  - 단 variant 자체는 유지 (device 결정의 일관성)

Migration 로직이 별도 필요. 그러나 single-device 단위에서는 위 코드만으로 충분히 robust.

이 설계가 thick client 실험의 robustness 의 본질. Web 실험에서는 cookie 가 자동 일관 유지하지만, thick client 는 명시적 cache + hash 설계 필요.

6 관련 주제

선행

다음 글

관련 챕터

다른 카테고리 연결

Subscribe

Enjoy this blog? Get notified of new posts by email: