이커머스 A/B 테스트 플랫폼 설계: 제품팀이 직접 실험을 돌리는 Feature Flag 아키텍처

커머스

A/B 테스트Feature Flag실험 플랫폼그로스이커머스 최적화

이 글은 누구를 위한 것인가

  • 데이터 기반 의사결정 문화를 만들고 싶은 이커머스 PM과 그로스팀
  • LaunchDarkly 같은 외부 도구 없이 자체 A/B 테스트 시스템을 구축하려는 개발자
  • 실험을 빠르게 돌리되 잘못된 결론을 내리지 않으려는 팀

들어가며

아마존은 매년 수만 건의 A/B 테스트를 돌린다. 넷플릭스의 제품 변경 중 90%는 A/B 테스트를 거친다. 이들이 빠르게 성장할 수 있었던 이유 중 하나는 "데이터 없이는 출시하지 않는다"는 실험 문화다.

문제는 실험 인프라가 없으면 A/B 테스트가 어렵다는 것이다. LaunchDarkly나 Optimizely 같은 SaaS 도구는 월 수백만 원에 달하고, 직접 구축하면 개발 공수가 크다. 하지만 이커머스 규모가 커질수록 실험 플랫폼 투자는 반드시 회수된다.

이 글에서는 자체 A/B 테스트·Feature Flag 시스템을 최소 비용으로 구축하는 방법을 설명한다.

이 글은 bluefoxdev.kr의 그로스 해킹 실험 설계 를 참고하고, 이커머스 플랫폼 설계 관점에서 확장하여 작성했습니다.


1. Feature Flag 시스템 설계

1.1 핵심 데이터 모델

interface FeatureFlag {
  id: string;
  key: string;              // 코드에서 사용하는 키 (예: 'checkout_v2')
  type: 'boolean' | 'multivariate';
  status: 'active' | 'inactive' | 'archived';
  rollout: RolloutConfig;
  targeting: TargetingRule[];
  createdBy: string;
  experiment?: ExperimentConfig;
}

interface RolloutConfig {
  type: 'percentage' | 'full' | 'off';
  percentage?: number;      // 0~100
  hashKey?: string;         // 일관된 버킷팅을 위한 해시 키 (보통 userId)
}

interface TargetingRule {
  attribute: string;        // 'userId', 'email', 'segment', 'region'
  operator: 'in' | 'not_in' | 'matches';
  values: string[];
}

interface ExperimentConfig {
  name: string;
  hypothesis: string;
  primaryMetric: string;
  secondaryMetrics: string[];
  startDate: Date;
  endDate?: Date;
  minimumSampleSize: number;
}

1.2 일관된 버킷팅 구현

같은 사용자는 항상 같은 버전을 보아야 한다. Murmur hash를 사용하면 일관된 버킷팅이 가능하다.

import { createHash } from 'crypto';

function getBucket(userId: string, flagKey: string): number {
  const hash = createHash('md5')
    .update(`${userId}:${flagKey}`)
    .digest('hex');
  
  // 상위 8자리 16진수를 0~99 범위로 변환
  const numericHash = parseInt(hash.substring(0, 8), 16);
  return numericHash % 100;
}

function isFeatureEnabled(
  flag: FeatureFlag,
  userId: string,
  userAttributes: Record<string, string>
): boolean {
  if (flag.status === 'inactive') return false;
  
  // 타게팅 규칙 우선 확인
  for (const rule of flag.targeting) {
    if (matchesRule(rule, userAttributes)) {
      return true; // 타게팅 규칙 매칭 시 무조건 활성화
    }
  }
  
  // 퍼센트 롤아웃
  if (flag.rollout.type === 'percentage') {
    const bucket = getBucket(userId, flag.key);
    return bucket < (flag.rollout.percentage ?? 0);
  }
  
  return flag.rollout.type === 'full';
}

2. A/B 테스트 실험 설계

2.1 실험 생명주기

가설 수립 → 샘플 크기 계산 → 실험 설계 → 코드 구현
     ↓                                          ↓
결론 도출 ← 통계 분석 ← 데이터 수집 ← 실험 실행

2.2 필요 샘플 크기 계산

from scipy import stats
import numpy as np

def calculate_sample_size(
    baseline_rate: float,    # 현재 전환율 (예: 0.03 = 3%)
    minimum_detectable_effect: float,  # 최소 감지 효과 (예: 0.005 = 0.5%p)
    alpha: float = 0.05,     # 유의 수준 (1종 오류)
    power: float = 0.80      # 검정력 (1 - 2종 오류)
) -> int:
    """
    각 그룹(A, B)별 필요 샘플 수 반환
    """
    p1 = baseline_rate
    p2 = baseline_rate + minimum_detectable_effect
    
    z_alpha = stats.norm.ppf(1 - alpha / 2)  # 양측 검정
    z_beta = stats.norm.ppf(power)
    
    p_bar = (p1 + p2) / 2
    
    n = (z_alpha * np.sqrt(2 * p_bar * (1 - p_bar)) + 
         z_beta * np.sqrt(p1 * (1 - p1) + p2 * (1 - p2))) ** 2 / \
        (p2 - p1) ** 2
    
    return int(np.ceil(n))

# 예시: 현재 전환율 3%, 0.5%p 개선 감지
n = calculate_sample_size(0.03, 0.005)
print(f"그룹당 필요 샘플: {n:,}명")  # 약 4,700명

2.3 통계적 유의성 검정

def analyze_experiment(
    control_conversions: int,
    control_visitors: int,
    treatment_conversions: int,
    treatment_visitors: int,
    alpha: float = 0.05
) -> dict:
    
    p_control = control_conversions / control_visitors
    p_treatment = treatment_conversions / treatment_visitors
    
    # Z-test (대규모 샘플)
    p_pool = (control_conversions + treatment_conversions) / \
             (control_visitors + treatment_visitors)
    
    se = np.sqrt(p_pool * (1 - p_pool) * 
                 (1/control_visitors + 1/treatment_visitors))
    z_score = (p_treatment - p_control) / se
    p_value = 2 * (1 - stats.norm.cdf(abs(z_score)))
    
    relative_lift = (p_treatment - p_control) / p_control * 100
    
    return {
        'control_rate': f"{p_control:.2%}",
        'treatment_rate': f"{p_treatment:.2%}",
        'relative_lift': f"{relative_lift:+.1f}%",
        'p_value': round(p_value, 4),
        'is_significant': p_value < alpha,
        'confidence': f"{(1 - p_value) * 100:.1f}%"
    }

3. 이커머스 실험 설계 베스트 프랙티스

3.1 동시 실험 충돌 방지

여러 실험이 동시에 돌아갈 때 상호작용 효과(interaction effect)가 발생할 수 있다. 독립적인 실험은 **직교 분할(orthogonal split)**로 충돌을 방지한다.

3.2 이커머스에서 주의할 편향

편향 유형설명방지 방법
노벨티 효과새 버전 초반에 참여율 높음2주 이상 실험
계절성 편향성수기/비성수기 차이동일 기간 비교
프라이밍 효과이전 실험 경험 영향쿨다운 기간
SRM (표본 비율 불일치)버킷팅 오류SRM 테스트 선행

마무리: 핵심 정리

자체 실험 플랫폼 구축의 핵심은 세 가지다.

  1. 일관된 버킷팅: 같은 사용자는 항상 같은 변형을 보도록 해시 기반 버킷팅
  2. 통계적 엄밀함: 샘플 크기 사전 계산, p-value < 0.05 기준 준수
  3. 실험 문화: 개발자와 PM이 쉽게 실험을 만들고 결과를 볼 수 있는 UI

실험 플랫폼은 한 번 구축하면 수년간 팀의 의사결정 품질을 높인다.