과금 구간별 분포로 ‘BM 밸런싱’ 근거 만들기
BM 회의에서 제일 많이 나온 말이 “이 가격이면 부담스럽지 않을까요?”였다. 근데 부담스러운지 아닌지를 아무도 숫자로 말하지 못했다. 결제 유저가 실제로 얼마씩 쓰는지 분포를 안 보고 상품 가격을 정하고 있었던 거다. 그래서 과금 분포를 두 각도로 갈라 봤다. 이 글은 그 두 장의 표를 만드는 방법과, 그걸로 회의를 근거 싸움으로 바꾼 이야기다. (아래 데이터·테이블명은 전부 합성이다. 실무에서 쓴 방법만 옮겼다.)
BM 밸런싱은 왜 늘 감으로 흘렀나?
문제는 하나였다. “결제 유저”를 한 덩어리로 봤다는 것. 평균 결제액(ARPPU) 하나로 뭉뚱그리면, 월 5천 원 쓰는 사람과 50만 원 쓰는 사람이 같은 칸에 들어간다. 그 평균으로 상품 가격을 정하니 아무도 만족 못 하는 가격이 나왔다.
실제로 겪은 장면이 있다. ARPPU가 3만 원이라길래 “3만 원짜리 패키지를 주력으로 밀자”는 결론이 났다. 그런데 막상 분포를 열어보니, 3만 원 근처에 있는 유저는 거의 없었다. 대부분은 5천 원 언저리에 몰려 있었고, 소수의 고액 결제자가 평균을 3만 원까지 끌어올린 거였다. 평균은 존재하지 않는 유저를 가리키고 있었다.
flowchart LR A[결제 유저 전체] --> B[평균 결제액 하나로 요약] B --> C[5천원 유저와 50만원 유저가 같은 칸] C --> D[아무도 아닌 평균으로 가격 결정] classDef bad fill:#fde8e8,stroke:#c81e1e,color:#6b1010,stroke-width:1px class A,B,C,D bad
평균의 함정에서 벗어나려면 분포를 봐야 한다. 나는 두 가지 방법을 겹쳐 썼다. 하나는 상대적 위치를 보는 데실(10등분), 하나는 절대 금액을 보는 구간(버킷). 둘은 서로 다른 질문에 답한다. 데실은 “누가 매출을 지탱하나”에, 버킷은 “어느 가격대가 비었나”에.
방법 1 — 결제 유저를 금액순 10등분한다 (데실)
먼저 유저별 월 결제액을 합치고, 많이 쓴 순서로 10등분(decile) 한다. SQL의 NTILE(10)이 이 일을 한 줄로 해준다. 각 데실이 결제자의 딱 10%씩을 담고, 1분위가 최상위 과금층이다.
-- 합성 예시: 유저별 월 결제액을 구해 상위→하위 10등분
SELECT
ntile,
COUNT(DISTINCT user_id) AS pu, -- 구간별 결제자 수
MIN(spend) AS min_spend,
MAX(spend) AS max_spend,
SUM(spend) AS total_spend -- 구간별 매출
FROM (
SELECT
user_id,
SUM(price) AS spend,
NTILE(10) OVER (ORDER BY SUM(price) DESC) AS ntile
FROM billing -- (합성 테이블명)
WHERE pay_date BETWEEN '2024-09-01' AND '2024-09-30'
GROUP BY user_id
) t
GROUP BY ntile
ORDER BY ntile;여기에 구간별 중앙값을 같이 뽑으면 그림이 선명해진다. PERCENTILE_DISC(0.5)로 각 데실의 중앙 결제액을 본다. 평균은 상위 이상치에 끌려가지만, 중앙값은 “그 구간의 보통 사람”을 보여준다. 특히 1분위(최상위)는 내부 편차가 크기 때문에, 평균과 중앙값을 나란히 두면 “고래 안에서도 초고래가 있다”는 게 드러난다.
결과를 합성 숫자로 그리면 대개 이렇게 나온다.
| 데실 | 결제자 비중 | 이 구간이 만든 매출 비중 | 중앙 결제액(합성) |
|---|---|---|---|
| 1 (최상위 10%) | 10% | 약 55~65% | 18만 원 |
| 2~3 | 20% | 약 20~25% | 3만 원 |
| 4~10 (하위 70%) | 70% | 약 15% 안팎 | 5천 원 |
상위 10%가 매출의 절반을 넘긴다 — 게임 과금의 전형적인 파레토다. 이 한 장이 “고래 유저 이탈이 왜 치명적인가”를 한눈에 설명한다. 하위 70%를 다 합쳐도 1분위 하나를 못 당한다는 사실은, 리텐션 정책의 우선순위를 통째로 바꾼다.
방법 2 — 절대 금액 구간으로 다시 센다
데실은 비율이라 “구체적으로 얼마짜리 상품이 비었나”는 안 보인다. 상위 10%가 매출의 60%라는 건 알겠는데, 그 10%가 실제로 얼마짜리 지갑인지는 데실만으론 모른다. 그래서 절대 금액 버킷으로 한 번 더 센다. CASE WHEN으로 금액대를 잘라 각 구간의 결제자 수를 세는 방식이다.
SELECT
COUNT(DISTINCT user_id) AS total_pu,
COUNT(DISTINCT CASE WHEN spend >= 1 AND spend < 5500 THEN user_id END) AS "~5천원",
COUNT(DISTINCT CASE WHEN spend >= 5500 AND spend < 11000 THEN user_id END) AS "5천~1만",
COUNT(DISTINCT CASE WHEN spend >= 11000 AND spend < 33000 THEN user_id END) AS "1만~3만",
COUNT(DISTINCT CASE WHEN spend >= 33000 AND spend < 55000 THEN user_id END) AS "3만~5만",
COUNT(DISTINCT CASE WHEN spend >= 55000 AND spend < 110000 THEN user_id END) AS "5만~10만",
COUNT(DISTINCT CASE WHEN spend >= 110000 THEN user_id END) AS "10만이상"
FROM (
SELECT user_id, SUM(price) AS spend
FROM billing
WHERE pay_date BETWEEN '2024-09-01' AND '2024-09-30'
GROUP BY user_id
) t;포인트는 버킷 경계를 상품 가격에 맞추는 것. 5,500원, 11,000원, 33,000원… 실제 판매 중인 패키지 가격 언저리로 자르면, “이 가격대 위로 넘어가는 유저가 급감하는 지점”이 바로 보인다. 임의의 만 원 단위로 자르는 것보다, 실제 결제 옵션 경계로 자르는 게 훨씬 말이 된다. 유저는 만 원이 아니라 “그 패키지를 살까 말까”로 결정하니까.
흔히 밟는 지뢰 세 가지
이 두 쿼리는 단순해 보여도, 실무에서 숫자가 틀어지는 지점이 정해져 있다.
flowchart TB subgraph 함정[결과를 틀어뜨리는 세 지점] P1[COUNT vs COUNT DISTINCT<br/>결제 건수를 사람 수로 착각] P2[기간 경계<br/>자정·타임존으로 하루가 새거나 겹침] P3[환불·취소<br/>결제액에 마이너스가 안 빠짐] end classDef trap fill:#fef3e2,stroke:#b25e02,color:#5c2e00,stroke-width:1px class P1,P2,P3 trap
- 사람 수 vs 건수: 결제자 분포를 볼 땐 반드시
COUNT(DISTINCT user_id)다. 그냥COUNT(*)를 쓰면 하루에 세 번 결제한 사람이 세 명으로 잡혀 분포가 부풀려진다. - 기간 경계:
pay_date가 날짜형이 아니라 시각형이면BETWEEN '9-01' AND '9-30'이 30일 자정까지만 잡아 마지막 날이 통째로 빠진다. 날짜로 캐스팅하거나 경계를 명확히 잡아야 한다. - 환불·취소: 취소 건이 별도 테이블에 있으면 순매출이 아니라 총매출로 부풀려진다. 환불을 음수로 반영한 뒤 합산해야 “실제 지갑”이 나온다.
그래서 이게 왜 BM 밸런싱 근거가 되나?
두 그림을 겹치면 결정이 데이터로 바뀐다. 데실이 “지켜야 할 층”을 알려주고, 버킷이 “비어 있는 가격대”를 알려준다.
flowchart TB subgraph 관찰 O1[데실: 상위 10%가 매출 과반] O2[금액구간: 3만~5만 구간이 텅 빔] end subgraph 해석 I1[고래 이탈 방어가 최우선] I2[중간 과금 유도 상품이 없다] end subgraph 결정 D1[상위 유저 전용 혜택 설계] D2[3만원대 패키지 신설로 계단 채우기] end O1 --> I1 --> D1 O2 --> I2 --> D2 classDef obs fill:#e8f0fe,stroke:#1a56db,color:#0b2a6b,stroke-width:1px classDef itp fill:#fef3e2,stroke:#b25e02,color:#5c2e00,stroke-width:1px classDef dec fill:#e6f4ea,stroke:#137333,color:#0b3d1f,stroke-width:1px class O1,O2 obs class I1,I2 itp class D1,D2 dec
“3만원대 상품 하나 넣자”는 말이, 이제 “3만~5만 구간에 결제자가 거의 없으니 계단을 하나 놓자”가 된다. 반대로 이미 5천 원 구간이 포화라면, 거기에 상품을 더 얹어봤자 카니발라이제이션(기존 매출 잠식)만 난다는 것도 같은 표가 말해준다. 회의가 취향 싸움에서 근거 싸움으로 바뀐다. 그게 이 분포 두 장의 값어치다.
비개발 부서엔 어떻게 전달하나?
분석의 마지막 관문은 전달이다. 기획·사업 부서에 데실이니 percentile이니를 들이밀면 대화가 끊긴다. 나는 세 가지 원칙으로 옮겼다. 첫째, 축은 “돈”으로 — ntile 1~10 대신 “월 18만 원대 / 3만 원대 / 5천 원대”처럼 중앙값을 라벨로 붙인다. 둘째, 막대는 두 개만 — 결제자 수 막대와 매출 기여 막대를 나란히 두면 “사람은 적은데 돈은 크다”가 즉시 보인다. 셋째, 결론을 문장 하나로 — “3만원대가 비어 있으니 계단을 놓자”까지 그림 밑에 박아둔다. 숫자를 행동으로 옮기는 건 결국 이 마지막 한 문장이다.
정리 — 분포는 ‘누가 얼마 쓰나’를 계단으로 보여준다
| 도구 | 보는 것 | BM에 주는 근거 |
|---|---|---|
NTILE(10) 데실 | 상대적 매출 집중도 | 어느 층을 지켜야 하나 |
CASE WHEN 금액 버킷 | 절대 가격대별 인원 | 어느 가격대가 비었나 |
PERCENTILE_DISC 중앙값 | 구간의 ‘보통 사람’ | 평균 왜곡 걷어내기 |
평균 하나로는 아무것도 못 정한다. 결제 유저를 계단으로 펼쳐 놓는 순간, 다음에 무엇을 팔지가 스스로 드러난다. BM 밸런싱은 창의력이 아니라, 분포를 제대로 세는 성실함에서 나온다.
합성 데이터로 재현한 방법론 글입니다. 특정 게임의 실제 수치·구조와 무관합니다.