주요업무 1번

결제 · 정산 플랫폼

원장을 가운데 두는 설계

원장을 먼저 세우지 않으면, 바꾼 게 맞는지 검증할 기준이 없다.

티빙 현행을 진단한 문서가 아니라 “제가 만든다면”의 목표 설계 초안이며, 기간·목표치는 전부 가정입니다.

§0. 용어 풀이

약어원어
SVODSubscription VOD월정액 구독형. 티빙의 주력 BM
AVODAdvertising VOD광고를 보는 대신 무료/저가로 시청
PPVPay Per View건별 결제. 신작 영화·스포츠 단건
FASTFree Ad-supported Streaming TV무료 광고 기반 편성 채널
TVODTransactional VOD개별 구매·대여 (PPV의 상위 개념)
PGPayment Gateway카드·계좌 결제를 중계하는 사업자 (토스페이먼츠·NICE 등)
IAPIn-App Purchase앱스토어 내 결제. Apple/Google이 수수료를 가져감
멱등키Idempotency Key같은 요청이 두 번 와도 한 번만 처리되게 하는 고유키
원장Ledger돈의 이동을 불변 기록으로 남기는 회계 장부
복식부기Double-entry모든 거래를 차변·대변 양쪽에 기록해 합이 0이 되게 하는 방식
대사Reconciliation우리 장부와 PG·스토어 정산서를 맞춰보는 검증 작업
RSRevenue Share배급사·CP와 수익을 나누는 배분 정산
MGMinimum Guarantee최소보증금. 매출이 적어도 계약상 반드시 지급해야 하는 금액
MRRMonthly Recurring Revenue월 반복 매출
ARPUAverage Revenue Per User사용자 1인당 평균 매출
처른Churn해지율. 구독이 끊기는 비율
던닝Dunning결제 실패 시 재시도·안내로 구독을 살리는 절차
프로레이션Proration요금제 변경 시 남은 기간을 일할 계산하는 것
PCI DSS카드 정보 취급 보안 표준. 직접 저장하지 않는 게 최선
MoRMerchant of Record판매 주체가 되어 각국 세금 신고·현지 법인·지급을 대신 처리하는 사업자(Stripe·Paddle 등)
FXForeign Exchange환율. 통화 간 환산에 쓰는 시세
VAT / GSTValue 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_RULEeffective_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(721일)보다 짧게(710일) 잡거나 후기 단계에서 신규 프리미엄만 제한하는 절충을 둡니다.

6-2. 결제 상태기계

도식을 탭하면 확대해서 볼 수 있습니다

AUTHORIZEDCAPTURED를 구분하는 이유: 승인은 "카드 한도를 잡아둔 것"이고, 매입은 "실제로 돈을 가져온 것"입니다.

구독 갱신에서 승인만 하고 매입에 실패하면, 사용자 카드에는 금액이 잡혀 있는데 우리 매출은 없는 상태가 됩니다. 이 구간을 상태로 명시하지 않으면 조용히 새어나갑니다.


§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사 이중화
간편결제카카오페이·네이버페이·토스바이전환율에 직접 영향
IAPApple 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 확인):

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건이 발생했다는 건 구조에 구멍이 있다는 뜻이고, 구멍은 반드시 커집니다.