콘텐츠로 이동

Experimentation & Feature Flags

분류: Layer 13 - Product Engineering & Growth Systems

제품 변경 뒤 지표가 올랐다는 사실만으로는 그 변경이 원인이라고 말할 수 없다. 같은 시기에 유입 채널, 요일, 가격, 장애가 함께 달라졌을 수 있기 때문이다. Product Engineer는 비교 가능한 집단을 만드는 실험사용자 노출을 제어하는 기능 플래그를 구분하고 연결해야 한다.

이 문서의 질문은 두 가지다.

  1. 관찰된 차이 중 제품 변경이 만든 인과 효과를 어떻게 분리할 것인가?
  2. 효과가 아직 불확실하거나 위험 신호가 보일 때 사용자 노출을 어떻게 제한할 것인가?

Experimentation(실험) 은 비교 조건과 판정 기준을 미리 정하고, 제품 변경이 결과에 미친 인과 효과를 추정하는 과정이다. 가장 단순한 A/B test(A/B 테스트) 는 기존 경험인 control과 변경 경험인 treatment에 단위를 무작위 배정해 두 집단의 결과를 비교한다. control과 treatment처럼 비교할 경험을 variant(변형) 라고 부른다.

Feature flag(기능 플래그) 는 실행 중인 코드가 어떤 경로를 선택할지 외부 설정으로 제어하는 장치다. 코드를 다시 배포하지 않고도 특정 workspace만 새 경로에 넣거나, 문제가 생긴 기능을 끄거나, 노출 비율을 단계적으로 높일 수 있다.

질문ExperimentationFeature flag
주된 목적변경의 인과 효과를 학습한다배포와 노출을 분리하고 위험 범위를 제어한다
필요한 핵심무작위 배정, 노출 기록, 지표 계약, 통계적 판단안정적인 targeting key, 기본값, kill switch, 제거 계획
답하지 못하는 것그 기능을 안전하게 즉시 끌 수 있는가관찰된 차이가 변경 때문인가
함께 쓸 때flag가 variant를 전달하고 실험 시스템이 배정·노출·결과를 연결한다실험 종료 뒤 rollout 또는 rollback을 수행한다

5% percentage rollout만 했다고 자동으로 실험이 되는 것은 아니다. 시간대별로 비율을 바꾸거나 특정 고객을 수동 추가하면 집단 구성이 달라진다. 반대로 실험 결과가 좋아도 flag에 안전한 기본값과 빠른 중단 경로가 없다면 출시는 취약하다.

2. 선행 방식의 한계 - 왜 두 도구가 함께 등장했는가

섹션 제목: “2. 선행 방식의 한계 - 왜 두 도구가 함께 등장했는가”

전통적인 release에서는 배포 단위와 사용자 노출 단위가 같았다. 배포가 끝나면 모든 사용자가 새 기능을 보았고, 문제가 생기면 전체 rollback이나 hotfix가 필요했다. 기능이 plan, region, role, workspace 상태에 따라 다르게 작동할수록 한 번의 전면 배포가 만드는 blast radius(영향 범위)는 커진다.

전후 비교도 한계가 있다. 월요일의 기존 화면과 금요일의 새 화면을 비교했을 때 conversion이 올랐더라도 마케팅 캠페인이나 사용자 구성 변화가 원인일 수 있다. Causal inference(인과 추론) 는 단순히 함께 변한 현상이 아니라, 다른 조건이 같을 때 변경이 결과를 얼마나 바꾸었는지 추정하려는 사고방식이다.

실험의 철학은 모든 불확실성을 제거하는 것이 아니다. 관찰할 수 없는 반사실을 비교 가능한 집단으로 근사하고, 남은 불확실성을 수치와 경계로 드러내는 것이다. feature flag의 철학은 새 코드를 숨기는 데 있지 않다. 변경의 전달 범위와 중단 비용을 코드 배포 주기에서 분리하는 데 있다.

따라서 이 문서는 테스트 전략의 연장선이지만 대상이 다르다. 자동화 테스트는 코드가 명세대로 작동하는지 검증하고, 실험은 그 명세대로 작동하는 변경이 사용자 결과를 실제로 개선하는지 평가한다.

한 workspace가 같은 순간에 control과 treatment를 모두 경험할 수는 없다. treatment를 경험한 workspace의 결과는 볼 수 있지만, 그 workspace가 control을 경험했더라면 어떤 결과가 나왔을지는 관찰할 수 없다. 이것이 counterfactual(반사실) 문제다.

무작위 배정은 많은 workspace를 control과 treatment로 나누어, 관찰된 특성과 관찰되지 않은 특성이 평균적으로 비슷한 두 집단을 만든다. 그러면 두 집단의 평균 결과 차이를 변경의 평균 인과 효과로 해석할 근거가 생긴다. 개별 workspace의 반사실을 복원하는 것이 아니라 집단 평균으로 근사하는 것이다.

이 추론이 성립하려면 다음 연결이 끊기지 않아야 한다.

eligible population
-> random assignment
-> actual exposure
-> outcome event
-> analysis at the randomization unit
  • Eligible population(적격 모집단) 은 실험 시작 전에 정한 포함·제외 조건을 만족하는 대상이다.
  • Random assignment(무작위 배정) 은 적격 단위를 확률 규칙으로 variant에 넣는 과정이다.
  • Exposure(노출) 는 배정된 경험이 실제로 사용자에게 전달된 순간이다. flag 평가만 했지만 화면을 보지 못했다면 배정은 있었어도 제품 노출은 없을 수 있다.
  • Outcome(결과) 은 변경이 영향을 주리라 예상한 사용자 또는 제품 상태다.
  • 분석 단위는 원칙적으로 배정 단위와 맞아야 한다. workspace로 배정하고 invitation 행 수를 독립 표본처럼 세면 표본 수와 확신을 부풀릴 수 있다.

무작위 배정은 마법이 아니다. 배정 뒤 treatment에만 적용한 필터, variant별 이벤트 누락, 서로 영향을 주는 사용자, 중간에 바뀐 식별자는 비교 가능성을 깨뜨린다. 그래서 uplift보다 먼저 배정과 데이터 품질을 확인한다.

4. Worked example - workspace 초대 실험 계약

섹션 제목: “4. Worked example - workspace 초대 실험 계약”

가상의 협업 제품에서 신규 workspace admin에게 보여 주는 초대 모달의 정보 순서와 CTA를 바꾼다고 하자. 두 variant 모두 초대 모달을 제공하고, treatment만 설명과 CTA 배치를 바꾼다. 실험이 관찰할 핵심 제품 흐름은 다음 세 사건이다.

invite_modal_opened -> invite_created -> invite_accepted

같은 이벤트 이름도 trigger(발생 조건), identity(식별자), deduplication(중복 제거) 단위가 다르면 다른 숫자를 만든다. 아래 계약은 클라이언트 클릭이 아니라 확인 가능한 상태 전이를 우선한다. workspace_createdITT(Intention-to-Treat, 실제 노출 여부와 무관하게 배정된 적격 집단 전체를 유지하는 분석) activation의 시작점이고, invite_modal_closed는 즉시 funnel window를 닫는 보조 lifecycle event다. 여기서 적격 집단은 뒤에서 정의하는 eligible workspace이며, 두 이벤트 모두 핵심 세 이벤트를 대체하지 않는다.

아래 모든 이벤트는 Tracking 문서의 공통 envelope인 event_id, occurred_at, schema_version, sampling_policy_version을 먼저 가진다. 표에서는 사건별 필드에 집중하기 위해 이를 반복하지 않는다. 이 예제의 전량 수집 이벤트는 sampling_policy_version=full-v1이고, 정책을 바꾸면 새 version으로 구분한다. experiment_exposed.event_id는 해당 노출의 전역 고유 ID이며, RUM에서 참조할 때 이름만 experiment_exposure_id로 사용한다. 즉 rum_event.experiment_exposure_id = experiment_exposed.event_id가 명시적 join 계약이다.

이벤트triggeridentity와 event timeingestion dedup / 논리 uniqueness·metric 역할
workspace_created서버 트랜잭션이 새 Workspace를 commit하고 같은 성공 경계의 outbox에 도메인 이벤트를 저장한 순간workspace_id, creator인 actor_user_id, 필수 domain_event_id, commit 시각인 occurred_atdomain_event_id로 ingestion 중복을 제거한다. (event_type, workspace_id)는 생성 사실의 도메인 논리 uniqueness와 ITT cohort join에 사용한다
invite_modal_opened초대 모달이 workspace 문맥에서 실제로 렌더링되어 상호작용 가능한 순간. 단순 flag 평가나 버튼 hover에는 기록하지 않는다workspace_id, actor_user_id, 한 번의 진입인 flow_id, 한 노출인 modal_view_id, experiment_id, variant, occurred_at클라이언트 수집 중복은 (event_type, modal_view_id)로 제거한다. 지표에서는 cohort 기간의 workspace_id당 첫 qualifying modal 한 번만 센다
invite_modal_closed열려 있던 모달이 명시적 닫기, 취소, route 이동, unmount, 생성 성공 뒤 dismissal로 visible에서 not-visible 상태가 된 순간open과 동일한 workspace_id, actor_user_id, flow_id, modal_view_id, 실제 종료 시각인 closed_at, 선택 close_reason클라이언트 수집 중복은 (event_type, modal_view_id)로 제거한다. close가 유실되면 metric 계산에서 opened_at + 30분을 종료 시각으로 사용한다
invite_created서버 트랜잭션이 pending Invite와 전달 job/outbox를 같은 성공 경계에 commit한 순간. 제출 클릭이나 API 시작은 제외한다항상 workspace_id, actor_user_id, invite_id, 필수 domain_event_id, occurred_at; UI modal 경로에서만 flow_id, modal_view_id, experiment_id, variant가 조건부로 필요domain_event_id로 ingestion 중복을 제거한다. (event_type, invite_id)는 하나의 생성 사실이라는 도메인 논리 uniqueness이고, invite_id는 accepted와의 join·metric unique key다
invite_accepted유효한 Invite가 pending -> accepted로 전이되고 해당 workspace의 Membership 생성이 같은 성공 경계에서 commit된 순간workspace_id, invite_id, membership_id, accepting actor_user_id, 필수 domain_event_id, occurred_atdomain_event_id로 ingestion 중복을 제거한다. (event_type, invite_id)는 최초 accepted 상태 전이의 논리 uniqueness이고, invite_id는 created와의 join·metric unique key다

canonical server event인 workspace_created, invite_created, invite_accepteddomain_event_id는 domain state와 outbox를 commit할 때 한 번 생성한다. publisher·broker·collector가 재시도해도 같은 값을 유지해야 하며 ingestion의 필수 dedup key다. 값이 없으면 (event_type, invite_id)로 추측해 수용하지 않고 quarantine(격리)하여 producer 계약을 고친다. (event_type, invite_id)(event_type, workspace_id)는 ingestion fallback이 아니라 도메인에서 같은 논리적 상태 전이가 두 번 생기지 않게 하는 uniqueness 또는 분석 join 역할이다.

flow_id는 modal open부터 create까지 한 번의 UI 시도를 연결하고, modal_view_id는 그 시도에서 실제로 보인 한 번의 모달 노출을 식별한다. 수락은 다른 사용자·기기·시간에 일어날 수 있으므로 flow_id가 아니라 생성 때 발급된 invite_id로 created와 accepted를 연결한다.

Qualifying modal(지표 대상 모달) 은 eligible workspace가 분석 기간에 처음 연 모달 중 flow_id, modal_view_id, opened_at이 유효하고 실제로 visible·interactive 상태가 된 모달이다. 같은 flow_idmodal_view_id의 첫 유효 invite_modal_closed.closed_at을 종료 시각으로 사용하되, close event가 유실되거나 closed_at > opened_at + 30분이면 opened_at + 30분으로 대체한다. closed_at < opened_at이거나 다른 flow에 속한 close는 close row만 데이터 품질 오류로 격리하고, 유효한 open에는 opened_at + 30분 fallback을 적용한다. 따라서 유효 window는 반열린 구간 opened_at <= invite_created.occurred_at < min(closed_at, opened_at + 30분)이며, 유효한 close가 없을 때 오른쪽 경계는 opened_at + 30분이다.

이 계약은 이벤트 순서도 검증 가능하게 만든다. 같은 invite_idinvite_accepted보다 앞선 invite_created가 없다면 instrumentation 누락, 잘못된 join, legacy 초대 유입 중 하나를 의심해야 한다. 한 workspace가 모달을 열 번 열고 초대를 다섯 명 수락해도 아래 workspace-level rate의 분자와 분모에는 각각 한 번만 기여한다.

4.2 모호한 rate를 세 지표로 분해한다

섹션 제목: “4.2 모호한 rate를 세 지표로 분해한다”

“7일 내 invite_accepted rate”는 사용할 수 없는 정의다. 무엇부터 7일인지, 초대 건을 세는지 workspace를 세는지, 모달을 열지 않은 workspace가 분모인지 알 수 없기 때문이다. 이 실험에서는 다음 세 지표를 별개로 둔다.

이 worked example에서 eligible new workspace는 enrollment 구간 [start_at, end_at)에 production에서 처음 생성되고, 생성자가 admin이며, 생성 시점에 is_internal = false, is_test = false, is_automated = false인 workspace다. region이나 plan 제한이 있다면 역시 생성 시점 값으로 배정 전에 고정한다. treatment 결과를 본 뒤 제외 조건을 추가하지 않으며 control과 treatment에 같은 조건을 적용한다.

지표cohort와 분모분자window와 dedup답하는 질문
invite_creation_rate실험 기간에 첫 qualifying invite_modal_opened가 발생한 eligible unique workspace같은 flow_id에서 opened_at <= invite_created.occurred_at < min(closed_at, opened_at + 30분)을 만족하는 workspaceclose 유실 시 opened_at + 30분을 fallback 종료로 쓴다. lifecycle은 modal_view_id, 서버 event는 domain_event_id, 생성 사실은 invite_id, 최종 rate는 workspace_id로 unique 처리한다모달을 본 workspace가 같은 시도에서 생성까지 갔는가
7-day accepted/modal funnel outcome실험 기간에 첫 qualifying invite_modal_opened가 발생한 eligible unique workspace같은 flow_id의 creation window 안에서 생성된 invite_id 중 하나 이상이 최초 open부터 7 x 24시간 안에 invite_accepted가 된 workspacecreation window는 위와 같은 min(closed_at, opened_at + 30분)이다. workspace_id당 1회, 수락 연결은 dedup된 invite_id를 사용하며 미성숙 workspace는 보류한다모달 노출 뒤 생성과 수락까지 이어졌는가
새 workspace 7-day activation_rateenrollment 기간에 생성되어 사전 적격 조건을 만족한 randomized unique new workspace 전체. modal open·exposure 여부와 무관하게 ITT 분모에 남긴다workspace_created.occurred_at <= invite_created.occurred_at <= invite_accepted.occurred_at < workspace_created.occurred_at + 7일이고 created와 accepted의 invite_id·workspace_id가 같은 workspace서버 event는 필수 domain_event_id로 ingestion dedup하고, created·accepted는 같은 invite_id로 join한다. 최종 지표는 workspace_id당 1회이며 미성숙 7일 cohort는 확정하지 않는다신규 workspace가 modal과 무관하게 7일 안에 협업 시작 상태에 도달했는가

첫 두 지표는 모달을 실제로 본 집단의 funnel을 설명한다. 세 번째 지표는 배정된 eligible new workspace 전체의 modal 비종속 ITT outcome이며, modal을 열지 못했거나 다른 surface에서 초대를 만든 workspace도 분모와 판정에 남긴다. 따라서 세 숫자는 이름만 다른 같은 지표가 아니다.

예를 들어 신규 workspace가 100개, 그중 첫 qualifying modal을 연 workspace가 80개, 같은 flow_id에서 modal 종료 또는 최대 30분 안에 초대를 만든 workspace가 48개, 해당 invite_id 중 하나가 modal open 뒤 7일 안에 수락된 workspace가 24개라고 하자. 여기에 모달이 아닌 API surface에서 초대를 생성·수락한 eligible workspace가 3개 더 있다면 activation 분자는 27개다.

invite_creation_rate = 48 / 80 = 60%
7-day accepted/modal funnel outcome = 24 / 80 = 30%
new workspace 7-day activation_rate = 27 / 100 = 27%

60%를 activation이라고 부르거나 30%를 모든 신규 workspace의 결과라고 부르면 제품 판단이 달라진다. API 경로의 3개가 activation에는 들어가지만 modal 지표에는 들어가지 않는 것도 같은 이유다. 특히 treatment가 모달 자체의 도달률을 바꿀 수 있다면 노출자만 비교한 funnel outcome은 변경의 전체 효과를 숨긴다.

좋은 실험은 variant보다 가설이 먼저다. 위 정의를 사용하면 가설은 다음처럼 쓸 수 있다.

  • MDE(Minimum Detectable Effect, 최소 탐지 효과) 는 사전에 정한 유의수준과 power로 효과 = 0인 귀무가설과 구분하도록 표본을 계획할 때 가정하는 최소 효과 크기다. 이 문서의 +2%p MDE는 fixed-horizon 표본 계획값이지 참 효과가 +2%p 이상인지 검정하는 superiority margin이 아니다.
  • 95% CI(95% Confidence Interval, 95% 신뢰구간) 는 같은 표집·계산 절차를 반복할 때 만들어진 구간의 95%가 참 효과를 포함하도록 구성한 불확실성 범위다.
  • p-value(p값) 는 귀무가설과 분석 가정이 맞을 때 현재 관측값만큼 또는 더 극단적인 결과가 나올 확률이다. 효과 크기나 귀무가설이 참일 확률이 아니다.
  • Fixed-horizon(고정 종료점) 은 시작 전에 표본 수와 outcome 성숙 시점을 정하고 그 종료점에서 한 번 판정하는 설계다. 중간 결과에 따라 표본을 임의로 늘리지 않는다.
Population:
enrollment 기간에 처음 생성된 eligible workspace
Randomization unit:
workspace_id, control 50% / treatment 50%, 최초 배정을 실험 종료까지 고정
Change:
treatment는 초대 모달의 정보 순서와 CTA를 변경
Primary outcome:
새 workspace 7-day activation_rate
Mechanism metrics:
invite_creation_rate, 7-day accepted/modal funnel outcome
Guardrails:
invite_create_failed_rate, p95 invite API latency,
workspace_created -> first_project_created conversion, support tickets per 1k workspace
Fixed-horizon planning:
양측 유의수준 5%, power 80%, MDE +2%p로
효과가 0인지 구분할 workspace 표본 수와 종료 시점을 사전 고정
Practical ship threshold:
activation point estimate +2%p 이상
Decision rule for this example:
fixed horizon에서 activation uplift의 95% 신뢰구간이 0을 배제하고,
point estimate가 사전 practical threshold +2%p 이상이며,
guardrail uplift의 95% 신뢰구간 상한은 각 hold 경계보다 작아야 한다

“새 버튼이 더 좋을까?”는 반증 기준이 없다. 위 계약은 대상, 변경, 배정 단위, 결과, 시간 범위, 최소 관심 효과와 안전 경계를 드러낸다. 관찰 뒤 가장 좋아 보이는 분모나 기간으로 바꾸는 일을 막는 것도 가설의 역할이다.

5. Randomization unit - 무엇을 한 번 배정할 것인가

섹션 제목: “5. Randomization unit - 무엇을 한 번 배정할 것인가”

Randomization unit(무작위 배정 단위) 은 control 또는 treatment에 독립적으로 배정되는 최소 단위다. 결과를 만드는 상태의 소유 경계와 맞춰야 한다.

단위적합한 경우위험
User개인별 UX이고 다른 사용자에게 영향이 거의 없음같은 workspace 구성원이 서로 다른 경험을 보고 상태를 공유할 수 있음
Workspace/Account협업, 권한, 결제처럼 조직 상태가 바뀜사용자 수보다 독립 표본 수가 줄고 큰 workspace의 영향이 다를 수 있음
Session서로 독립적인 일회성 경험같은 사용자가 여러 variant를 보아 학습 효과와 일관성 문제가 생김
Request검색 알고리즘이나 인프라 경로의 요청별 비교한 사용자의 연속 경험이 흔들리고 결과가 서로 영향을 줄 수 있음

초대 실험은 workspace 상태를 바꾼다. user 단위로 배정하면 같은 workspace의 두 admin이 서로 다른 모달을 보고, 한 admin이 만든 Invitation과 Membership이 다른 admin의 결과에 영향을 준다. 이는 interference(간섭), 즉 한 단위에 준 처리가 다른 단위의 결과를 바꾸는 문제다. 이 사례는 workspace 단위 배정이 더 자연스럽다.

배정 구현에서는 hash(experiment_id, workspace_id)처럼 안정적인 key로 variant를 결정하고 최초 배정을 고정한다. Math.random()을 요청마다 호출하거나 로그인 전에는 device, 로그인 후에는 user, 서버에서는 workspace를 쓰면 multiple exposure가 생긴다.

퀴즈

workspace 초대 UX를 user 단위로 A/B 테스트하면 어떤 문제가 생길 수 있는가?

힌트: 초대는 개인 UI가 아니라 팀 상태를 바꾼다.

정답 보기

같은 workspace 안의 admin들이 서로 다른 초대 UX를 보거나, 한 variant에서 만든 Invitation과 Membership이 다른 variant 사용자의 결과에 영향을 줄 수 있다. 협업·권한·결제 흐름은 workspace/account 단위 randomization을 검토해야 한다.

Assignment(배정) 은 실험 단위를 variant에 넣은 사실이고, exposure(노출) 는 그 variant가 실제 경험에 영향을 준 순간이다. 초대 실험에서는 workspace 생성 시 배정할 수 있지만, admin이 초대 모달을 실제로 볼 때의 첫 invite_modal_opened를 노출 시각으로 사용한다.

둘을 분리해야 하는 이유는 세 가지다.

  1. 배정됐지만 모달을 열지 않은 workspace가 있을 수 있다.
  2. 모달을 먼저 본 뒤 잘못된 순서로 exposure를 기록하면 outcome을 보고 표본을 고르는 문제가 생긴다.
  3. treatment가 모달 도달 자체를 바꾸면 노출자만 비교한 집단은 더 이상 무작위로 비교 가능하지 않을 수 있다.

Primary outcome은 보통 ITT(Intention-to-Treat, 최초 배정 기준 분석) 로 본다. 실제 노출 여부와 상관없이 처음 배정된 모든 eligible workspace를 원래 variant에 남겨 비교하는 방식이다. ITT는 미노출 때문에 효과가 희석될 수 있지만, 배정 뒤 행동으로 대상을 다시 고르며 생기는 선택 편향을 줄인다.

노출자 기준 invite_creation_rate7-day accepted/modal funnel outcome은 메커니즘을 이해하는 데 유용하다. 다만 이를 ITT activation과 같은 인과 추정치로 부르면 안 된다. 배정 기준 결과, 노출 기준 funnel, 전체 제품 health 지표를 나란히 두어야 “효과가 없었다”와 “기능이 사용자에게 도달하지 않았다”를 구분할 수 있다.

7. Primary, secondary, guardrail을 역할로 나눈다

섹션 제목: “7. Primary, secondary, guardrail을 역할로 나눈다”

Primary metric(주요 지표) 은 가설의 성공 여부를 판단하는 대표 결과다. Secondary metric(보조 지표) 은 효과가 어떤 경로로 발생했는지 설명한다. Guardrail metric(안전 경계 지표) 은 primary가 좋아져도 넘으면 출시를 중단하거나 재검토할 피해 경계다.

실험 지표의 네 역할

Primary metric

실험이 개선하려는 핵심 결과

예: 새 workspace 7-day activation_rate

Secondary metric

효과가 생긴 경로를 설명하는 보조 결과

예: invite_creation_rate, 7-day accepted/modal funnel outcome

Guardrail metric

악화되면 실험을 중단하거나 재검토할 안전 지표

예: invite_create_failed_rate, p95 latency, support tickets

Data quality metric

배정·노출·수집이 정상인지 확인하는 진단 지표

예: SRM, multiple exposure, missing assignment

guardrail은 결과를 본 뒤 불편한 숫자를 설명하기 위한 목록이 아니다. 시작 전에 metric, 허용 악화 폭, 관찰 window, 중단 owner를 정한다. 예를 들어 invite_create_failed_rate는 초대 생성 API의 unique request 중 실패한 request 비율로 정의하고, treatment-control 차이의 95% CI 상한이 +0.5%p hold 경계에 닿으면 hold한다고 정할 수 있다. threshold(임계값)는 예시이며 실제 값은 baseline 변동과 사업 위험으로 정한다.

Product Engineer는 제품 metric과 시스템 metric을 함께 본다. activation이 올라도 latency, error, accessibility issue가 악화되면 좋은 release가 아니다. 반대로 guardrail이 조금 움직였다는 이유만으로 무조건 중단하지 않고, 사전 경계와 불확실성, 피해 크기를 함께 본다.

8. 결과보다 먼저 실험 건강을 진단한다

섹션 제목: “8. 결과보다 먼저 실험 건강을 진단한다”

SRM(Sample Ratio Mismatch, 표본 비율 불일치) 은 실제 분석에 들어온 randomization unit의 variant 비율이 사전에 설정한 비율과 통계적으로 맞지 않는 현상이다. 50:50 배정에서 10,000개 workspace를 기대했는데 control 5,400, treatment 4,600이라면 treatment 쪽이 기대치 5,000에서 400만큼 벗어났다. 단순한 우연 변동으로 보기 어려우므로 uplift를 읽기 전에 배정과 수집 경로를 조사해야 한다.

SRM은 원인 이름이 아니라 실패 신호다. 다음 층에서 원인을 찾는다.

가능한 원인먼저 비교할 것
Assignment잘못된 bucketing, key 변경, 특정 variant 배정 실패원본 assignment log의 variant별 unique workspace 수
Execution한 variant만 redirect·crash, flag 기본값 차이assignment 대비 첫 exposure 도달률
Collectionad blocker, client 이벤트 유실, variant property 누락서버 배정 수와 수집된 exposure 수의 차이
Join/Analysisidentity merge 실패, outcome 보유자만 join, 배정 뒤 필터join 전후 variant별 탈락 수와 제외 사유

함께 확인할 데이터 품질 신호는 다음과 같다.

  • Multiple exposure(다중 노출): 같은 workspace_id가 둘 이상의 variant를 경험한다. randomization key 변경, 캐시 불일치, 로그인 전후 identity merge에서 생긴다.
  • Missing assignment(배정 누락): outcome은 있는데 대응하는 실험 배정이나 variant가 없다.
  • Impossible sequence(불가능한 순서): 같은 invite_idinvite_acceptedinvite_created보다 먼저이거나, 모달 실험인데 invite_modal_opened 없이 funnel outcome만 있다.
  • Late event(지연 이벤트): 7일 window가 끝난 뒤 도착한 이벤트를 무조건 버리거나 다른 cohort에 넣어 variant별 지연 차이를 만든다.

SRM 검정의 p-value가 작다는 것은 “설정한 배정 비율과 현재 데이터가 이 정도 이상 어긋날 가능성이 작다”는 진단 신호이지, treatment가 효과적이라는 뜻이 아니다. SRM, multiple exposure, 누락을 해결하지 못하면 primary 결과를 ship 근거로 사용하지 않는다.

9. Power - 답할 수 있는 크기의 실험인가

섹션 제목: “9. Power - 답할 수 있는 크기의 실험인가”

Statistical power(통계적 검정력) 는 실제로 관심 있는 크기의 효과가 있을 때 실험이 그 효과를 탐지할 확률이다. 보통 실험 전에 다음 값을 함께 정한다.

  • baseline rate: control에서 예상하는 현재 비율
  • planning MDE: 효과 = 0과 구분하도록 fixed-horizon 표본 수를 계산할 때 가정한 최소 차이
  • significance level(유의수준): 효과가 없는데 있다고 판단할 허용 확률. 예시는 양측 5%
  • target power: MDE가 실제일 때 탐지할 목표 확률. 예시는 80%
  • randomization unit 수, 배정 비율, metric variance(분산)

초대 실험의 baseline activation이 18%이고, 2%p 개선을 MDE로 두며, 양측 유의수준 5%, power 80%, 50:50 배정을 가정하자. 독립적인 두 비율 비교의 단순 근사에서는 variant당 약 6,000개 workspace가 필요하다. 이는 0과의 차이를 탐지하기 위한 계획용 수치이며 실제 계산은 사용하는 분석 방법, variance reduction, 제외 규칙에 맞는 calculator로 다시 확인해야 한다.

여기서 초대 event가 60,000건이라고 표본이 60,000이 되는 것은 아니다. workspace로 배정했다면 독립 표본의 기반은 workspace 수다. 큰 workspace가 초대를 많이 보낸다는 이유로 invitation 행을 독립 단위처럼 세면 표준 오차를 과소평가할 수 있다.

계획한 6,000개 중 variant당 2,000개만 모인 중간 시점의 차이가 명확하지 않다고 “효과가 없다”고 결론 내리거나 종료점을 뒤로 미루지 않는다. 사전 horizon과 7일 outcome 성숙을 기다린다. fixed horizon에서 신뢰구간이 0을 가로지르면 inconclusive(효과 방향을 결론 내리기 어려움) 이고, 양의 효과가 구분되지만 point estimate가 practical threshold보다 작으면 실용성 부족으로 no-ship이다. 신뢰구간이 명확히 음수면 harmful/no-ship, guardrail 경계를 넘으면 hold 또는 rollback으로 분류한다. 사후에 같은 실험의 표본만 더 모으지 않고, 추가 학습 가치가 있으면 새 가설·표본 계획·실험 ID로 후속 실험을 설계한다. 아주 큰 표본에서 0.1%p 차이가 명확해도 제품 가치가 비용보다 작다면 ship 근거가 약하다는 경계도 그대로다.

7일 activation을 측정한다면 마지막으로 배정된 workspace도 7일을 관찰해야 한다. 목표 표본에 도달한 즉시 결과를 읽으면 최근 cohort의 outcome이 덜 성숙한 right censoring(우측 중도절단) 이 생긴다. 필요한 표본 수, 자연스러운 주간 주기, outcome 성숙 window를 모두 만족할 때 판정한다.

10. Feature flag는 배포 제어면이다

섹션 제목: “10. Feature flag는 배포 제어면이다”

feature flag는 단순한 on/off 스위치보다 넓다. 목적에 따라 수명과 실패 시 기본 동작이 다르다.

패턴목적예시제거·유지 기준
Release flag배포와 노출 분리새 onboarding을 5%만 노출100% 안정화 뒤 분기와 구 코드를 제거
Experiment flag고정된 variant 비교control vs treatment분석 종료와 ship 결정 뒤 승자 경로로 통합
Permission flag특정 사용자·plan 접근beta customer allowlistentitlement로 옮길지 장기 정책을 명시
Operational flag장애 시 기능 축소heavy export 기능 임시 off장애 대응 가치가 지속되면 소유자와 정기 훈련 유지
Kill switch위험 기능 즉시 중단결제 webhook 오류 시 신규 checkout off긴급 중단 경로를 주기적으로 검증

실험 flag에는 안정적인 배정, variant 값, 노출 telemetry가 필요하다. release flag에는 재시작 없이 반영되는 제어 경로, 안전한 default, audit log가 더 중요할 수 있다. permission flag를 임시 release flag처럼 운영하면 접근 권한이 설정 실수에 의존하고, experiment flag를 영구 entitlement처럼 남기면 제품 규칙이 코드 밖에 흩어진다.

flag가 늘어나면 flag debt(플래그 부채) 가 생긴다. 종료된 실험, 임시 allowlist, 오래된 fallback 분기는 테스트 조합과 인지 부하를 키운다. 생성 시 owner, 목적, 생성일, 예상 종료일, 제거 조건을 기록하고, cleanup은 rollout의 마지막 단계가 아니라 완료 조건으로 본다.

11. Experiment와 rollout은 다른 단계다

섹션 제목: “11. Experiment와 rollout은 다른 단계다”

Rollout(점진적 출시) 은 이미 배포된 기능의 노출 범위를 단계적으로 넓히는 과정이다. 실험이 “효과가 있는가”를 묻는다면 rollout은 “더 넓은 실제 트래픽에서도 안전한가”를 묻는다.

  1. internal dogfood: 내부 계정에서 이벤트 계약과 기본 동작을 확인한다.
  2. beta allowlist: 동의한 일부 고객 또는 workspace에서 질적 피드백과 경계 사례를 본다.
  3. canary rollout: 1% 또는 5%에 노출해 오류, latency, RUM(Real User Monitoring, 실제 사용자 모니터링) 신호를 확인한다.
  4. controlled experiment: 고정된 적격 모집단, 배정 비율, metric 계약으로 인과 효과를 평가한다.
  5. gradual rollout: 판정 뒤 25% -> 50% -> 100%로 확대하며 guardrail을 다시 본다.
  6. cleanup: flag와 dead code를 제거하고 최종 metric·결정을 기록한다.

실험 중 트래픽을 10%, 25%, 50%로 자주 바꾸면 각 시점의 사용자 구성과 관찰 기간이 섞일 수 있다. 표본을 늘려야 한다면 실험 플랫폼의 allocation 정책에 따라 새 iteration이 필요한지 판단한다. 안전한 rollout의 속도와 통계 실험의 고정 계약을 같은 숫자 하나로 관리하지 않는다.

rollback 기준도 미리 정한다. 치명적 데이터 손상이나 결제 오류는 즉시 kill switch를 작동시키고, 완만한 latency 악화는 사전 threshold와 지속 시간에 따라 hold할 수 있다. rollback 뒤에는 flag를 끈 사실만 확인하지 말고 신규 요청이 구 경로로 돌아갔는지, 이미 생성된 상태를 복구해야 하는지, guardrail이 baseline으로 회복했는지 본다.

12. 판정 실습 - uplift만 보고 ship하지 않는다

섹션 제목: “12. 판정 실습 - uplift만 보고 ship하지 않는다”

다음은 사전 fixed horizon과 7일 outcome 성숙이 모두 끝난 뒤의 예시다. 모든 비율은 사전 정의한 workspace 단위 dedup을 적용했다고 가정한다. 아래 95% CI는 해석 연습용이며, 비율에는 두 비율 차이 구간을, p95 latency에는 해당 분포에 맞는 bootstrap 구간을 사용했다고 가정한다.

항목ControlTreatmentUplift와 95% 신뢰구간사전 판정 경계
assignment6,0125,988해당 없음50:50, SRM 없음
새 workspace 7-day activation_rate18.0%20.1%+2.1%p [+0.7, +3.5]CI가 0 배제, point +2.0%p 이상
invite_creation_rate56.0%62.4%+6.4%p [+4.7, +8.1]mechanism metric
7-day accepted/modal funnel outcome29.8%31.2%+1.4%p [-0.2, +3.0]mechanism metric
invite_create_failed_rate1.2%1.3%+0.1%p [-0.1, +0.3]hold: +0.5%p 이상
p95 invite API latency540ms560ms+20ms [-5, +45]hold: +100ms 이상

판정은 점추정치 하나가 아니라 구간과 경계의 위치를 비교한다.

  1. activation uplift 구간 [+0.7, +3.5]%p0을 포함하지 않는다. 이 예시에서는 “변경 효과가 0과 구분되지 않는다”는 설명보다 양의 효과라는 설명을 더 지지한다.
  2. 관측 uplift +2.1%p는 사전 practical threshold +2.0%p 이상이다. 같은 숫자인 MDE +2.0%p0 대비 fixed-horizon 표본 계획에만 사용했으며 판정 margin이 아니다. 신뢰구간 하한은 +0.7%p이므로 이 ship 규칙을 통과해도 참 효과가 +2.0%p 이상임을 증명하지 않는다. point estimate가 MDE보다 크다는 사실만으로 통계적 구분 가능성이 생기는 것도 아니며, 이 예시의 구분 가능성은 CI가 0을 배제한다는 별도 조건에서 온다.
  3. invite_creation_rate 구간은 0 위에 있지만 accepted/modal 구간은 0을 가로지른다. 따라서 생성 단계 개선은 비교적 선명하지만 수락 단계의 추가 효과는 아직 불확실하다고 읽는다.
  4. 두 guardrail의 신뢰구간 상한 +0.3%p, +45ms는 사전 hold 경계 +0.5%p, +100ms보다 낮다. 현재 표본에서는 경계를 넘는 악화를 뒷받침하지 않지만, 피해 가능성이 0이라고 증명한 것은 아니므로 rollout 중 계속 관찰한다.

이 예시는 fixed horizon에서 CI가 0을 배제하고, point estimate가 사전 practical threshold를 넘으며, guardrail도 통과했으므로 ship 후 gradual rollout으로 판정한다. 이 판정은 기대 효과와 위험을 함께 만족했다는 제품 규칙이지, true effect가 +2.0%p 이상이라는 superiority 결론이 아니다. 조건을 충족하지 못했을 때는 앞 절의 분류를 따른다. 효과 방향이 불확실할 때만 inconclusive이고, 실용성 부족은 no-ship, 명확한 악화는 harmful/no-ship, guardrail 위반은 hold/rollback이다.

12.2 반례 - primary가 좋아도 hold할 수 있다

섹션 제목: “12.2 반례 - primary가 좋아도 hold할 수 있다”

시나리오

새 checkout UX를 100% 배포할지 결정해야 한다

treatment variant의 checkout_completed rate는 올랐지만, payment_failed rate와 support ticket이 같이 증가했다.

primary metric만 보고 승리 선언하지 않으려면 어떤 guardrail과 세그먼트를 확인할 것인가?
항목ControlTreatment판단
assignment ratio50.1%49.9%SRM 의심 낮음
checkout_completed rate18.2%20.1%primary metric 개선
payment_failed rate2.1%3.8%guardrail 악화
p95 checkout latency820ms1,420msperformance guardrail 악화
support ticket per 1k checkout4.27.9사용자 혼란 가능

이 경우 기본 판단은 ship이 아니라 hold다. conversion은 좋아졌지만 결제 실패, 지연, support ticket이 함께 증가했다. 다음 iteration에서는 payment provider, mobile latency, 오류 단계별 segment를 분리한다. 피해가 진행 중이면 feature flag로 노출을 25% 이하로 유지하거나 rollback하고 hotfix를 검증한다.

반대로 primary가 개선되고 guardrail이 baseline과 실질적으로 같으며 SRM과 multiple exposure가 없다면 staged rollout을 진행할 수 있다. 이때도 cleanup 조건, rollback 기준, monitoring window를 같이 정해야 한다.

13. 경계 - 실험하지 않는 편이 나은 경우

섹션 제목: “13. 경계 - 실험하지 않는 편이 나은 경우”

실험은 모든 결정을 대신하지 않는다.

  • 보안 결함, 법적 의무, 개인정보 침해처럼 일부 사용자에게 의도적으로 열어 둘 수 없는 변경
  • 접근성 차단이나 데이터 손상처럼 낮은 빈도라도 피해가 치명적인 변경
  • 독립 단위를 만들기 어려운 강한 network effect 때문에 control이 treatment의 영향을 직접 받는 경우
  • 트래픽이 너무 적어 의사결정 기한 안에 현실적인 MDE의 power를 확보할 수 없는 경우
  • 되돌릴 수 없는 migration처럼 노출 제어보다 사전 검증과 복구 설계가 우선인 경우

이때는 자동화 테스트, staged rollout, 관찰 연구, 사용자 인터뷰, replay, shadow traffic 같은 다른 증거를 조합한다. 실험을 하지 않는다는 뜻은 근거 없이 출시한다는 뜻이 아니다. 질문과 피해 구조에 맞는 검증 방법을 고른다는 뜻이다.

결과 대시보드보다 먼저 다음 실패 신호를 본다.

실패 신호의미먼저 확인할 경계
variant 비율이 예상과 다름비교 집단 생성 또는 수집이 깨짐SRM을 assignment, exposure, join 단계로 분해
같은 workspace에 variant가 둘 이상안정적 배정이 깨짐randomization key, cache, identity merge
outcome은 있는데 assignment가 없음결과와 실험 집단 연결이 깨짐event schema와 join key
server event에 domain_event_id가 없음canonical ingestion dedup 불가event quarantine, producer outbox 계약
modal close가 없음lifecycle 종료 수집이 유실됨같은 flow_id·modal_view_id, 30분 fallback
closed_at < opened_atlifecycle event time이 역전됨close row만 quarantine, open+30분 fallback
accepted가 created보다 먼저 보임이벤트 의미·지연·join이 깨짐invite_id와 server commit trigger
activation과 accepted/modal을 같은 이름으로 보고cohort와 분모가 섞임metric registry의 cohort, window, unique unit
목표 표본 도달 직후 7일 지표 판정최근 cohort가 덜 성숙함observation maturity와 right censoring
primary만 개선하고 guardrail 악화국소 최적화가 제품 피해를 숨김사전 threshold와 segment
실험 종료 뒤 flag가 계속 남음flag debt와 상태 조합 증가owner, expiry, 제거 PR

판정 순서는 계약 확인 -> SRM·노출 품질 -> 표본과 window 성숙 -> primary와 불확실성 -> guardrail -> segment -> rollout/hold/rollback이다. 순서를 뒤집어 uplift부터 보면 데이터 품질 문제를 제품 효과로 오해하기 쉽다.

Experiment/Feature Flag 체크

  • 가설에 population, 변경, expected outcome, MDE, 시간 범위가 포함되어 있다
  • randomization unit이 제품 상태 경계와 metric unique 단위에 맞는다
  • assignment와 actual exposure를 분리해 기록한다
  • invite_modal_opened -> invite_created는 flow_id, created -> accepted는 invite_id로 연결하고 modal_view_id로 노출 중복을 제거한다
  • invite_modal_closed는 동일 flow_id와 modal_view_id, closed_at을 가지며 유실 시 open + 30분 fallback을 적용한다
  • invite_creation_rate는 min(close, open + 30분)의 동일 flow_id window를 사용한다
  • workspace_created, invite_created, invite_accepted는 commit/outbox에서 생성해 retry에도 불변인 필수 domain_event_id를 가진다
  • invite_creation_rate, 7-day accepted/modal outcome, 새 workspace 7-day activation_rate를 구분한다
  • primary, secondary, guardrail, data quality metric의 역할과 threshold가 사전 정의되어 있다
  • SRM, multiple exposure, missing assignment, impossible sequence를 확인할 수 있다
  • baseline, MDE, power, randomization unit 기준으로 필요한 표본을 계획했다
  • fixed horizon 뒤 미결과를 사후 연장하지 않고 inconclusive로 종료하거나 새 실험을 설계한다
  • feature flag가 deploy와 release를 분리하고 안전한 default와 kill switch를 가진다
  • rollout 단계마다 metric check와 rollback owner가 있다
  • 실험 종료 후 flag와 dead code의 cleanup 조건이 있다

실험과 flag는 제품 상태 모델과 강하게 연결된다. 권한, plan, workspace, subscription의 의미가 명확해야 배정과 노출 조건이 재현 가능하다.

  • Product Domain Modeling: flag와 experiment가 참조하는 account, workspace, role 상태와 Invitation -> Membership 전이를 모델링한다.
  • Billing, Subscription & Entitlement: plan과 entitlement 기반 feature exposure를 설계하고 permission flag와 영구 권한 규칙을 구분한다.
  • Frontend Product Quality & RUM: rollout 중 실제 사용자 환경의 성능·오류·접근성 guardrail을 확인한다.
  • A/B testing
  • Causal inference
  • Counterfactual
  • Randomization unit
  • Intention-to-Treat
  • Exposure logging
  • Guardrail metric
  • Sample Ratio Mismatch
  • Multiple exposure
  • Statistical power
  • Minimum Detectable Effect
  • Feature flag
  • Staged rollout
  • Kill switch
  • Flag debt