주요업무 1번
결제 · 정산 플랫폼
원장을 가운데 두는 설계
“원장을 먼저 세우지 않으면, 바꾼 게 맞는지 검증할 기준이 없다.”
티빙 현행을 진단한 문서가 아니라 “제가 만든다면”의 목표 설계 초안이며, 기간·목표치는 전부 가정입니다.
§0. 용어 풀이
| 약어 | 원어 | 뜻 |
|---|---|---|
| SVOD | Subscription VOD | 월정액 구독형. 티빙의 주력 BM |
| AVOD | Advertising VOD | 광고를 보는 대신 무료/저가로 시청 |
| PPV | Pay Per View | 건별 결제. 신작 영화·스포츠 단건 |
| FAST | Free Ad-supported Streaming TV | 무료 광고 기반 편성 채널 |
| TVOD | Transactional VOD | 개별 구매·대여 (PPV의 상위 개념) |
| PG | Payment Gateway | 카드·계좌 결제를 중계하는 사업자 (토스페이먼츠·NICE 등) |
| IAP | In-App Purchase | 앱스토어 내 결제. Apple/Google이 수수료를 가져감 |
| 멱등키 | Idempotency Key | 같은 요청이 두 번 와도 한 번만 처리되게 하는 고유키 |
| 원장 | Ledger | 돈의 이동을 불변 기록으로 남기는 회계 장부 |
| 복식부기 | Double-entry | 모든 거래를 차변·대변 양쪽에 기록해 합이 0이 되게 하는 방식 |
| 대사 | Reconciliation | 우리 장부와 PG·스토어 정산서를 맞춰보는 검증 작업 |
| RS | Revenue Share | 배급사·CP와 수익을 나누는 배분 정산 |
| MG | Minimum Guarantee | 최소보증금. 매출이 적어도 계약상 반드시 지급해야 하는 금액 |
| MRR | Monthly Recurring Revenue | 월 반복 매출 |
| ARPU | Average Revenue Per User | 사용자 1인당 평균 매출 |
| 처른 | Churn | 해지율. 구독이 끊기는 비율 |
| 던닝 | Dunning | 결제 실패 시 재시도·안내로 구독을 살리는 절차 |
| 프로레이션 | Proration | 요금제 변경 시 남은 기간을 일할 계산하는 것 |
| PCI DSS | — | 카드 정보 취급 보안 표준. 직접 저장하지 않는 게 최선 |
| MoR | Merchant of Record | 판매 주체가 되어 각국 세금 신고·현지 법인·지급을 대신 처리하는 사업자(Stripe·Paddle 등) |
| FX | Foreign Exchange | 환율. 통화 간 환산에 쓰는 시세 |
| VAT / GST | Value Added / Goods & Services Tax | 부가가치세 계열의 소비세. 국가마다 세율·신고 방식이 다름 |
§1. 도메인 정의 — 경계와 내부 고객
1-1. 결제와 정산은 다른 시스템이다
가장 흔한 설계 실수는 결제(Payment)와 정산(Settlement)을 한 덩어리로 보는 것이다. 둘은 시간축도, 정합성 요구도, 고객도 다르다.
| 축 | 결제 (Payment) | 정산 (Settlement) |
|---|---|---|
| 시점 | 실시간 (초 단위) | 배치 (일·월 마감) |
| 정합성 | 강한 일관성. 이중청구는 사고 | 최종 일관성. 단 재계산 가능해야 함 |
| 실패 대응 | 즉시 재시도·롤백 | 재계산·조정전표 |
| 고객 | 시청자, CS | 재무팀, 배급사, 세무 |
| 핵심 위험 | 이중 결제 · 결제 누락 | 배분 오류 · 대사 불일치 |
원장이 가운데 있는 이유: 결제도 정산도 원장에 기록을 남길 뿐, 서로를 직접 호출하지 않습니다.
원장을 append-only(추가만 가능)로 두면 "3개월 전 그날 무슨 일이 있었나"를 언제든 재구성할 수 있습니다.
정산 오류를 고치는 방법은 과거 기록을 수정하는 게 아니라 조정 전표를 추가하는 것입니다. 회계의 기본이고, 시스템도 그래야 합니다.
1-2. 내부 고객
| 내부 고객 | 하는 일 | 이 사람의 "시간"이 어디로 새는가 |
|---|---|---|
| 재무·정산 담당 | 월 마감, PG·스토어 대사, 배급사 지급 | 엑셀 대사, 불일치 원인 추적 |
| CS | 결제 문의·환불 처리 | 결제 이력이 여러 화면에 흩어져 퍼즐 맞추기 |
| 콘텐츠 사업(라이선싱) | 배급사 계약·RS 조건 협상 | 계약 조건별 배분액 시뮬레이션 불가 |
| 마케터 | 프로모션·쿠폰·요금제 실험 | 요금제 변경마다 개발 티켓 |
| 경영진 | MRR·해지율·ARPU 추이 | 지표가 시스템마다 다르게 나옴 |
1-3. 결제·정산이 반드시 푸는 여섯 가지 난제
아래는 티빙 현행에 대한 진단이 아닙니다. 구독형 미디어 서비스라면 규모와 무관하게 마주치는 도메인 보편의 난제입니다.
이 문서의 설계 결정은 전부 이 여섯 개 중 하나에 대한 답입니다.
| # | 난제 | 왜 어려운가 | 이 문서의 답 |
|---|---|---|---|
| N1 | 이중 청구 | 네트워크 재시도·서버 다중화로 같은 요청이 두 번 온다 | §2-1 원칙 3, §3-2 결정 ② |
| N2 | 진실의 원천 | 상태 컬럼만 있으면 "왜 이 금액이 됐나"를 재구성할 수 없다 | §2-1 원칙 2, 원장 |
| N3 | 결제 채널의 다양성 | 카드·계좌·통신사·IAP·기프트카드가 뒤섞인다 | §2-1 원칙 1, 어댑터 |
| N4 | 과거의 재계산 | 정산 오류는 몇 달 뒤 발견된다. 그때 필요한 건 그 시점의 계약 | §3-2 결정 ④ |
| N5 | 대사 불일치 | 우리 장부와 PG·스토어 정산서는 반드시 어긋난다 | §4-3, §5-3 |
| N6 | 요금제 출시 속도 | 요금제가 코드면 마케터가 개발팀을 기다린다 | §2-1 원칙 5 |
N1이 가장 무섭습니다. 이중 청구는 금액이 작아도 신뢰를 즉시 파괴합니다.
그래서 이 도메인의 가드레일은 "이중 청구 0건"이며, 이는 목표가 아니라 전제조건입니다.
§2. To-Be 목표 아키텍처
2-1. 설계 원칙 다섯 가지
| # | 원칙 | 구체적으로 | 답하는 난제 |
|---|---|---|---|
| 1 | 결제 채널은 어댑터로 격리 | PG·IAP·통신사를 공통 인터페이스 뒤로. 새 채널 추가가 코어를 건드리지 않음 | N3 |
| 2 | 모든 돈의 이동은 원장에 append | 상태 컬럼을 신뢰하지 않는다. 상태는 원장의 투영(projection) | N2 |
| 3 | 멱등키를 API 계약에 포함 | 클라이언트가 키를 만들고, 서버는 같은 키에 같은 응답 | N1 |
| 4 | 정산은 재계산 가능해야 한다 | 특정 시점 스냅샷으로 언제든 다시 돌릴 수 있게 | N4·N5 |
| 5 | 요금제·프로모션은 데이터, 코드가 아니다 | 마케터가 개발 티켓 없이 요금제를 만든다 (셀프서비스) | N6 |
2-2. 목표 구성도
2-3. 이 구성도를 한 문장으로
"어떤 채널로 들어온 돈이든 멱등성 게이트를 하나 통과해 원장에 append되고, 정산은 그 원장만 읽는다."
되돌리기 어려운 결정은 둘입니다 — 원장 스키마(§3)와 멱등키를 API 계약에 넣을 것인가(§5-1).
이 둘을 나중에 바꾸는 비용은, 처음에 제대로 하는 비용의 몇 배입니다.
§3. 데이터 모델 (ERD)
3-2. 설계 결정 네 가지
① 금액은 bigint amount_minor — 절대 float을 쓰지 않는다
부동소수점은 0.1 + 0.2 != 0.3이다. 돈을 float으로 다루면 반드시 1원이 사라진다. 최소 화폐단위 정수(원화는 1원, 엔화는 1엔, 달러는 센트)로 저장하고, 표시할 때만 나눈다.
② idempotency_key에 유니크 제약 — 이중 청구를 DB가 막는다 (난제 N1)
애플리케이션 락은 서버가 여러 대면 깨진다. 유니크 인덱스는 안 깨진다. 같은 키로 두 번째 INSERT가 들어오면 DB가 거부하고, 우리는 첫 번째 결과를 그대로 돌려준다.
③ LEDGER_ENTRY는 복식부기 — journal_id로 묶인 전표의 차변·대변 합은 항상 0 (난제 N2)
이 불변식(invariant)이 있으면 정합성 검증이 쿼리 한 줄로 끝난다(§4-4). 돈이 어디선가 새면 즉시 드러난다.
④ RS_RULE에 effective_from/to — 계약 조건은 시간의 함수다 (난제 N4)
배급사 계약은 갱신되고 조건이 바뀐다. 현재 조건만 저장하면 과거 정산을 재계산할 수 없다. 규칙에 유효기간을 두면 "작년 3월 기준으로 다시 계산해줘"가 가능해진다.
④번이 실무에서 가장 자주 빠지는 설계입니다.
정산 오류가 발견되는 시점은 언제나 몇 달 뒤이고, 그때 필요한 건 그 시점의 계약 조건입니다.
§4. DB 샘플 쿼리
4-1. MRR과 해지율 (경영 지표)
-- 무엇을 답하는가: "월별 반복 매출과 해지율 추이는?"
-- 주의: 프로모션 할인·프로레이션이 반영된 실효 금액으로 계산한다.
WITH monthly AS (
SELECT
DATE_TRUNC('month', p.period_start) AS 월,
SUM(p.amount_minor) AS mrr_minor,
COUNT(DISTINCT s.subscription_id) AS 활성_구독
FROM subscription_period p
JOIN subscription s ON s.subscription_id = p.subscription_id
WHERE s.state IN ('ACTIVE', 'PAST_DUE')
AND p.period_start >= NOW() - INTERVAL '12 months'
GROUP BY 1
),
churned AS (
SELECT
DATE_TRUNC('month', canceled_at) AS 월,
COUNT(*) AS 해지_구독
FROM subscription
WHERE canceled_at >= NOW() - INTERVAL '12 months'
GROUP BY 1
)
SELECT
m.월,
m.mrr_minor / 100 AS mrr_원단위,
m.활성_구독,
COALESCE(c.해지_구독, 0) AS 해지_구독,
ROUND(100.0 * COALESCE(c.해지_구독, 0) / NULLIF(m.활성_구독, 0), 2) AS 월_해지율_pct,
ROUND(m.mrr_minor::numeric / NULLIF(m.활성_구독, 0) / 100, 0) AS arpu_원단위
FROM monthly m
LEFT JOIN churned c ON c.월 = m.월
ORDER BY m.월;4-2. 이중 청구 탐지 (가드레일 — 목표 0건, 난제 N1)
-- 무엇을 답하는가: "같은 청구 주기에 두 번 이상 결제된 건이 있는가"
-- 이 쿼리가 1건이라도 반환하면 즉시 인시던트. 멱등키가 뚫렸다는 뜻이다.
SELECT
p.period_id,
s.customer_id,
COUNT(*) AS 결제_건수,
SUM(pay.amount_minor) / 100 AS 총_청구액_원,
ARRAY_AGG(pay.payment_id) AS 결제_ID_목록,
ARRAY_AGG(pay.idempotency_key) AS 멱등키_목록
FROM payment pay
JOIN subscription_period p ON p.period_id = pay.period_id
JOIN subscription s ON s.subscription_id = p.subscription_id
WHERE pay.state = 'CAPTURED'
AND pay.captured_at >= NOW() - INTERVAL '90 days'
GROUP BY p.period_id, s.customer_id
HAVING COUNT(*) > 1;4-3. 대사(Reconciliation) 불일치 — 우리 장부 vs PG 정산서 (난제 N5)
-- 무엇을 답하는가: "PG가 보낸 정산 내역과 우리 원장이 어디서 어긋나는가"
-- recon_diff는 외부 정산서를 적재한 뒤 우리 원장과 조인해 채운다.
SELECT
d.source AS 채널,
d.status,
COUNT(*) AS 불일치_건수,
SUM(ABS(d.our_amount_minor - d.their_amount_minor)) / 100 AS 총_차액_원,
MIN(d.external_txn_id) AS 예시_거래번호
FROM recon_diff d
JOIN settlement_run r ON r.run_id = d.run_id
WHERE r.period = '2027-03'
GROUP BY d.source, d.status
ORDER BY 총_차액_원 DESC;4-4. 원장 정합성 — 복식부기 불변식 검증 (난제 N2)
-- 무엇을 답하는가: "차변과 대변의 합이 0이 아닌 전표가 있는가"
-- 하나라도 나오면 원장이 깨진 것. 돈이 새고 있다는 뜻이다.
-- 이 쿼리는 매일 자동 실행되어야 하고, 결과가 비어 있지 않으면 알림이 울려야 한다.
SELECT
journal_id,
currency,
SUM(CASE WHEN direction = 'DEBIT' THEN amount_minor ELSE 0 END) AS 차변합,
SUM(CASE WHEN direction = 'CREDIT' THEN amount_minor ELSE 0 END) AS 대변합,
SUM(CASE WHEN direction = 'DEBIT' THEN amount_minor
ELSE -amount_minor END) AS 불균형
FROM ledger_entry
WHERE occurred_at >= NOW() - INTERVAL '7 days'
GROUP BY journal_id, currency
HAVING SUM(CASE WHEN direction = 'DEBIT' THEN amount_minor ELSE -amount_minor END) <> 0;4-5. 배급사 RS 정산 — 시청시간 비례 배분 + 시점 계약 적용 (난제 N4)
SVOD 구독료는 특정 콘텐츠에 귀속되지 않는다. 사용자는 카탈로그 전체에 돈을 낸다. 그래서 실무의 표준은 이번 달 구독 순매출을 각 콘텐츠의 유효 시청시간 비중대로 배분(pro-rata) 하는 것이다.
-- 무엇을 답하는가: "2027년 3월 구독 매출을 배급사에 어떻게 나눠 지급하는가"
-- 방식: 유효 시청시간 비중으로 순매출을 배분(pro-rata) → 계약 조건(share_rate) 적용 → 최소보증과 비교
-- 핵심: rs_rule의 effective_from/to로 '그 시점의 계약'을 고른다. 최신 계약이 아니다.
WITH period_bounds AS (
SELECT '2027-03-01'::timestamptz AS ps, '2027-04-01'::timestamptz AS pe
),
-- 1) 해당 월 구독 순매출 (스토어·PG 수수료를 뺀 금액)
net_revenue AS (
SELECT SUM(
CASE WHEN le.account = 'REVENUE' AND le.direction = 'CREDIT' THEN le.amount_minor
WHEN le.account = 'CHANNEL_FEE' AND le.direction = 'DEBIT' THEN -le.amount_minor
ELSE 0 END
) AS net_minor
FROM ledger_entry le, period_bounds pb
WHERE le.occurred_at >= pb.ps AND le.occurred_at < pb.pe
),
-- 2) 콘텐츠별 유효 시청시간 비중
watch_share AS (
SELECT
cr.partner_id,
SUM(v.watched_sec)::numeric
/ NULLIF(SUM(SUM(v.watched_sec)) OVER (), 0) AS share_ratio
FROM view_event v
JOIN content_ref cr ON cr.content_ref_id = v.content_ref_id
CROSS JOIN period_bounds pb
WHERE v.occurred_at >= pb.ps AND v.occurred_at < pb.pe
GROUP BY cr.partner_id
)
SELECT
p.name AS 배급사,
r.basis AS 정산_기준,
ROUND(w.share_ratio * 100, 2) AS 시청시간_비중_pct,
(nr.net_minor * w.share_ratio)::bigint / 100 AS 귀속_매출_원,
GREATEST(
(nr.net_minor * w.share_ratio * r.share_rate)::bigint,
COALESCE(r.min_guarantee_minor, 0)
) / 100 AS 지급액_원,
CASE WHEN (nr.net_minor * w.share_ratio * r.share_rate)::bigint
< COALESCE(r.min_guarantee_minor, 0)
THEN '최소보증 적용' ELSE '배분율 적용' END AS 적용_경로
FROM watch_share w
JOIN partner p ON p.partner_id = w.partner_id
JOIN rs_rule r ON r.partner_id = w.partner_id
CROSS JOIN net_revenue nr
CROSS JOIN period_bounds pb
WHERE r.effective_from <= pb.ps
AND (r.effective_to IS NULL OR r.effective_to >= pb.pe) -- ★ 그 시점의 계약
ORDER BY 지급액_원 DESC;이 쿼리에 담긴 판단 세 가지
① 시청시간 비례 배분: 구독료는 콘텐츠 하나에 귀속되지 않으므로,
SUM() OVER ()윈도우 함수로 전체 대비 비중을 구해 배분합니다.②
GREATEST(배분액, 최소보증): 배급사 계약에는 **최소보증금(MG)**이 흔합니다. 매출이 적어도 정해진 금액은 지급해야 합니다.적용_경로컬럼으로 어느 쪽이 적용됐는지 남깁니다.③ 순매출 기준: 스토어·PG 수수료를 뺀 net을 배분 대상으로 삼을지, gross로 삼을지는 계약마다 다릅니다. 그래서
rs_rule.revenue_basis에 두며, 코드에 박으면 안 됩니다.
4-6. 던닝 대상 — 결제 실패로 위험에 빠진 구독
-- 무엇을 답하는가: "결제가 실패해 곧 해지될 구독은 몇 건이고, 얼마나 살릴 수 있는가"
SELECT
s.subscription_id,
s.customer_id,
pl.code AS 요금제,
COUNT(a.attempt_id) AS 실패_횟수,
MAX(a.decline_code) AS 최근_실패사유,
s.current_period_end AS 유예_만료일,
s.current_period_end - NOW() AS 남은_시간
FROM subscription s
JOIN plan pl ON pl.plan_id = s.plan_id
JOIN subscription_period p ON p.subscription_id = s.subscription_id
JOIN payment pay ON pay.period_id = p.period_id
JOIN payment_attempt a ON a.payment_id = pay.payment_id
WHERE s.state = 'PAST_DUE'
AND a.result IN ('DECLINED', 'ERROR')
GROUP BY s.subscription_id, s.customer_id, pl.code, s.current_period_end
ORDER BY s.current_period_end;§5. 서비스 흐름도 (시퀀스)
5-1. 시나리오 A — 신규 구독 결제 (멱등성 포함)
3~6번이 이 시퀀스의 심장입니다.
네트워크가 끊겨 앱이 재시도해도, 같은 키로 오면 저장된 응답을 그대로 돌려줍니다.
이중 청구를 막는 건 트랜잭션 격리 수준이 아니라 유니크 인덱스와 응답 캐시입니다.
5-2. 시나리오 B — Apple IAP 구독 갱신 (서버 알림 기반)
IAP의 함정 두 가지:
첫째, 스토어 수수료를 매출에서 빼면 안 됩니다. 총액을 매출로, 수수료를 비용으로 각각 기록해야 배급사 RS 계산의 기준(gross)이 맞습니다.
둘째, Apple/Google은 알림을 재전송합니다. 핸들러가 멱등하지 않으면 구독 기간이 두 번 연장됩니다.
5-3. 시나리오 C — 월 정산 마감 (난제 N5)
§6. 상태 전이
6-1. 구독 상태기계
6-1-1. 구독 상태별 권한(entitlement) — 각 상태에서 고객이 무엇을 쓸 수 있나
위 상태기계는 결제 상태의 전이를 그립니다. 제품 관점의 진짜 질문은 "이 상태에서 고객이 서비스를 쓸 수 있나?" 입니다.
핵심 원칙은 결제 상태와 권한(entitlement)의 분리입니다. 결제가 실패했다고 즉시 접속을 끊지 않고,
PAST_DUE라는 완충 구간을 두어 살려낼 시간을 법니다. 차단은 던닝이 소진된UNPAID이후에만 일어납니다.
| 상태 | 접속·시청 권한 | 백그라운드 결제 | 고객에게 보이는 것 | 원가(라이선스) 관점 |
|---|---|---|---|---|
| TRIALING | 전체 사용 | 없음 (체험 종료 시 첫 결제) | 체험 잔여일 안내 | 시청분 원가 발생 (미수익) |
| ACTIVE | 전체 사용 | 정상 갱신 | 없음 (정상) | 정상 매출·정산 |
| PAST_DUE (초기, ~7일) | 전체 사용 유지 | 재시도 진행 (D+3·D+7) | 결제 실패 배너·이메일 (소프트 던닝) | 미회수 매출인데 원가 발생 시작 |
| PAST_DUE (후기, 7~14일) | 사용 유지, 압박 강화 | 재시도 (D+14) | 결제수단 갱신 모달·알림 빈도↑ | 미회수 구간 지속 → 유예 짧게 |
| UNPAID | 차단 (또는 무료 등급 강등) | 재시도 소진 | 재개하려면 결제수단 갱신 필요 | 원가 발생 차단 |
| PAUSED | 차단 (합의된 정지) | 없음 | 재개 안내 | 원가 없음 |
| CANCELED | 이미 낸 기간까지 사용 | 없음 | 만료 예정일 안내 | 낸 기간까지 정상 정산 |
| EXPIRED | 차단 (권한 회수) | 없음 | 재구독 유도 | 종료 |
OTT 특유의 제약: 일반 SaaS와 달리 OTT는 시청 = 콘텐츠 제공사에 지급할 라이선스 원가 발생입니다.
던닝 기간에 무료로 계속 보게 두면 미회수 매출인데 원가는 나가는 구간이 생기므로, OTT의 유예기간은 SaaS(7
21일)보다 짧게(710일) 잡거나 후기 단계에서 신규 프리미엄만 제한하는 절충을 둡니다.
6-2. 결제 상태기계
AUTHORIZED와CAPTURED를 구분하는 이유: 승인은 "카드 한도를 잡아둔 것"이고, 매입은 "실제로 돈을 가져온 것"입니다.구독 갱신에서 승인만 하고 매입에 실패하면, 사용자 카드에는 금액이 잡혀 있는데 우리 매출은 없는 상태가 됩니다. 이 구간을 상태로 명시하지 않으면 조용히 새어나갑니다.
§7. 동선 — 목표 상태
7-1. 정산 담당자의 월 마감 (To-Be)
"불일치 건 원인 확인"(4점)을 5점으로 만들려 하지 않습니다.
대사 불일치는 반드시 생깁니다(난제 N5). 목표는 불일치를 0으로 만드는 게 아니라, 자동 매칭되지 않은 것만 사람 앞에 올리는 것입니다.
7-2. CS 담당자 동선 — "이중 결제된 것 같아요"
"단일 화면에 표시"(2번)가 이 동선의 핵심입니다.
원장이 있으면 "이 고객의 돈이 어떻게 움직였는가"를 시간순으로 한 번에 보여줄 수 있습니다. 원장이 없으면 CS는 여러 화면을 오가며 퍼즐을 맞추고, 그동안 고객은 기다립니다.
7-3. 시청자 결제 동선 — 마찰 지점
던닝(M2)은 결제 플랫폼이 해지율에 직접 기여하는 유일한 지점입니다.
카드 한도 초과로 실패한 결제를 3일 뒤 재시도하면 상당수가 살아납니다. 재시도 타이밍·횟수·안내 채널은 실험 대상입니다.
해지의 상당 부분은 "떠나고 싶어서"가 아니라 "카드가 안 됐는데 몰라서"입니다.
§8. 솔루션 후보 비교 — 빌드 vs 바이
8-1. 구독·결제 코어
| 후보 | 강점 | 약점 | 한국 OTT 적합성 | 판단 |
|---|---|---|---|---|
| Stripe Billing | 구독·프로레이션·던닝 완비, 문서 최고 | 국내 카드 결제 커버리지 제한, IAP 통합은 별도, 수수료 | 중 | △ (해외 확장 시 재검토) |
| Recurly / Chargebee | 구독 관리 특화, 요금제 실험 강함 | 국내 PG 연동 커스텀 필요, IAP·통신사 번들 취약 | 낮음 | ✕ |
| Zuora | 엔터프라이즈 빌링·수익인식 | 고비용, 무거움, 변경 느림 | 낮음 | ✕ |
| 자체 구축 + PG/스토어 어댑터 | 국내 결제수단·통신사 번들·PPV·FAST 혼합 BM에 정확히 맞춤 | 초기 구축 비용, 던닝·프로레이션을 직접 만들어야 함 | 높음 | ✅ |
판단: 결제·구독 코어는 빌드입니다. 단, 이유가 "우리가 더 잘 만들어서"가 아닙니다.
국내 OTT는 카드·계좌·통신사 번들·기프트카드·IAP·PPV·광고형 요금제가 뒤섞입니다. 해외 SaaS는 이 조합을 전제하지 않습니다.
게다가 배급사 RS 정산은 어떤 상용 빌링 제품에도 없습니다. 결국 정산은 자체 구축이고, 그렇다면 결제와 원장을 같은 모델 안에 두는 편이 정합성 관리가 쉽습니다.
8-2. 원장 (Ledger)
| 후보 | 성격 | 판단 |
|---|---|---|
| 자체 append-only 테이블 + 복식부기 제약 | PostgreSQL로 충분. 불변식을 쿼리로 검증(§4-4) | ✅ 채택 |
| TigerBeetle | 금융 특화 초고성능 원장 DB | △ (규모가 정당화할 때. 운영 부담) |
| Formance Ledger | 오픈소스 원장 엔진 | △ (검토 가치 있음) |
| ERP에 직접 기록 | 회계는 ERP가 진실 | ✕ (실시간 결제 흐름을 감당 못함) |
원장은 "무엇을 쓰느냐"보다 "불변식을 지키느냐"의 문제입니다.
append-only, 복식부기 합=0, 조정은 수정이 아니라 추가 — 이 세 규칙만 지키면 PostgreSQL로 충분합니다.
8-3. PG · 스토어 · 부가
| 영역 | 후보 | 판단 | 비고 |
|---|---|---|---|
| PG | 토스페이먼츠 / NICE페이 / KG이니시스 | 바이 · 다중화 | 단일 PG 장애 시 결제 전면 중단. 최소 2사 이중화 |
| 간편결제 | 카카오페이·네이버페이·토스 | 바이 | 전환율에 직접 영향 |
| IAP | Apple StoreKit 2 / Google Play Billing | 바이 (강제) | 선택지 없음. S2S 알림 + 영수증 서버 검증 필수 |
| 통신사 번들 | KT·SKT·LGU+ 제휴 | 바이 · 어댑터 | 정산 주기·수수료 구조가 PG와 다름 |
| 카드 정보 저장 | PG 빌링키 (토큰) | 절대 자체 저장 금지 | PCI DSS. 카드번호를 우리 DB에 두지 않는다 |
| 세금계산서 | 팝빌 / 바로빌 | 바이 | 국세청 연동을 직접 만들 이유 없음 |
| 정산 배치 | 자체 (Temporal 재사용) | 빌드 | 01_CMS.md §8-2의 워크플로우 엔진 공유 |
8-3-1. 현행 티빙 결제수단 — 공개 확인 기준 (2026-07)
위 §8-3은 설계 관점의 후보입니다. 아래는 소비자에게 노출되는 티빙 현행 결제수단을 공개 정보로 정리한 것입니다.
백엔드 PG사(전자결제대행사)는 공식 미공개입니다. 특정 PG 채택을 단정하지 않습니다.
| 결제수단 | 현행 | 메모 |
|---|---|---|
| 신용·체크카드 | ✅ 기본 | 정기결제의 축. 신규 가입은 사실상 카드 중심 |
| 간편결제 | ✅ 네이버페이·카카오페이·토스페이 | 네이버페이는 웹 결제에서만 설정 |
| 앱 인앱결제(IAP) | △ Apple·Google | 앱 가입 시. 티빙이 공식 "비추천" → 웹 결제 유도(수수료·상태소유) |
| CJ ONE 포인트 | ✅ | 자체 '티빙캐시'는 사용 중단 |
| 통신사 휴대폰 결제 | ✕ 신규 종료 (2024.10) | 기존 설정자만 유지. 넷플릭스·디즈니+와 동일 방향 |
| 영화 VOD 개별구매(PPV) | ✕ 중단 (2024.6) | 소액결제·환불 처리 부담으로 정리 |
주목: 티빙은 채널을 늘린 게 아니라 쳐냈습니다.
PPV·통신사 결제를 접고 카드 중심으로 단순화했습니다. §8-1이 "채널이 뒤섞인다"를 자체 구축 근거로 들었지만, 현행 티빙은 운영·환불 비용 대비 전환율로 채널을 취사선택하는 방향입니다. IAP 비추천은 30% 수수료 회피 + 결제 상태 자체 소유(pull) 의도입니다.
근거 — 공개 자료 (2026-07 확인):
- 티빙도 카드만…휴대폰 신규 결제 종료 (뉴스토마토, 2024) — 통신사 결제·PPV 종료, 카드 일원화, 환불 처리 부담
- 약관 및 정책 (TVING 공식) — 유료이용약관 진입점 (백엔드 PG사는 미명시)
- 티빙 결제수단 변경 방법 (모바일·웹) — 카드·네이버페이·카카오페이·토스페이, 네이버페이 웹 전용
- 티빙 해지·환불·정기결제 안내 — 앱 결제(IAP)는 스토어에서 관리, 웹 결제 유도
8-4. 빌드/바이 한눈에
8-5. 글로벌 확장 시 결제·정산 — MoR vs 직접 인수
§8-1의 판단은 국내 우선입니다. 그래서 Stripe를 탈락(✕)이 아니라 "해외 확장 시 재검토(△)" 로 남겼습니다. 이 절이 그 "재검토"를 채웁니다.
원칙은 하나입니다. 글로벌은 코어를 바꾸는 문제가 아니라 레이어를 끼우는 문제입니다. 원장·구독 엔진·RS 정산은 그대로 두고, 바뀌는 것은 수납(acquiring)·세금·지급(payout) 세 레이어뿐입니다.
8-5-1. 코어는 이미 통화 인지(currency-aware)로 지어져 있다
원장은 복식부기 합=0을 통화별로 검증합니다(§4-4의
GROUP BY journal_id, currency). 서로 다른 통화를 숫자로 상계하지 않으므로, 국내용 원장이 그대로 글로벌 원장의 골격이 됩니다.요금도
PLAN_PRICE가 지역·통화별 가격으로 분리돼 있어(§3 데이터 모델), 새 통화를 붙이는 데 스키마 변경이 필요 없습니다. 바뀌는 건 아래 세 레이어입니다.
| 축 | 국내 (현 설계) | 글로벌에서 추가 | 빌드/바이 |
|---|---|---|---|
| ① 현지 결제수단 | PG·간편결제·통신사 번들 | 일본 콤비니·캐리어빌링, 미국 카드, 동남아 e-wallet | 바이 · 어댑터 확장 (§8-3 패턴 재사용) |
| ② 다중 통화·환율(FX) | KRW 단일 | 현지 통화 표시가 + FX 스냅샷(연결 리포팅용) | 빌드 (원장 이미 통화별, 환율 기록만 추가) |
| ③ 세금 | 부가세·세금계산서(팝빌) | 국가별 VAT/GST/Sales Tax, 디지털서비스세 | 바이 급부상 — 판단이 바뀌는 지점 |
| ④ 지급·정산(payout) | 국내 계좌 | 국경 간 정산, 국가별 법인·현지 인수사 | 바이 (법인 10개를 세울 순 없음) |
8-5-2. 핵심 결정 — MoR이 글로벌에선 빌드/바이 계산을 뒤집는다
국내에선 "자체 구축이 이긴다"였습니다. 그러나 글로벌의 ③ 세금·④ 지급 때문에 판단이 바뀝니다.
MoR(Merchant of Record) — Stripe·Paddle 같은 사업자가 판매 주체가 되어 각국 세금 신고·현지 법인·환율·지급을 대신 처리합니다. 수수료는 국내 PG보다 비싸지만(통상 5%+), 나라마다 법인을 세우고 세무 등록하는 비용·리스크를 통째로 아웃소싱합니다.
그래서 글로벌 초기의 정답은 하이브리드입니다 — 코어(원장·구독·RS 정산)는 자체 유지 + 각국 수납·세금은 MoR/PSP를 어댑터로 붙인다.
| 채널 | 세금·환율·지급 | 수수료 | 적합 시점 |
|---|---|---|---|
| 스토어 IAP (Apple/Google) | 30% 안에 전부 포함 | 높음 (30%) | 신규 시장 즉시 진입 — 세무 인프라 불필요 |
| MoR (Stripe·Paddle) | 대행사가 부담 | 중 (5%+) | 웹 결제로 거래액 키우는 확장기 |
| 직접 인수사 + 자체 정산 | 우리가 부담 (현지 법인·세무 등록) | 낮음 | 거래액이 법인 설립을 정당화할 만큼 클 때 |
판단: 글로벌은 "IAP·MoR로 빠르게 진입 → 규모가 커지면 직접 인수사로 전환"의 단계적 접근입니다.
규모가 커지기 전에 직접 구축하면 손해입니다. 이는 §9-2의 "국내를 첫 테넌트로 만들어 검증한 뒤 두 번째 테넌트" 논리와 정확히 같은 결입니다 — 구조를 먼저 검증하고, 규모가 비용을 정당화할 때 내재화합니다.
참고로 IAP는 국내에선 "수수료 비싸다"가 단점이지만, 세무 인프라가 없는 신규 해외 시장에선 즉시 과금 가능한 유일한 채널이라 오히려 초기 진입 통로가 됩니다.
§9. 실행 로드맵
9-1. Phase 0 — 무엇을 실측하는가
세 갈래 중 하나를 고르는 게 아니라, 순서를 정하는 것입니다.
제 판단으로는 멱등성·원장이 항상 먼저입니다. 기반이 없는 상태에서 자동화하면 틀린 계산을 빠르게 하게 됩니다.
9-2. 전환 원칙 — 결제는 CMS보다 더 보수적으로
CMS는 잘못되면 콘텐츠가 늦게 걸립니다. 결제는 잘못되면 돈이 사라지거나 두 번 빠져나갑니다.
그래서 결제 도메인의 전환은 원장 → 그림자 쓰기 → 카나리 → 정산 이관 4단계를 거칩니다.
9-3. 일정 (가정)
원장이 맨 앞에 오는 게 이 로드맵의 유일한 고집입니다.
원장 없이 결제 코어를 바꾸면, 바꾼 게 맞는지 검증할 기준이 없습니다.
§10. 성과지표
10-1. 지표 트리
10-2. 지표 정의표
| 지표 | 유형 | 정의 | 쿼리 |
|---|---|---|---|
| 결제 성공률 | 입력 | CAPTURED ÷ 전체 결제 시도 | payment_attempt 집계 |
| 던닝 회수율 | 입력 | PAST_DUE → ACTIVE 복귀 비율 | 상태 전이 로그 |
| 정산 마감 소요일 | 입력 | 월말 → CLOSED까지 일수 | settlement_run |
| 대사 자동 매칭률 | 입력 | 사람 개입 없이 매칭된 거래 비율 | recon_diff 역산 |
| 요금제 출시 리드타임 | 입력 | 기획 승인 → 판매 시작 | 어드민 로그 |
| 이중 청구 | 가드레일 | 동일 주기 2건 이상 CAPTURED (목표 0) | §4-2 |
| 원장 불변식 위반 | 가드레일 | 차변≠대변 전표 수 (목표 0) | §4-4 |
| 권한 부여 지연 | 가드레일 | 결제 완료 → 시청 가능 p99 | 애플리케이션 트레이스 |
10-3. 중단 기준
| 조건 | 조치 |
|---|---|
| 카나리 구간 이중 청구 1건 | 즉시 100% 기존 시스템 복귀. 사후 검토 필수 |
| 원장 불변식 위반 발생 | 신규 코어 쓰기 중단. 원인 규명 전 재개 금지 |
| 그림자 쓰기 결과가 기존과 0.01% 이상 불일치 | 카나리 착수 금지 |
| 대사 자동 매칭률이 기존 수작업 정확도 미만 | 병행 운영 연장 |
"이중 청구 1건이면 롤백"이 과하다고 느껴질 수 있습니다. 과하지 않습니다.
결제 사고는 확률이 아니라 신뢰의 문제입니다. 1건이 발생했다는 건 구조에 구멍이 있다는 뜻이고, 구멍은 반드시 커집니다.