본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 1. · 15 Views
무작위 배정과 실험군 분할 완벽 가이드
A/B 테스트와 실험 설계의 핵심인 무작위 배정(Randomization)과 실험군 분할 기법을 초급 개발자도 이해할 수 있도록 쉽게 설명합니다. 데이터 과학에서 가장 중요한 실험 설계의 기초를 다룹니다.
목차
1. 무작위 배정의 기초
어느 날 김개발 씨는 데이터 분석팀 회의에 참석했습니다. 팀장이 "새로운 버튼 색상이 정말 효과가 있는지 A/B 테스트를 진행해봅시다"라고 말했는데, 김개발 씨는 문득 궁금해졌습니다.
도대체 사용자들을 어떻게 나눠야 공정한 실험이 될까요?
**무작위 배정(Randomization)**은 한마디로 실험 대상을 동전 던지기처럼 우연에 맡겨 그룹에 배치하는 것입니다. 마치 제비뽑기로 청소 당번을 정하는 것과 같습니다.
이렇게 하면 두 그룹이 비슷한 특성을 갖게 되어 실험 결과를 신뢰할 수 있게 됩니다.
다음 코드를 살펴봅시다.
import random
def simple_randomization(user_id):
# 핵심 포인트: 각 사용자를 50% 확률로 배정합니다
random.seed(user_id) # 같은 사용자는 항상 같은 그룹
# 0 또는 1을 무작위로 선택합니다
assignment = random.randint(0, 1)
# 0이면 대조군(A), 1이면 실험군(B)
if assignment == 0:
return "control" # 기존 버전
else:
return "treatment" # 새로운 버전
# 사용 예시
user_group = simple_randomization(12345)
print(f"사용자 12345는 {user_group} 그룹입니다")
김개발 씨는 입사 6개월 차 주니어 데이터 분석가입니다. 오늘 처음으로 A/B 테스트 설계를 맡게 되었는데, 어디서부터 시작해야 할지 막막합니다.
선배 개발자 박시니어 씨가 다가와 질문합니다. "김개발 씨, 왜 사용자를 무작위로 배정해야 하는지 알아요?" 그렇다면 무작위 배정이란 정확히 무엇일까요?
쉽게 비유하자면, 무작위 배정은 마치 학교에서 반을 편성할 때 제비뽑기를 하는 것과 같습니다. 만약 선생님이 "공부 잘하는 학생은 1반, 못하는 학생은 2반"으로 나누면 공정한 비교가 불가능합니다.
하지만 제비뽑기로 나누면 두 반의 평균 실력이 비슷해지겠죠. 이처럼 무작위 배정도 실험군과 대조군의 특성을 비슷하게 만들어 줍니다.
무작위 배정이 없던 시절에는 어땠을까요? 개발자들은 "가입일이 짝수인 사용자는 A그룹, 홀수인 사용자는 B그룹"과 같은 방식을 사용했습니다.
하지만 이렇게 하면 숨겨진 편향이 생길 수 있습니다. 예를 들어 짝수 날에 가입한 사용자들이 우연히 더 활발한 사용자일 수도 있습니다.
더 큰 문제는 **선택 편향(Selection Bias)**이었습니다. 실험 결과가 좋게 나와도 "정말 우리 기능이 좋아서인가, 아니면 원래 좋은 사용자들만 실험군에 들어가서인가?"라는 의문이 남았습니다.
바로 이런 문제를 해결하기 위해 무작위 배정이 등장했습니다. 무작위 배정을 사용하면 두 그룹이 통계적으로 동등해집니다.
나이, 성별, 활동량 같은 눈에 보이는 특성뿐 아니라, 우리가 미처 파악하지 못한 특성까지도 비슷하게 분포됩니다. 이것이 무작위 배정의 진정한 힘입니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 random.seed(user_id) 부분을 보면 사용자 ID를 시드로 사용합니다.
이렇게 하면 같은 사용자는 항상 같은 그룹에 배정됩니다. 다음으로 **random.randint(0, 1)**에서 0 또는 1을 무작위로 선택합니다.
마지막으로 그 결과에 따라 대조군 또는 실험군을 반환합니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 쇼핑몰에서 "구매 버튼을 빨간색에서 초록색으로 바꾸면 매출이 오를까?"라는 가설을 검증한다고 가정해봅시다. 방문하는 고객을 무작위로 두 그룹으로 나누어 한 그룹에는 빨간 버튼을, 다른 그룹에는 초록 버튼을 보여줍니다.
일주일 후 두 그룹의 구매율을 비교하면 버튼 색상의 효과를 알 수 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 시드를 고정하지 않는 것입니다. 시드 없이 무작위 배정을 하면 같은 사용자가 접속할 때마다 다른 그룹에 배정될 수 있습니다.
이렇게 되면 실험 결과를 신뢰할 수 없게 됩니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 무작위가 중요한 거군요!" 무작위 배정을 제대로 이해하면 편향 없는 실험을 설계할 수 있습니다.
여러분도 오늘 배운 내용을 실제 A/B 테스트에 적용해 보세요.
실전 팁
💡 - 사용자 ID를 시드로 사용하면 일관된 배정이 가능합니다
- 실험 시작 전에 무작위 배정 로직을 충분히 테스트하세요
- 배정 결과를 로그로 남겨 나중에 검증할 수 있게 하세요
2. 층화 무작위 배정
김개발 씨가 첫 번째 A/B 테스트를 진행한 후 이상한 결과를 발견했습니다. 분명히 무작위로 배정했는데, 실험군에 유독 신규 사용자가 많이 몰려있었습니다.
박시니어 씨가 "이럴 때는 층화 무작위 배정을 써야 해요"라고 조언해주었습니다.
**층화 무작위 배정(Stratified Randomization)**은 중요한 특성별로 그룹을 나눈 뒤, 각 그룹 안에서 무작위 배정을 하는 방법입니다. 마치 학년별로 반을 나눈 뒤 각 학년 안에서 제비뽑기를 하는 것과 같습니다.
이렇게 하면 중요한 특성이 두 그룹에 균등하게 분포됩니다.
다음 코드를 살펴봅시다.
import random
from collections import defaultdict
def stratified_randomization(users):
# 핵심 포인트: 특성별로 층을 나눕니다
strata = defaultdict(list)
# 사용자를 특성별로 분류합니다
for user in users:
stratum = user["user_type"] # 신규/기존 사용자
strata[stratum].append(user)
control, treatment = [], []
# 각 층 안에서 무작위 배정합니다
for stratum_name, stratum_users in strata.items():
random.shuffle(stratum_users)
mid = len(stratum_users) // 2
control.extend(stratum_users[:mid])
treatment.extend(stratum_users[mid:])
return control, treatment
김개발 씨는 첫 번째 실험 결과를 분석하다가 당황했습니다. 실험군의 전환율이 10% 낮게 나왔는데, 자세히 보니 실험군에 신규 사용자가 70%나 몰려있었습니다.
신규 사용자는 원래 전환율이 낮으니 당연한 결과였죠. 박시니어 씨가 설명합니다.
"단순 무작위 배정은 표본이 작을 때 이런 불균형이 생길 수 있어요. 이럴 때 층화 무작위 배정을 사용합니다." 그렇다면 층화 무작위 배정이란 정확히 무엇일까요?
쉽게 비유하자면, 남녀 혼성 배구 대회를 생각해보세요. 만약 순수하게 무작위로 팀을 나누면 한 팀에 남자만 몰릴 수 있습니다.
그래서 먼저 남자끼리, 여자끼리 나눈 다음 각각 절반씩 팀에 배정합니다. 이처럼 층화 무작위 배정도 중요한 특성을 먼저 고려한 뒤 무작위 배정을 진행합니다.
층화를 하지 않으면 어떤 문제가 생길까요? 특히 표본 크기가 작을 때 문제가 심각합니다.
동전을 10번 던지면 7:3으로 나올 확률이 꽤 높지만, 10000번 던지면 거의 50:50에 가까워집니다. 마찬가지로 사용자 수가 적은 실험에서는 중요한 특성이 불균형하게 분포될 가능성이 높습니다.
바로 이런 문제를 해결하기 위해 층화 무작위 배정이 등장했습니다. 층화 무작위 배정을 사용하면 핵심 특성의 균형이 보장됩니다.
신규 사용자와 기존 사용자가 각각 실험군과 대조군에 50:50으로 배정됩니다. 결과적으로 두 그룹의 비교 가능성이 높아집니다.
위의 코드를 살펴보겠습니다. 먼저 defaultdict를 사용하여 사용자를 특성별로 분류합니다.
여기서는 신규 사용자와 기존 사용자로 나누었습니다. 그 다음 각 층(stratum) 안에서 사용자 목록을 섞고 절반씩 나눕니다.
이렇게 하면 각 층의 사용자가 실험군과 대조군에 균등하게 배정됩니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 유료 서비스 프로모션 효과를 측정한다고 가정해봅시다. 무료 사용자와 유료 사용자의 반응이 크게 다를 수 있습니다.
이때 사용자 유형으로 층화하면 두 그룹 모두에서 프로모션 효과를 정확하게 측정할 수 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 너무 많은 특성으로 층화하는 것입니다. 특성이 많아지면 각 층의 크기가 작아져서 오히려 통계적 검정력이 떨어집니다.
보통 1-3개의 핵심 특성만 선택하는 것이 좋습니다. 김개발 씨는 두 번째 실험에서 사용자 유형으로 층화 무작위 배정을 적용했습니다.
이번에는 두 그룹의 신규/기존 사용자 비율이 거의 동일했고, 훨씬 신뢰할 수 있는 결과를 얻을 수 있었습니다.
실전 팁
💡 - 결과에 큰 영향을 미치는 1-3개 특성만 층화에 사용하세요
- 각 층의 크기가 너무 작아지지 않도록 주의하세요
- 층화 기준은 실험 시작 전에 미리 정해야 합니다
3. 블록 무작위 배정
김개발 씨가 실험을 진행하던 중 이상한 현상을 발견했습니다. 처음 100명은 대부분 대조군에 배정되고, 나중 100명은 대부분 실험군에 배정된 것입니다.
시간에 따라 사용자 특성이 다를 수 있어서 이건 큰 문제였습니다.
**블록 무작위 배정(Block Randomization)**은 일정한 블록 단위로 실험군과 대조군의 비율을 맞추는 방법입니다. 마치 카드 한 벌을 섞을 때 빨간 카드와 검은 카드가 번갈아 나오도록 하는 것과 같습니다.
이렇게 하면 실험 중 어느 시점에서도 두 그룹의 크기가 비슷하게 유지됩니다.
다음 코드를 살펴봅시다.
import random
def block_randomization(block_size=4):
# 핵심 포인트: 블록 안에서 균등하게 배정합니다
assignments = []
# 여러 블록을 생성합니다
for _ in range(10): # 10개 블록 = 40명 배정
# 한 블록: 절반은 대조군, 절반은 실험군
block = ["control"] * (block_size // 2)
block += ["treatment"] * (block_size // 2)
# 블록 안에서 순서를 섞습니다
random.shuffle(block)
assignments.extend(block)
return assignments
# 사용 예시: 4명 단위로 2:2 비율 보장
result = block_randomization(block_size=4)
print(f"처음 8명: {result[:8]}")
print(f"대조군: {result.count('control')}, 실험군: {result.count('treatment')}")
김개발 씨는 실험 데이터를 시간 순으로 정렬해보고 깜짝 놀랐습니다. 오전에 접속한 사용자 100명 중 70명이 대조군이었고, 오후 사용자 100명 중에서는 70명이 실험군이었습니다.
무작위 배정이 맞긴 한데, 뭔가 이상했습니다. 박시니어 씨가 문제점을 짚어줍니다.
"오전 사용자와 오후 사용자의 특성이 다를 수 있어요. 이런 시간적 편향을 막으려면 블록 무작위 배정을 써야 합니다." 그렇다면 블록 무작위 배정이란 정확히 무엇일까요?
쉽게 비유하자면, 젠가 게임을 생각해보세요. 젠가 블록을 쌓을 때 한 층에 3개씩 올리고 다음 층은 방향을 바꿔서 올립니다.
이렇게 하면 탑 전체가 균형을 이룹니다. 블록 무작위 배정도 마찬가지로 일정한 단위(블록) 안에서 균형을 맞추고, 이를 반복합니다.
왜 단순 무작위로는 부족할까요? 단순 무작위 배정은 전체적으로는 균형을 이루지만, 부분적으로는 불균형이 생길 수 있습니다.
앞서 예를 든 것처럼 처음 100명과 나중 100명의 분포가 크게 다를 수 있습니다. 실험을 중간에 중단해야 할 때 이런 불균형은 치명적입니다.
바로 이런 문제를 해결하기 위해 블록 무작위 배정이 등장했습니다. 블록 무작위 배정을 사용하면 어느 시점에서 끊어도 균형이 유지됩니다.
블록 크기가 4라면 4명마다 정확히 2:2 비율이 됩니다. 실험 중간에 분석해도 두 그룹의 크기가 비슷합니다.
위의 코드를 살펴보겠습니다. 먼저 블록 하나를 만듭니다.
블록 크기가 4라면 control 2개, treatment 2개로 구성합니다. 그 다음 이 블록 안의 순서를 섞습니다.
이 과정을 반복하면 전체 배정 목록이 완성됩니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 신규 가입 흐름 개선 실험을 한다고 가정해봅시다. 월요일에 가입하는 사용자와 주말에 가입하는 사용자의 특성이 다를 수 있습니다.
블록 무작위 배정을 사용하면 매일 그룹 비율이 균등하게 유지되어 시간에 따른 편향을 방지할 수 있습니다. 하지만 주의할 점도 있습니다.
블록 크기가 너무 작으면 배정 패턴을 예측할 수 있게 됩니다. 예를 들어 블록 크기가 2면 control 다음에는 반드시 treatment가 옵니다.
이를 방지하려면 블록 크기를 다양하게 섞거나 충분히 크게 설정해야 합니다. 김개발 씨는 세 번째 실험에서 블록 크기 6을 사용했습니다.
이번에는 어느 시점에서 데이터를 확인해도 두 그룹의 크기가 거의 동일했습니다.
실전 팁
💡 - 블록 크기는 보통 4, 6, 8 중에서 선택합니다
- 예측을 방지하려면 블록 크기를 무작위로 섞어 사용하세요
- 블록 크기는 실험 참여자에게 공개하지 마세요
4. 해시 기반 배정
김개발 씨는 실험이 점점 복잡해지면서 새로운 문제에 직면했습니다. 서버가 여러 대인데, 각 서버에서 같은 사용자를 항상 같은 그룹에 배정해야 합니다.
random 함수만으로는 서버 간 일관성을 보장하기 어려웠습니다.
**해시 기반 배정(Hash-based Assignment)**은 사용자 ID를 해시 함수에 통과시켜 결정론적으로 그룹을 배정하는 방법입니다. 마치 주민등록번호 끝자리로 마스크 구매 요일을 정하는 것과 같습니다.
어느 서버에서 계산해도 같은 결과가 나오므로 분산 환경에서 유용합니다.
다음 코드를 살펴봅시다.
import hashlib
def hash_based_assignment(user_id, experiment_name, num_groups=2):
# 핵심 포인트: 해시 값으로 결정론적 배정을 합니다
key = f"{experiment_name}:{user_id}"
# MD5 해시를 계산합니다
hash_value = hashlib.md5(key.encode()).hexdigest()
# 해시의 처음 8자리를 숫자로 변환합니다
hash_int = int(hash_value[:8], 16)
# 그룹 번호를 계산합니다
group = hash_int % num_groups
return "control" if group == 0 else "treatment"
# 어느 서버에서 실행해도 같은 결과
print(hash_based_assignment("user_123", "button_color_test"))
print(hash_based_assignment("user_123", "button_color_test")) # 동일
김개발 씨의 서비스는 이제 서버가 10대로 늘어났습니다. 사용자가 접속할 때마다 다른 서버에 연결될 수 있는데, 모든 서버에서 같은 사용자를 같은 실험 그룹에 배정해야 합니다.
박시니어 씨가 해결책을 제시합니다. "데이터베이스에 저장하는 방법도 있지만, 해시 기반 배정이 더 간단하고 빨라요." 그렇다면 해시 기반 배정이란 정확히 무엇일까요?
쉽게 비유하자면, 대학교 수강신청 시간을 학번으로 정하는 것과 같습니다. "학번 끝자리가 1-2인 학생은 9시, 3-4인 학생은 10시..." 이런 규칙은 어떤 컴퓨터에서 확인해도 같은 결과가 나옵니다.
해시 기반 배정도 사용자 ID에서 일관된 규칙으로 그룹을 계산합니다. 왜 random 함수로는 안 될까요?
random 함수는 상태를 가집니다. 같은 시드를 설정해도 호출 순서에 따라 결과가 달라질 수 있습니다.
또한 서버마다 random의 상태가 다르면 같은 사용자도 다른 그룹에 배정될 수 있습니다. 이런 불일치는 실험 결과를 완전히 망칠 수 있습니다.
바로 이런 문제를 해결하기 위해 해시 기반 배정이 등장했습니다. 해시 기반 배정을 사용하면 입력이 같으면 출력도 항상 같습니다.
user_123이라는 ID를 넣으면 서울 서버에서든 미국 서버에서든 항상 같은 그룹이 나옵니다. 이것을 **결정론적(deterministic)**이라고 합니다.
위의 코드를 살펴보겠습니다. 먼저 실험 이름과 사용자 ID를 결합하여 고유한 키를 만듭니다.
실험마다 다른 배정을 하기 위해 실험 이름을 포함합니다. 그 다음 MD5 해시를 계산하고, 그 결과를 숫자로 변환합니다.
마지막으로 그룹 수로 나눈 나머지를 그룹 번호로 사용합니다. 실제 현업에서는 어떻게 활용할까요?
넷플릭스, 구글, 페이스북 같은 대규모 서비스들이 이 방식을 사용합니다. 수억 명의 사용자를 데이터베이스에 저장하지 않고도 실시간으로 일관된 배정을 할 수 있기 때문입니다.
또한 새로운 실험을 시작할 때 기존 배정에 영향을 주지 않습니다. 하지만 주의할 점도 있습니다.
해시 함수가 균등하게 분포하는지 반드시 검증해야 합니다. 일부 해시 함수는 특정 패턴의 입력에서 편향된 결과를 낼 수 있습니다.
또한 실험 이름을 포함하지 않으면 모든 실험에서 같은 사용자가 같은 그룹에 배정되어 캐리오버 효과가 생길 수 있습니다. 김개발 씨는 해시 기반 배정을 도입한 후 서버 확장에도 실험이 안정적으로 운영되었습니다.
사용자들은 어느 서버에 접속해도 일관된 경험을 할 수 있게 되었습니다.
실전 팁
💡 - 실험 이름을 반드시 키에 포함하여 실험 간 독립성을 보장하세요
- MD5 대신 더 빠른 xxHash나 MurmurHash를 고려해보세요
- 배정 분포가 균등한지 정기적으로 모니터링하세요
5. 트래픽 분할과 점진적 배포
드디어 김개발 씨의 새 기능이 완성되었습니다. 그런데 바로 50%의 사용자에게 보여주기에는 불안합니다.
"처음에는 1%만 실험군으로 하고, 문제가 없으면 점점 늘려가면 안 될까요?" 이것이 바로 점진적 배포입니다.
**트래픽 분할(Traffic Splitting)**은 전체 사용자를 여러 실험 그룹으로 나누고, 각 그룹의 비율을 조절하는 방법입니다. 마치 수도꼭지를 조금씩 열어서 물의 양을 조절하는 것과 같습니다.
점진적 배포를 통해 위험을 최소화하면서 새 기능을 검증할 수 있습니다.
다음 코드를 살펴봅시다.
import hashlib
def traffic_split(user_id, experiment_config):
# 핵심 포인트: 트래픽 비율에 따라 그룹을 배정합니다
hash_value = hashlib.md5(user_id.encode()).hexdigest()
bucket = int(hash_value[:8], 16) % 100 # 0-99 버킷
# 설정 예: {"control": 95, "treatment": 5}
cumulative = 0
for group, percentage in experiment_config.items():
cumulative += percentage
if bucket < cumulative:
return group
return "control" # 기본값
# 1% 실험 시작
config_1pct = {"control": 99, "treatment": 1}
print(traffic_split("user_001", config_1pct))
# 10%로 확대
config_10pct = {"control": 90, "treatment": 10}
print(traffic_split("user_001", config_10pct))
김개발 씨는 새 결제 시스템을 개발했습니다. 테스트는 모두 통과했지만, 실제 사용자에게 바로 적용하기에는 걱정이 됩니다.
버그가 있으면 매출에 직접적인 영향을 미칠 테니까요. 박시니어 씨가 안심시켜 줍니다.
"처음에는 1%만 실험군으로 시작하고, 문제가 없으면 5%, 10%, 50%, 100%로 점진적으로 늘려가면 돼요." 그렇다면 트래픽 분할이란 정확히 무엇일까요? 쉽게 비유하자면, 새로 만든 요리를 손님에게 내놓기 전에 먼저 직원들에게 맛보게 하는 것과 같습니다.
직원들 반응이 좋으면 단골손님 몇 명에게 추천하고, 그다음에는 전체 메뉴판에 올립니다. 이렇게 단계적으로 확대하면 실패했을 때 피해를 최소화할 수 있습니다.
왜 바로 50:50으로 실험하면 안 될까요? 새 기능에 치명적인 버그가 있다면 50%의 사용자가 피해를 입습니다.
또한 서버 성능 문제가 발생하면 절반의 트래픽에 영향을 미칩니다. 1%로 시작하면 문제가 생겨도 빠르게 롤백할 수 있고, 영향받는 사용자도 최소화됩니다.
바로 이런 위험 관리를 위해 점진적 배포가 필요합니다. 트래픽 분할을 사용하면 실시간으로 비율을 조절할 수 있습니다.
문제가 발견되면 즉시 0%로 줄이고, 안정적이면 점점 늘려갑니다. 이 과정을 **카나리 배포(Canary Deployment)**라고도 합니다.
위의 코드를 살펴보겠습니다. 먼저 해시 값을 0-99 사이의 버킷 번호로 변환합니다.
그 다음 설정된 비율에 따라 그룹을 결정합니다. control: 99, treatment: 1이면 버킷 0-98은 대조군, 99만 실험군이 됩니다.
비율을 바꾸면 같은 사용자도 다른 그룹에 배정될 수 있다는 점에 주의해야 합니다. 실제 현업에서는 어떻게 활용할까요?
대부분의 IT 기업에서 새 기능을 출시할 때 이 방식을 사용합니다. 먼저 내부 직원에게만 공개하고, 그다음 1% 사용자, 5% 사용자 순으로 확대합니다.
각 단계에서 에러율, 성능, 사용자 반응을 모니터링하고 문제가 없으면 다음 단계로 진행합니다. 하지만 주의할 점도 있습니다.
비율을 바꿀 때 기존 실험군 사용자가 대조군으로 이동할 수 있습니다. 이런 상황이 문제가 된다면 "한 번 실험군이면 계속 실험군"이 되도록 로직을 수정해야 합니다.
또한 1%로는 통계적 유의성을 확보하기 어려우므로 탐색적 분석에만 활용해야 합니다. 김개발 씨의 새 결제 시스템은 1%로 시작해서 2주에 걸쳐 100%까지 확대되었습니다.
중간에 발견된 작은 버그들도 큰 피해 없이 수정할 수 있었습니다.
실전 팁
💡 - 첫 단계는 충분히 작게(0.1-1%) 시작하세요
- 각 단계에서 최소 24-48시간은 모니터링하세요
- 롤백 절차를 미리 준비해두고 실험을 시작하세요
6. 다중 실험 관리
몇 달이 지나자 김개발 씨의 회사에서는 동시에 10개 이상의 A/B 테스트가 진행되고 있었습니다. 그런데 어느 날 이상한 현상이 발견되었습니다.
실험 A의 결과가 실험 B를 켜고 끄는 것에 따라 달라지는 것입니다. 이것이 바로 실험 간 간섭 문제입니다.
**다중 실험 관리(Multiple Experiment Management)**는 여러 실험을 동시에 운영할 때 서로 간섭하지 않도록 설계하는 방법입니다. 마치 여러 과학자가 같은 실험실을 공유하면서도 서로의 실험에 영향을 주지 않도록 구역을 나누는 것과 같습니다.
다음 코드를 살펴봅시다.
import hashlib
class ExperimentManager:
def __init__(self):
# 핵심 포인트: 실험별로 독립적인 레이어를 사용합니다
self.layers = {
"ui_layer": ["button_color", "font_size"],
"algorithm_layer": ["search_ranking", "recommendation"],
"pricing_layer": ["discount_rate"]
}
def get_assignment(self, user_id, experiment_name):
# 실험이 속한 레이어 찾기
layer = self._find_layer(experiment_name)
# 레이어 + 사용자 ID로 해시 계산
key = f"{layer}:{user_id}"
hash_int = int(hashlib.md5(key.encode()).hexdigest()[:8], 16)
return "control" if hash_int % 2 == 0 else "treatment"
def _find_layer(self, experiment_name):
for layer, experiments in self.layers.items():
if experiment_name in experiments:
return layer
return "default_layer"
김개발 씨는 버튼 색상 실험과 폰트 크기 실험을 동시에 진행했습니다. 그런데 버튼 색상 실험의 결과가 이상했습니다.
폰트 크기 실험을 끄면 결과가 바뀌었습니다. 두 실험이 서로 간섭하고 있었던 것입니다.
박시니어 씨가 원인을 설명합니다. "두 실험이 같은 해시 키를 사용하고 있어서 그래요.
같은 사용자가 두 실험에서 항상 같은 그룹에 배정되고 있었습니다." 그렇다면 어떻게 실험 간 간섭을 막을 수 있을까요? 쉽게 비유하자면, 아파트의 층별 구조를 생각해보세요.
1층에서 아무리 시끄럽게 해도 10층에는 영향이 없습니다. 마찬가지로 실험을 **레이어(층)**로 분리하면 서로 간섭하지 않습니다.
왜 같은 레이어에 있으면 문제가 될까요? 같은 레이어의 실험들은 같은 트래픽을 나눠 씁니다.
버튼 색상 실험군 50%와 폰트 크기 실험군 50%가 같은 사용자일 수도, 완전히 다른 사용자일 수도 있습니다. 이런 불확실성이 결과 해석을 어렵게 만듭니다.
바로 이런 문제를 해결하기 위해 레이어 구조가 필요합니다. 레이어를 사용하면 같은 레이어의 실험들은 상호 배타적입니다.
UI 레이어에서 버튼 색상 실험군인 사용자는 폰트 크기 실험에 참여하지 않습니다. 반면 다른 레이어의 실험들은 독립적입니다.
UI 레이어와 알고리즘 레이어의 실험은 서로 영향을 주지 않습니다. 위의 코드를 살펴보겠습니다.
먼저 실험들을 레이어로 분류합니다. UI 관련 실험은 ui_layer에, 추천 알고리즘 관련 실험은 algorithm_layer에 배치합니다.
해시를 계산할 때 레이어 이름을 키에 포함합니다. 이렇게 하면 다른 레이어의 실험은 완전히 독립적인 배정을 받습니다.
실제 현업에서는 어떻게 활용할까요? 구글의 Overlapping Experiment Infrastructure가 대표적인 예입니다.
수천 개의 실험을 동시에 운영하면서도 서로 간섭하지 않도록 정교한 레이어 구조를 사용합니다. 넷플릭스, 링크드인 등도 비슷한 시스템을 운영합니다.
하지만 주의할 점도 있습니다. 어떤 실험을 어떤 레이어에 넣을지 신중하게 결정해야 합니다.
서로 영향을 줄 수 있는 실험은 같은 레이어에 넣어 상호 배타적으로 만들어야 합니다. 반면 독립적인 실험을 같은 레이어에 넣으면 트래픽이 낭비됩니다.
김개발 씨는 실험 관리 시스템에 레이어 구조를 도입했습니다. 이제 10개의 실험이 동시에 진행되어도 서로 간섭하지 않고, 각 실험의 결과를 신뢰할 수 있게 되었습니다.
실전 팁
💡 - 서로 영향을 줄 수 있는 실험은 같은 레이어에 배치하세요
- 레이어가 너무 많으면 관리가 복잡해지니 적정 수준을 유지하세요
- 레이어 구조와 실험 배치를 문서화해두세요
7. 통계적 검정력과 표본 크기
김개발 씨는 일주일간 실험을 진행했지만 결과가 "통계적으로 유의하지 않다"고 나왔습니다. 실험군의 전환율이 5% 더 높았는데도 말입니다.
박시니어 씨가 "표본 크기가 너무 작았어요"라고 설명해주었습니다. 과연 얼마나 많은 사용자가 필요한 걸까요?
**통계적 검정력(Statistical Power)**은 실제로 효과가 있을 때 그것을 탐지할 확률입니다. 표본 크기가 작으면 진짜 효과가 있어도 발견하지 못합니다.
마치 어두운 방에서 작은 물건을 찾을 때 손전등이 밝을수록 찾기 쉬운 것과 같습니다. 실험 전에 필요한 표본 크기를 계산해야 합니다.
다음 코드를 살펴봅시다.
import math
def calculate_sample_size(baseline_rate, mde, power=0.8, alpha=0.05):
# 핵심 포인트: 필요한 표본 크기를 계산합니다
# baseline_rate: 기존 전환율 (예: 0.10 = 10%)
# mde: 최소 탐지 효과 크기 (예: 0.01 = 1%p 증가)
# Z 점수 계산
z_alpha = 1.96 # 95% 신뢰수준
z_beta = 0.84 # 80% 검정력
# 표본 크기 공식
p1 = baseline_rate
p2 = baseline_rate + mde
pooled_p = (p1 + p2) / 2
numerator = 2 * pooled_p * (1 - pooled_p) * (z_alpha + z_beta) ** 2
denominator = (p2 - p1) ** 2
n_per_group = math.ceil(numerator / denominator)
return n_per_group
# 기존 전환율 10%, 1%p 향상을 감지하려면?
sample_size = calculate_sample_size(0.10, 0.01)
print(f"그룹당 필요 표본: {sample_size:,}명")
print(f"총 필요 표본: {sample_size * 2:,}명")
김개발 씨는 실험 결과를 보고 혼란스러웠습니다. 실험군의 전환율이 10.5%로 대조군의 10%보다 높았습니다.
0.5%p나 향상되었는데 왜 "유의하지 않다"고 나오는 걸까요? 박시니어 씨가 설명합니다.
"표본 크기가 각 그룹에 500명밖에 안 됐어요. 이 정도로는 0.5%p 차이를 확신할 수 없어요." 그렇다면 통계적 검정력이란 정확히 무엇일까요?
쉽게 비유하자면, 금속 탐지기를 생각해보세요. 민감도가 낮은 탐지기로는 작은 금속 조각을 발견하지 못합니다.
통계적 검정력도 마찬가지로 작은 효과를 탐지할 수 있는 능력입니다. 검정력이 80%라면, 실제로 효과가 있을 때 80% 확률로 이를 발견한다는 의미입니다.
왜 표본 크기가 중요할까요? 작은 표본에서는 우연에 의한 변동이 큽니다.
동전을 10번 던지면 7:3이 나올 수 있지만, 10000번 던지면 거의 50:50에 가깝습니다. 실험도 마찬가지로 표본이 작으면 우연히 나타난 차이인지 진짜 효과인지 구분하기 어렵습니다.
바로 이런 이유로 실험 전에 표본 크기를 계산해야 합니다. 표본 크기 계산에는 네 가지 요소가 필요합니다.
첫째, 기존 전환율(baseline rate)입니다. 둘째, 최소 탐지 효과 크기(MDE, Minimum Detectable Effect)입니다.
셋째, 검정력(보통 80%)입니다. 넷째, 유의수준(보통 5%)입니다.
위의 코드를 살펴보겠습니다. 먼저 Z 점수를 설정합니다.
z_alpha=1.96은 95% 신뢰수준, z_beta=0.84는 80% 검정력에 해당합니다. 그 다음 공식에 따라 필요한 표본 크기를 계산합니다.
기존 전환율 10%에서 1%p 향상을 감지하려면 그룹당 약 15,000명이 필요합니다. 실제 현업에서는 어떻게 활용할까요?
실험을 시작하기 전에 반드시 표본 크기를 계산해야 합니다. "일주일 동안 실험하자"가 아니라 "15,000명이 모일 때까지 실험하자"가 올바른 접근입니다.
트래픽이 부족하면 실험 기간을 늘리거나 MDE를 높여야 합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 결과를 보면서 실험을 중단하는 것입니다. "어, 유의해졌네?
그만하자"라고 하면 안 됩니다. 이것을 **피킹(peeking)**이라고 하며, 거짓 양성률을 높입니다.
미리 정한 표본 크기에 도달할 때까지 기다려야 합니다. 김개발 씨는 다음 실험부터 먼저 표본 크기를 계산했습니다.
"이번 실험은 2% 향상을 감지하려면 그룹당 8,000명이 필요하니, 약 2주가 걸리겠네요." 명확한 계획으로 더 신뢰할 수 있는 결과를 얻을 수 있게 되었습니다.
실전 팁
💡 - 실험 시작 전에 반드시 필요한 표본 크기를 계산하세요
- MDE가 작을수록 더 많은 표본이 필요합니다
- 검정력 80%, 유의수준 5%가 표준적인 설정입니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.
AWS Organizations 완벽 가이드
여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.
AWS KMS 암호화 완벽 가이드
AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.
AWS Secrets Manager 완벽 가이드
AWS에서 데이터베이스 비밀번호, API 키 등 민감한 정보를 안전하게 관리하는 Secrets Manager의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.