안녕하세요, 하이퍼커넥트에서 data scientist로 근무하는 신준형입니다. Azar와 같은 거대한 규모의 서비스에서는 데이터 기반으로 의사결정을 하는 경우가 많습니다. 이러한 의사결정은 주로 A/B test를 통해 이루어지는데요, 많은 A/B test를 운영하다 보면 다양한 challenge를 마주하게 됩니다.

하이퍼커넥트 데이터 사이언스 팀에서는 이러한 여러 가지 challenge를 해결한 A/B test 운영 시스템 ABzar를 설계 및 개발하여 사내에서 이루어지는 수많은 A/B test를 지원하고 있습니다.

이 글에서는 Azar와 같은 서비스에서는 A/B test를 하는 데 어떤 어려움이 있는지, ABzar를 통해 이러한 어려움들을 어떻게 해결했는지 소개합니다.

Challenges of A/B testing

A/B test가 공정하게 이루어져야 실험 분석이 용이하고, 실험 결과가 설득력이 높아집니다. 공정하고 효율적으로 A/B test를 하기 위해서는 어떤 점들을 신경써야 할까요?

변인 통제

공정한 A/B test가 되기 위해 가장 중요한 것으로, 실험 결과에 영향을 줄 수 있는 통제 변인(control variable)을 조절해야 합니다.

어떤 실험을 할 때는 항상 수많은 변인이 뒤따릅니다. 가령 아래와 같은 실험을 한다고 생각해 봅시다.

  • 이번에 X 앱에 구매 버튼을 새롭게 디자인했다. 새로운 구매 버튼이 유저의 클릭률(CTR)을 더 높일 수 있을까?

간단하게 유저들을 대조군(control group)과 실험군(experiment group)으로 임의로 나누고, 대조군에 새로운 구매 버튼을 도입하여 양쪽의 CTR을 비교해보는 실험을 수행했고, 대조군의 CTR이 약 10% 높게 나왔다고 합시다.

하지만 나중에 알고보니 대조군에 실험군보다 X 앱을 자주 쓰는 헤비 유저가 더 많이 있었다면 어떻게 될까요? 구매 버튼의 차이 때문이 아니라 헤비 유저가 더 많아서 CTR이 높게 나왔을 수도 있기 때문에, 두 유저 그룹의 비교가 공정하지 못하게 됩니다. 결과적으로 실험 결과의 설득력을 떨어뜨리죠.

즉, 실험군과 대조군의 비교가 공정하게 이루어지기 위해서는 결과에 영향을 줄 수 있는 변수들이 양쪽 그룹에서 동등하게 나타나도록 통제해야 합니다.

2개 이상의 실험군

좀더 다양한 형태의 실험을 위해 2개 이상의 실험군으로 실험을 진행할 수 있어야 합니다.

일반적으로 A/B test라고 하면 대조군 A와 실험군 B를 설정해서, B가 A에 비해 어떤 긍정적인 또는 부정적인 효과가 있는지 검정하는 실험을 말합니다. 하지만 아래와 같은 경우를 생각해 봅시다.

  • 이번에 X 앱에 구매 버튼을 새롭게 디자인하고, 구매 화면 배경 색상도 바꿔보았다. 어떤 버튼 - 색상 조합이 CTR을 더 높일 수 있을까?

구매 버튼이 두 종류, 배경 색상이 두 종류라면 기존 형태 유지를 위한 대조군을 제외하면 실험군이 총 3개가 필요합니다. 새로운 구매 버튼의 효과와 새로운 색상의 효과를 따로 따로 A/B test를 해볼 수도 있지만, 두 가지 변화를 조합하면 새로운 현상이 나타날 수도 있기 때문에 별로 바람직하지 않습니다.

구매 버튼이나 색상의 종류가 늘어나면 더 많은 실험군이 필요할 수 있습니다. 따라서 원하는 개수의 실험군을 설정할 수 있어야 합니다.

수많은 실험의 공존

여러 실험이 안정적으로 수행되기 위해서는 유저 풀(user pool)을 효율적이고 효과적으로 관리해야 합니다.

Azar와 같이 수억 명의 유저를 보유한 큰 규모의 글로벌 서비스는 동시에 여러 가지 실험을 수행해야 할 일이 많습니다. 이렇게 여러 실험이 동시에 수행될 때 신경써야 할 점들도 많습니다.

  • 한 유저가 동시에 여러 실험에 배정되는 것은 바람직하지 않습니다. 어떤 실험에서 아무리 변인 통제를 잘해도, 다른 실험과 유저 풀이 겹쳐버려 다른 실험의 영향이 생겨버린다면 변인 통제가 제대로 이루어지지 않습니다. 따라서 하나의 유저는 최대 하나의 실험군에만 속하는 것이 바람직합니다. 하이퍼커넥트 데이터 사이언스 팀에서는 이러한 정책을 1 user - 1 group policy라고 부릅니다.

  • 한 실험이 특정 카테고리의 유저를 너무 많이 차지하는 것은 좋지 않습니다. 특히 Azar와 같은 글로벌 서비스에서 이러한 경우를 자주 만나게 됩니다. 예를 들어 특정 국가에서 진행되는 A/B test가 유저를 너무 많이 점유하면, 1 user - 1 group policy에 의해 해당 실험이 끝나기 전에는 그 국가에서 다른 실험을 할 수 없게 됩니다. 국가뿐 아니라 특정 성별을 가진 유저 풀, 특정 구매력을 가진 유저 풀 등도 모두 마찬가지로, 하나의 실험이 특정 카테고리의 유저 풀을 너무 많이 점유하지 않게 주의해야 합니다.

  • 실험의 시작과 종료를 잘 관리해야 합니다. 실험을 시작했을 때 해당 실험에 할당된 유저는, 실험이 종료되었을 때 해제해 주어야 다른 실험에서 그 유저 풀을 사용할 수 있습니다. 예를 들어 어떤 실험이 X 국가 유저 1000명 중 800명을 사용하고 있다면, X 국가에서 다른 실험을 진행할 때에는 유저를 200명까지밖에 쓸 수 없습니다. 실험이 종료되면 800명의 유저를 다른 실험에서 사용할 수 있도록 풀어주어야 합니다.

이러한 기능들을 모두 갖출 수 있도록 유저 풀 관리 시스템을 설계해야 합니다.

유저들의 변화

A/B test가 안정적으로 이루어지기 위해서는 유저들의 변화에 유연하게 대처할 수 있어야 합니다.

다음과 같은 상황을 생각해 봅시다.

  • 두 개의 실험 A와 B가 있다. 두 실험 모두 X 국가 유저를 대상으로 한다.
  • 동시에 진행되어야 하는 실험이므로, 유저 풀을 반씩 나누어 가지고 있었다.
  • A 실험은 조기 종료되고, B 실험은 재개되어야 한다.

이 경우 A 실험이 종료되어 A에 할당되었던 유저를 B에서 사용할 수 있게 됩니다. 모수는 많을수록 통계적으로 더 유의미해지기 때문에, 사용할 수 있는 유저 풀이 생기면 최대한 활용하는 것이 좋습니다. (앞서 말했던 대로 너무 다 차지해버리면 안되겠지만요)

또 다른 예로는, 최근 30일 내에 앱에 접속한 유저들을 대상으로 하는 실험이 있다고 합시다. 실험을 시작했을 때는 최근 접속 기록이 없어 실험 대상이 아니었던 유저들이, 이후에 앱에 접속을 하면서 실험 대상에 포함될 수 있습니다. 이러한 유저들을 실험에 활용할 수 있으면 더 효과적인 실험을 진행할 수 있습니다.

실험의 규모

많은 수의 유저를 관리하기 위해서는 유저 풀 관리 시스템이 효율적인 알고리즘으로 작성될 수 있어야 합니다.

Azar와 같은 글로벌 서비스에서는 A/B test를 작게는 1만 명 단위, 크게는 100만 명 단위의 큰 규모로 하는 경우가 많습니다. 따라서 유저 풀 관리 알고리즘의 효율성도 무시할 수 없는 중요한 변수입니다.

따라서 유저 풀 관리 시스템은 앞서 언급한 challenge들을 해결하면서 효율적으로 동작해야 합니다.

Azar의 A/B test 운영 시스템 ABzar

이제 하이퍼커넥트에서 사용하는 A/B test 운영 시스템 ABzar에 대해 소개하려고 합니다. ABzar의 핵심은 유저 풀을 관리하는 User Pool Manager (이하 UPM)입니다. 이 글을 통해 UPM의 구조와 동작 방식을 소개하는 것이 목표입니다.

UPM의 임무는 위에서 언급한 여러 가지 A/B test의 challenge를, 가능한 직관적이고 이해하기 쉬운 구조로 해결하는 것입니다. 특히 A/B test가 공정하게 진행되는 데 가장 중요한 변인 통제 문제를 효율적이고 효과적으로 해결하는 것이 중요합니다.

먼저 UPM을 구성하는 중요한 개념인 Cube, Experiment, ExperimentGroup 등에 대하여 이야기해보려고 합니다.

Cube

Cube는 간단히 말해 유저의 카테고리입니다. 유저의 카테고리를 나타내는 지표는 여러 가지가 있을 수 있습니다. 예를 들면 성별, 국가, 나이대, 구매량, 플랫폼(iOS/Android) 등이 있죠. 이러한 지표들 중 중요한 지표들을 고른 뒤 그 지표들을 기준으로 유저들을 묶게 되는데, 각각의 묶음을 Cube라고 부릅니다.

중요한 지표들은 사전 결정된 지표들이며, 이들을 어떻게 선정할 수 있을지는 UPM의 구조를 소개한 뒤에 이야기하려고 합니다.

아주 간단한 예를 들어 봅시다. 아래와 같은 유저들이 있다고 합시다.

번호 성별 나이대 플랫폼
1 20대 iOS
2 30대 Android
3 20대 iOS
4 20대 iOS
5 30대 Android
6 20대 Android
7 40대 Android

Cube를 구성할 지표로 성별, 플랫폼을 선택했다면, 아래와 같이 총 네 개의 Cube가 있게 됩니다.

Cube 유저 목록
남/iOS 3
남/Android 2, 6, 7
여/iOS 1, 4
여/Android 5

Cube를 구성할 지표로 성별과 나이대를 선택했다면, 아래와 같이 총 여섯 개의 Cube가 있게 되겠죠.

Cube 유저 목록
남/20대 3, 6
남/30대 2
남/40대 7
여/20대 1, 4
여/30대 5
여/40대  

앞으로 UPM에서는 유저 풀을 이러한 Cube 단위로 관리하게 됩니다.

Experiment, ExperimentGroup

ABzar는 각각의 실험(A/B test)을 Experiment라는 단위로 관리합니다. Experiment는 다음과 같이 두 가지로 구성되어 있습니다.

  • Cube list - 실험에 사용할 유저 풀을 의미하는 Cube의 목록입니다.
  • ExperimentGroup - 실험에 사용할 유저 그룹입니다. 예를 들어 일반적인 A/B test라면, A 그룹(대조군)과 B 그룹(실험군) 이렇게 두 가지를 사용하게 되겠죠. 3개 이상의 유저 그룹도 설정이 가능합니다. 또한, 실험의 각 유저 그룹이 Cube의 몇 퍼센트를 점유할지도 설정할 수 있습니다.

간단한 예를 들어보겠습니다. Cube는 성별, 플랫폼 두 가지 지표로 구성되었다고 가정하고, 각 Cube 별 유저 수는 아래와 같다고 합시다.

Cube 유저 수
남/iOS 1000
남/Android 1200
여/iOS 800
여/Android 780

만일 iOS 유저 전체의 30%를 대상으로 3가지 UI를 실험해보고 싶다면, 아래와 같이 실험을 설정할 수 있습니다.

  • Cube list - 남/iOS, 여/iOS (전체 1800명)
  • ExperimentGroup - A 그룹, B 그룹, C 그룹 각 10%

이렇게 설정된 실험이 원하는 유저 풀의 상태는 아래와 같습니다.

ExperimentGroup Cube 유저 수
A 그룹 남/iOS 100
여/iOS 80
B 그룹 남/iOS 100
여/iOS 80
C 그룹 남/iOS 100
여/iOS 80

남성과 여성 유저 비율이 모든 유저 그룹에 균등하게 분배된 사실에 주목해 봅시다. 유저를 Cube 단위로 관리하면 이와 같이 Cube를 구성할 때 사용한 지표로 유저를 균등하게 나누기 매우 쉬워집니다. 즉, 위에서 언급한 변인 통제가 용이해집니다.

User Pool Manager의 역할

앞서 언급한 Cube, ExperimentGroup 등의 개념들만으로도 변인 통제, 2개 이상의 실험군 문제는 해결할 수 있을 것 같습니다. 하지만 수많은 실험의 공존 문제, 특히 1 user - 1 group policy를 구현하기 위해서는 별도 관리가 필요합니다. 예를 들어 위 예시에서의 실험이 진행중인 상황에서 iOS 유저 대상으로 다른 실험을 하고 싶다면, 이미 진행중인 실험에 배정된 유저를 새로운 실험에 배정해서는 안되겠죠.

ABzar UPM은 유저 추출 요청이 들어왔을 때만 추출을 진행하는 방식으로 이 문제들을 해결합니다. UPM 알고리즘의 기본 골자는 아래와 같습니다.

  • Experiment는 해당 실험이 원하는 유저 풀의 상태만을 정의합니다. 즉 Experiment를 생성하는 것과 유저 풀을 추출해서 실험에 배정하는 것과는 다릅니다.
  • 어떤 Experiment의 유저 추출 요청이 들어오면, UPM은 해당 Experiment에 정의된 유저 풀의 상태를 최대한 맞출 수 있도록 유저를 추출하되 1 user - 1 group policy에는 위배되지 않도록 합니다.

요약하자면, 실험의 단위인 Experiment는 해당 실험이 원하는 상태 또는 요구 사항만을 기록하며, 유저 추출은 정책을 위반하지 않는 선에서 요구 사항을 최대한 들어줄 수 있도록 하는 것이 UPM의 아이디어입니다.

유저 추출 요청은 최초 1회만 수행될 수도 있고, 주기적으로(예를 들어 매일 또는 하루에 2번) 수행될 수도 있습니다. UPM은 요청이 올 때 처리만 하면 됩니다.

UPM은 매우 단순한 알고리즘으로 이러한 역할을 수행할 수 있습니다. 어떤 Experiment \(E\)의 유저 추출 요청이 들어오면, 다음과 같은 알고리즘을 실행합니다.

  1. Cube는 정적이지 않습니다. 신규 유저가 유입되거나 유저의 지표가 바뀌면, 시간에 따라 각 Cube에 속한 유저 목록이 변경될 수 있습니다. 따라서 먼저 각 유저가 속한 Cube를 다시 계산해야 합니다.
  2. \(E\)에 설정된 각 Cube \(C\)에 대해 다음 하위 알고리즘을 수행합니다.
    1. \(C\)의 전체 유저 수 \(\lvert C\rvert\)에서 \(E\)가 아닌 다른 Experiment들이 \(C\)에서 점유하고 있는 유저 수를 뺀 값 \(a\)를 구합니다. 즉 \(a\)는 \(C\)에서 \(E\)가 사용할 수 있는 전체 유저 수입니다.
    2. \(E\)가 점유하고자 하는 Cube의 유저 수 \(x\)를 계산합니다. 만일 \(E\)의 ExperimentGroup 각각의 비율 총합이 \(r\)이라면, \(x=r\lvert C\rvert\)로 계산할 수 있습니다.
    3. \(E\)에게 제공하고자 하는 유저 수 \(y=\min\lbrace a, x\rbrace\)를 계산합니다. 즉 \(x\)만큼을 제공하고자 하지만, 사용할 수 있는 전체 유저 수 \(a\)보다 많이 제공할 수는 없습니다.
    4. \(E\)가 기존에 \(C\)에서 점유하고 있던 유저 수 \(z\)가 \(y\) 미만이라면 \((y-z)\)만큼의 유저를 추출하고, 이미 \(z\)가 \(y\) 이상이라면 원하는 만큼의 유저를 이미 가지고 있으므로 하위 알고리즘을 종료합니다.
    5. 신규 추출한 유저는 각 ExperimentGroup에서 설정한 비율에 맞게 각 유저 그룹에 배정합니다.

실험의 종료는 더욱 간단한데, 단순히 지금까지 점유하고 있던 유저를 더 이상 점유하지 않겠다고 선언하면 됩니다. 그러면 다른 실험에서 유저를 추출할 때 위 알고리즘의 1번 과정에서 \(a\)를 계산할 때 해당 변화가 반영됩니다.

이 알고리즘은 구현이 단순하면서 1 user - 1 group policy를 쉽게 해결할 수 있습니다. 뿐만 아니라 하위 알고리즘은 각 Cube마다 독립적으로 동작하고, 정의상 서로 다른 Cube는 교집합이 없기 때문에, 분산 처리가 가능하다는 장점이 있습니다. 따라서 UPM은 매우 효율적으로 구현될 수 있고, 실험의 규모 문제까지 해결이 가능합니다.

실제로 하이퍼커넥트에서 사용하고 있는 ABzar는 수십만, 수백만 명의 유저가 포함된 실험을 여럿 운영하면서도 유저 추출 요청 처리 시간이 2분을 넘긴 적이 없습니다.

Cube를 구성할 지표 선정하기

Cube를 구성할 지표들은 UPM을 구축하기 전에 이미 선정되어 있어야 합니다. 모든 실험이 이 Cube를 기준으로 설정되기 때문이죠.

이제 UPM의 역할을 알았으니, Cube를 구성할 지표를 어떻게 선정하면 되는지 이야기해봅시다.

  • 자주 만나게 될 통제 변인들을 선정하면 좋습니다. Cube라는 단위를 구성한 가장 큰 이유 중 하나가 변인 통제의 용이함입니다. 여러 A/B test에서 공통적으로 결과에 영향을 주기 쉬운 변수들, 예를 들면 성별, 국가, 구매력, 나이 등을 선정하면 좋습니다.

  • 쉽게 변하는 지표는 선정하지 않는 것이 좋습니다. 예를 들어 ‘유저의 하루 전 로그인 여부’와 같은 지표는 유저마다 매일매일 바뀌기 쉬운 지표이고, 따라서 유저의 Cube 간 이동이 잦아집니다. 이렇게 되면 UPM의 전체적인 변인 통제의 효과가 떨어집니다. 애초에 이런 지표는 통제하기도 어렵고 통제할 필요도 없는 경우가 많기 때문에, 가급적 선정하지 않는 것이 좋습니다.

  • 선정할 지표의 개수는 적당해야 합니다. 지표 개수가 너무 적으면 변인 통제에 어려움을 겪을 수 있습니다. 반면 지표가 너무 많으면 Cube의 개수가 기하급수적으로 증가하게 되고, 이는 UPM의 효율을 크게 떨어뜨리고 모수 확보를 어렵게 합니다. 따라서 Cube의 개수가 너무 많지도, 적지도 않도록 지표를 선정하는 것이 중요합니다.

UPM을 구축한 이후에도 Cube를 구성할 지표를 변경하는 것이 불가능하지는 않지만, 시간과 노력이 많이 들 수 있기 때문에 가급적 최초 1회 결정 후 변경하지 않는 것이 좋습니다.

ABzar UPM의 동작 예시

예시를 통해 UPM이 어떻게 동작하는지 보는 것으로 글을 마치려고 합니다. 먼저 아래와 같은 상황에서 시작해 봅시다.

Cube 유저 수
남/iOS 1000
남/Android 1200
여/iOS 800
여/Android 780

또한 아래와 같은 Experiment가 있다고 생각합시다. 최대한 단순화하기 위해 ExperimentGroup이 두 개인 실험 두 개만 생각하겠습니다.

Experiment 구분 Cube 목록 ExperimentGroup 구분 점유율
$$E_1$$ 남/iOS, 여/iOS A 20%
B 30%
$$E_2$$ 남/iOS, 남/Android A 30%
B 30%

실험 타임라인은 아래와 같다고 합시다.

  1. \(E_1\), \(E_2\) 실험 설정
  2. \(E_1\) 유저 추출 요청
  3. 모든 Cube 각각 100명씩 신규 유저 유입
  4. \(E_1\) 유저 추출 요청
  5. \(E_2\) 유저 추출 요청
  6. \(E_1\) 실험 종료
  7. \(E_2\) 유저 추출 요청

이제 시간이 흐름에 따라 각 실험이 점유하고 있는 유저 풀이 어떻게 되는지 보겠습니다.

\(E_1\), \(E_2\) 실험 설정

앞서 언급한 대로, 실험을 설정한 것 자체로는 아무 일도 일어나지 않습니다. 따라서 현재 상황은 아래와 같습니다.

p1

\(E_1\) 유저 추출 요청

\(E_1\)이 유저 추출을 요청했습니다. 현재 유저 풀은 모두 사용할 수 있기 때문에 요구 사항을 모두 들어줄 수 있습니다.

p2

모든 Cube 각각 100명씩 신규 유저 유입

신규 유저가 유입됐지만, \(E_1\)이 추가 요청한 것이 없기 때문에 \(E_1\)이 점유한 유저 수는 동일합니다.

p3

\(E_1\) 유저 추출 요청

\(E_1\)이 추가로 유저 추출을 요청했습니다. 전체 유저 수가 늘어났기 때문에 현재 \(E_1\)의 각 ExperimentGroup은 20%, 30%보다 낮은 비율의 유저를 점유하고 있습니다. 따라서 추가로 유저를 배정할 수 있습니다.

p4

\(E_2\) 유저 추출 요청

\(E_2\)에서 유저 추출을 요청했으나, 남/iOS Cube에서 문제가 발생합니다. \(E_1\)의 현재 점유율이 50%인 상황에서 \(E_2\)가 요청한 점유율이 60%이기 때문에, \(E_2\)에게 남/iOS Cube에서 원하는 유저 수를 보장해줄 수 없습니다. 어쩔 수 없이 남은 50%라도 균등하게 나눠줍니다.

p5

\(E_1\) 실험 종료

\(E_1\)이 종료되어서, \(E_1\)이 점유하고 있던 유저가 풀립니다.

p6

\(E_2\) 유저 추출 요청

이제 \(E_1\)과 경합이 붙었던 남/iOS Cube에서 부족한 유저들을 보충해줄 수 있게 되었습니다.

p7

마무리

이 글에서는 A/B test를 하는 데 있어서 발생하는 어려움과, 그 어려움을 해결하기 위해 하이퍼커넥트 데이터 사이언스 팀에서 설계 및 개발한 운영 시스템 ABzar에 대해서 소개했습니다.

ABzar UPM 알고리즘의 설명과 예시는 최대한 단순한 상황을 가정하여 소개했습니다. 실제로는 중간에 Experiment의 Cube 목록을 변경하거나, ExperimentGroup을 추가 혹은 삭제하거나, 점유율을 변경해야 하는 등 훨씬 복잡한 상황이 발생할 수 있습니다. 하지만 위의 프레임워크를 기반으로 설계하면 이러한 변화에도 유연하게 대응할 수 있고, 앞서 설명한 것 외에 더욱 다양한 기능도 지원할 수 있습니다.

ABzar는 이러한 기능들을 지원하며, 현재까지 하이퍼커넥트에서 6개월간 수십 여개의 실험을 성공적으로 운영해왔습니다. A/B test를 어떻게 운영할지 고민하시는 분들께 도움이 되기를 바라며 글을 마치겠습니다.