과금 구간별 분포로 ‘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~320%약 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 밸런싱은 창의력이 아니라, 분포를 제대로 세는 성실함에서 나온다.

합성 데이터로 재현한 방법론 글입니다. 특정 게임의 실제 수치·구조와 무관합니다.