콘텐츠로 이동

Billing, Subscription & Entitlement

분류: Layer 13 - Product Engineering & Growth Systems

Product Engineer가 결제 기능을 다룬다는 것은 checkout 버튼을 붙이는 것에서 끝나지 않는다. 사용자가 어떤 plan에 있고, 결제가 실패했을 때 어떤 유예 상태가 되며, 어떤 feature에 접근할 수 있고, webhook이 지연되거나 중복되어도 제품 상태가 맞게 회복되는지를 설계해야 한다.

Billing, Subscription & Entitlement는 가격제, 구독 lifecycle, 결제 이벤트, 기능 접근 권한을 제품 도메인 모델과 연결하는 Product Engineering 역량이다.

SaaS 제품에서 billing은 revenue와 user experience가 만나는 가장 민감한 흐름이다.

  • 결제에 성공했는데 유료 기능이 열리지 않으면 신뢰가 깨진다.
  • 결제가 실패했는데 기능이 계속 열리면 revenue leakage가 생긴다.
  • plan 이름으로 직접 기능 접근을 판단하면 pricing 변경 때 코드가 흔들린다.
  • webhook이 중복·지연·누락되면 subscription 상태가 틀어진다.
  • seat, usage limit, trial, coupon, cancellation이 얽히면 support 대응이 어려워진다.

Product Engineer는 결제 provider의 객체와 내부 제품 객체를 분리해서 이해해야 한다. Provider의 subscription과 내부의 workspace entitlement는 같지 않다. 하나는 billing source of truth이고, 다른 하나는 제품 접근 제어의 source of truth다.

2.5. 선행 방식의 한계 — 왜 Entitlement가 필요한가

섹션 제목: “2.5. 선행 방식의 한계 — 왜 Entitlement가 필요한가”

초기 제품은 plan = pro이면 특정 기능을 열어주는 방식으로 충분해 보인다. 하지만 pricing이 바뀌고, add-on이 생기고, enterprise 예외 계약이 생기고, trial과 grace period가 생기면 plan 이름으로 기능 접근을 직접 판단하는 코드는 빠르게 복잡해진다.

Stripe의 Billing Entitlements 문서는 Stripe product와 내부 service feature를 매핑하고 entitlement 변경 webhook으로 access provisioning을 구동하는 구조를 제안한다. Subscription Webhooks 문서는 subscription status change, payment failure, cancellation 같은 lifecycle event를 webhook으로 처리해야 함을 보여준다.

따라서 entitlement는 “plan 이름을 feature check로 바로 쓰는 방식”의 한계에서 나온다. Product Engineer는 결제 상태와 기능 접근 권한 사이에 entitlement layer를 두어 pricing 변화, webhook 지연, 예외 계약, rollout을 견딜 수 있게 만들어야 한다.

객체의미내부 모델링 포인트
Customer결제 provider의 고객내부 workspace/account와 매핑
Product/Price판매 상품과 가격내부 plan 또는 package와 매핑
Subscription반복 결제 상태trialing, active, past_due, canceled
Invoice청구 단위payment_failed, paid, void
Payment Method결제 수단만료, 실패, 갱신 필요
Entitlement기능 접근 권한feature key, limit, enabled
Seat/Usage사용량 기반 과금 축member count, API calls, storage

중요한 것은 결제 provider 객체를 내부 제품 모델에 그대로 복사하지 않는 것이다. 내부 제품은 workspace_id, entitlement_key, feature_limit, access_state 같은 자체 언어가 필요하다.

subscription은 단순히 active/inactive가 아니다.

trialing -> active
trialing -> canceled
active -> past_due
past_due -> active
past_due -> canceled
active -> canceled_at_period_end
canceled_at_period_end -> canceled

각 상태는 제품 접근에 영향을 준다.

상태제품 접근 판단
trialingtrial entitlement 열림, trial end 안내
activepaid entitlement 열림
past_due유예 기간 동안 제한적 접근 또는 안내
canceled_at_period_end기간 종료 전까지 접근 유지
canceledpaid entitlement 회수

Product Engineer는 이 표를 제품 정책으로 명확히 해야 한다. 결제 provider의 상태를 그대로 노출하는 것보다, 내부 access state로 번역하는 편이 안전하다.

퀴즈

plan 이름으로 직접 기능 접근을 판단하면 왜 위험한가?

힌트: pricing은 바뀌지만 기능 접근 규칙은 더 안정적인 계약이어야 한다.

정답 보기

plan 변경, add-on, enterprise 예외, trial, grace period가 생기면 plan 조건문이 코드 곳곳에 퍼진다. entitlement를 별도 모델로 두면 결제 상품과 내부 기능 접근 권한을 느슨하게 연결할 수 있다.

Entitlement는 “이 account가 이 feature를 어떤 한도로 사용할 수 있는가”를 표현한다.

type Entitlement = {
workspaceId: string;
featureKey: "advanced_export" | "sso" | "api_access";
enabled: boolean;
limit?: number;
source: "subscription" | "trial" | "enterprise_override";
validUntil?: string;
};

이 모델을 두면 UI, API, background job이 같은 방식으로 feature access를 확인할 수 있다.

Plan Check와 Entitlement Check

Plan check

if plan === pro 같은 조건으로 기능을 연다.

초기에는 빠르지만 pricing 변경과 예외 계약에 약하다.

Entitlement check

feature key별 접근 권한과 limit을 조회한다.

add-on, trial, enterprise override, staged rollout에 강하다.

Usage check

entitlement의 limit과 현재 사용량을 함께 본다.

seat, API call, storage, export count 같은 제한에 필요하다.

Audit check

누가 언제 entitlement를 열고 닫았는지 추적한다.

billing incident와 support 대응에 필요하다.

결제 상태는 webhook으로 들어오는 경우가 많다. webhook은 반드시 중복, 역순, 지연을 견딘다고 가정해야 한다.

필수 기준은 다음과 같다.

  • event id를 저장해 중복 처리를 막는다.
  • provider signature를 검증한다.
  • subscription/customer id로 내부 workspace를 찾는다.
  • 이벤트를 바로 처리하지 못하면 재시도 가능한 queue에 넣는다.
  • 상태 전이는 현재 상태와 event timestamp를 보고 결정한다.
  • 결제 provider API로 최종 상태를 재조회해 reconcile할 수 있어야 한다.
  • 실패한 entitlement provisioning은 dead-letter 또는 repair job으로 복구한다.

시나리오

결제 성공 webhook은 왔지만 유료 기능이 열리지 않았다

provider에는 subscription이 active로 보이지만 내부 workspace entitlement는 free 상태다. 사용자는 결제 완료 직후 premium export가 막혔다고 문의했다.

webhook event log, subscription mapping, entitlement provisioning, retry/reconcile 중 무엇을 어떤 순서로 확인할 것인가?

Billing은 시스템 상태이면서 UX다. 사용자는 돈과 접근 권한에 민감하므로 상태를 명확히 보여줘야 한다.

상황UX 원칙
trial 종료 임박남은 기간과 다음 과금 시점을 명확히 보여준다
payment failed기능 차단보다 먼저 복구 행동을 제공한다
seat limit왜 막혔고 어떤 선택지가 있는지 보여준다
cancellation즉시 해지와 period end 해지를 구분한다
upgrade success어떤 기능이 열렸는지 즉시 확인시킨다
downgrade잃는 기능과 데이터 보존 정책을 설명한다

Product Engineer는 billing state와 UI state를 맞춰야 한다. 내부 상태는 active인데 UI가 past_due로 보이거나, entitlement가 닫혔는데 UI가 upgrade success를 보여주면 신뢰가 깨진다.

Billing 기능도 제품 지표로 봐야 한다.

  • checkout_started -> checkout_completed conversion
  • payment_failed recovery rate
  • trial_started -> paid conversion
  • upgrade_completed 후 feature activation
  • downgrade/cancellation reason
  • entitlement_provision_failed count
  • billing support ticket rate

하지만 결제 이벤트에는 민감정보가 많다. card number, email 원문, 주소, invoice 상세 원문을 analytics event에 넣지 않는다. 필요한 경우 결제 provider id와 내부 id도 접근 제한을 둔다.

Billing/Subscription/Entitlement 체크

  • 결제 provider 객체와 내부 workspace/account 객체의 매핑이 명확하다
  • subscription lifecycle 상태별 제품 접근 정책이 정의되어 있다
  • plan 이름 대신 entitlement feature key로 접근을 판단한다
  • seat, usage limit, add-on, enterprise override를 표현할 수 있다
  • webhook 처리는 signature 검증, idempotency, retry, reconcile을 포함한다
  • entitlement provisioning 실패를 복구할 수 있다
  • billing UX가 trial, past_due, cancellation, upgrade 상태를 명확히 보여준다
  • billing analytics에 민감 결제 정보가 들어가지 않는다

Billing은 제품 도메인 모델과 privacy governance를 동시에 요구한다.

  • Frontend Product Quality & RUM: 결제와 권한 흐름의 오류·성능·폼 안정성을 본다.
  • Privacy, Consent & Product Data Governance: 결제 정보와 분석 이벤트의 개인정보 경계를 다룬다.
  • Product Domain Modeling: workspace, membership, entitlement의 상태 전이를 기반으로 한다.
  • Subscription lifecycle
  • Entitlement
  • Access provisioning
  • Webhook idempotency
  • Billing reconciliation
  • Grace period
  • Seat billing
  • Usage limit
  • Revenue leakage