B2B SaaS 구독 해지 후 entitlement 회수 시점 검증 체크리스트 2026, access 종료 불일치 판별법
B2B SaaS 구독 해지 후 entitlement 회수 시점 검증 체크리스트 2026, access 종료 불일치 판별법
B2B SaaS에서 구독 해지 요청이 저장됐다고 해서 고객의 접근 권한이 바로 끝나는 것은 아닙니다. 고객이 현재 결제 기간 끝까지 사용할 수 있도록 예약 해지를 지원할 수도 있고, 즉시 해지·grace period·결제 실패에 따른 제한처럼 서로 다른 정책을 둘 수도 있습니다.
문제는 이 정책이 billing, subscription, 제품 권한, 캐시, CRM에 따로 구현될 때 생깁니다. cancellation_requested는 기록됐지만 실제 사용 권한은 계속 열려 있거나, 반대로 고객이 결제 기간 동안 사용할 수 있어야 하는데 너무 일찍 잠길 수 있습니다. 운영팀이 해지 수만 보면 이 차이를 찾기 어렵습니다.
이 글은 특정 결제 공급사의 해지 정책을 대신 결정하는 글이 아닙니다. B2B SaaS 운영팀이 구독 해지 뒤 다음 세 시각을 비교해 데이터·권한 전이의 누락을 찾는 체크리스트입니다.
이 글은 cancellation_reason, churn 분류, MRR·매출 인식, refund·proration 계산을 다루지 않습니다. cancel_at_period_end와 cancellation reason의 정의는 기존 취소·churn 글의 범위이며, 여기서는 해지 상태를 권한 시점 검증의 입력값으로만 사용합니다. feature flag rollout, plan upgrade, usage limit 변경도 entitlement 회수 지연으로 판정하지 않고 별도 권한·배포 상태로 분리합니다.
cancellation_effective_at: 해지가 실제로 효력을 갖는다고 정한 시각access_end_at: 고객이 제품 기능에 접근할 수 없게 된 시각entitlement_revoked_at: entitlement 원장에서 권한 회수가 기록된 시각
환불·credit note·proration의 금액 대사는 B2B SaaS 환불·크레딧 노트 추적 감사 체크리스트에서, 해지 사유와 churn 분류는 B2B SaaS 구독 취소·churn reason 추적 감사 체크리스트에서 다룹니다. 이 글에서는 금액이나 churn 분석을 다시 설명하지 않고 권한이 언제 닫혔는지 증명하는 시점 검증에 집중합니다.
먼저 해지 요청과 해지 효력을 분리합니다
운영 데이터에서 cancel_clicked_at, cancellation_requested_at, cancellation_effective_at을 하나의 cancelled_at으로 합치면 안 됩니다. 사용자가 해지 버튼을 누른 시각, 백엔드가 요청을 승인한 시각, 현재 서비스 접근이 끝나는 시각은 다를 수 있습니다.
| 필드 | 의미 | 흔한 오해 |
|---|---|---|
cancel_clicked_at |
사용자가 화면에서 해지 동작을 시작한 시각 | 이 시각에 이미 해지가 확정됐다고 봄 |
cancellation_requested_at |
서버가 해지 요청을 유효한 요청으로 저장한 시각 | 공급사 상태 변경 시각과 항상 같다고 봄 |
cancellation_effective_at |
구독 해지가 실제 효력을 갖는 기준 시각 | 예약 해지와 즉시 해지를 구분하지 않음 |
access_end_at |
제품 접근을 끝낼 예정 또는 실제 종료 시각 | entitlement 회수 시각으로 대체함 |
entitlement_revoked_at |
권한 원장에 회수가 기록된 시각 | 캐시가 갱신되기 전에도 실제 접근이 끝났다고 봄 |
access_observed_closed_at |
실제 권한 확인 또는 차단 결과를 관찰한 시각 | 내부 명령 시각과 실제 제품 결과를 구분하지 않음 |
예를 들어 예약 해지라면 cancellation_requested_at은 오늘이고 cancellation_effective_at과 access_end_at은 현재 결제 기간의 종료 시각일 수 있습니다. 즉시 해지라면 세 시각이 가까울 수 있지만, 비동기 webhook과 권한 projection 때문에 entitlement_revoked_at이 늦어질 수 있습니다.
Stripe 공식 문서도 구독 취소에서 즉시 취소와 현재 기간 종료 시점에 취소하는 흐름을 구분합니다. 공급사 문서의 필드와 실제 계정 설정은 다를 수 있으므로, 외부 필드명을 내부 필드에 그대로 복사하기보다 내부에서 정책을 명시적으로 매핑해야 합니다.
원문: 원문기사 보기
원문: 원문기사 보기
검증 대상은 세 가지 상태 원장입니다
해지 후 접근 권한을 검증하려면 구독 상태 하나만 읽어서는 부족합니다. 아래 세 원장을 같은 account_id, subscription_id, entitlement_key로 연결해야 합니다.
1. Subscription 원장
구독 원장은 공급사 또는 내부 billing 시스템의 계약 상태를 저장합니다.
최소 필드는 다음과 같습니다.
| 필드 | 기록할 값 | 확인 질문 |
|---|---|---|
subscription_id |
내부·공급사 구독 ID | 동일 고객의 다른 구독과 혼동하지 않는가 |
account_id |
workspace 또는 회사 ID | 개인 사용자와 회사 계정이 뒤섞이지 않는가 |
plan_code |
내부 요금제 코드 | 현재 entitlement mapping 버전과 연결되는가 |
billing_status |
active, past_due, canceled 등 | 공급사 원문 상태를 임의로 번역하지 않았는가 |
cancel_mode |
immediate, period_end, grace, manual_review | 해지 정책이 명시돼 있는가 |
cancellation_requested_at |
요청 시각 | 고객 행동과 서버 확정을 구분하는가 |
cancellation_effective_at |
효력 시각 | period end 또는 즉시 시각의 근거가 있는가 |
source_event_id |
webhook 또는 내부 명령 ID | 상태 변경 원문을 재조회할 수 있는가 |
state_version |
상태 버전 또는 sequence | 늦게 도착한 event가 최신 상태를 덮지 않는가 |
billing_status=canceled라는 값만으로 access_end_at을 계산하면 위험합니다. 어떤 제품은 해지 즉시 접근을 끝내고, 어떤 제품은 기간 종료까지 유지하며, 어떤 제품은 계약·지원 정책에 따라 별도 유예를 둡니다. 중요한 것은 어느 정책을 선택했는지가 아니라 선택한 정책과 실제 권한 전이가 같은지입니다.
2. Entitlement 원장
entitlement 원장은 고객이 실제로 사용할 수 있는 기능·좌석·용량을 표현합니다. 구독 원장과 동일한 시스템에 있더라도 별도 상태로 기록하는 편이 좋습니다.
| 필드 | 기록할 값 | 확인 질문 |
|---|---|---|
entitlement_id |
권한 레코드 ID | 한 계정의 여러 기능 권한을 구분하는가 |
entitlement_key |
api_access, sso, advanced_reports 등 |
기능명이 계획·제품과 일관되는가 |
expected_state |
정책 계산 결과 | 왜 active 또는 revoked가 되어야 하는가 |
actual_state |
권한 원장의 현재 상태 | billing 결과와 실제 값이 일치하는가 |
entitlement_effective_at |
권한 변경 적용 시각 | 명령 발행 시각과 구분되는가 |
entitlement_revoked_at |
회수 기록 시각 | 회수 command 성공과 기록 완료를 혼동하지 않는가 |
source_action_id |
권한 변경 action ID | 어느 event가 권한을 바꿨는가 |
mapping_version |
plan-to-entitlement 변환 버전 | 과거 결과를 재현할 수 있는가 |
projection_observed_at |
read model 확인 시각 | 캐시·projection 지연을 분리하는가 |
예상 상태가 revoked라고 계산됐지만 실제 상태가 active라면 즉시 고객 차단부터 하지 말고 원인을 분류합니다. 권한 action이 생성되지 않았을 수도 있고, action은 성공했지만 projection이 늦었을 수도 있으며, period_end 정책상 아직 접근이 정상일 수도 있습니다.
3. Access 관찰 원장
권한 원장에서 revoked가 보인다고 실제 모든 요청이 차단됐다고 단정하지 않습니다. API gateway, 애플리케이션 cache, 세션 token, background job이 별도 상태를 가질 수 있습니다.
| 필드 | 기록할 값 | 확인 질문 |
|---|---|---|
access_end_at |
정책상 접근 종료 예정·확정 시각 | 값의 출처가 정책 계산인지 관찰 결과인지 |
access_observed_closed_at |
실제 차단 또는 deny 확인 시각 | 어떤 probe·요청으로 확인했는가 |
access_layer |
API, web, worker, export 등 | 일부 경로만 계속 열려 있지 않은가 |
cache_version |
권한 cache 또는 policy version | 오래된 cache가 남아 있지 않은가 |
session_revoked_at |
세션·토큰 회수 시각 | 로그인 세션이 계속 유효하지 않은가 |
probe_result |
closed, still_open, unknown | unknown을 closed로 자동 변환하지 않는가 |
특히 browser 화면에서 기능이 사라진 것과 API가 거부된 것은 같은 증거가 아닙니다. 사용자 화면, API, export, scheduled workflow처럼 유료 기능이 소비되는 경로를 나눠 확인해야 합니다.
세 시각을 한 행으로 대사합니다
실무에서는 해지 1건을 다음처럼 한 행으로 묶어 비교하면 시작하기 쉽습니다.
| 검증 키 | cancellation effective | access end | entitlement revoked | 판정 |
|---|---|---|---|---|
sub_demo_001 / api_access |
2026-08-03 09:00 | 2026-08-03 09:00 | 2026-08-03 09:00 | match |
sub_demo_002 / reports |
2026-08-03 09:00 | 2026-08-03 09:00 | 2026-08-03 09:07 | delayed_revoke |
sub_demo_003 / export |
2026-08-03 09:00 | 2026-08-03 09:00 | 2026-08-02 18:00 | early_revoke |
sub_demo_004 / sso |
2026-08-31 00:00 | 2026-08-31 00:00 | null | expected_until_effective |
이 표에서 match는 값이 같은 경우만 뜻하지 않습니다. 해지 정책상 허용된 지연 범위와 검증 증거가 있어야 합니다. 예를 들어 period_end 정책으로 8월 31일까지 접근을 유지하기로 했다면 8월 3일에 entitlement_revoked_at=null인 것은 오류가 아닐 수 있습니다. 대신 access_end_at=2026-08-31과 그 정책의 근거가 기록되어 있어야 합니다.
권장 판정 코드
판정 코드는 원인을 한 단어로 숨기지 않도록 제한된 값으로 관리합니다.
| 판정 | 의미 | 다음 조치 |
|---|---|---|
match |
정책상 예정 시각과 실제 권한 상태가 일치 | 종료 또는 표본 모니터링 |
expected_until_effective |
아직 해지 효력 시각 전 | 효력 시각까지 관찰 예약 |
delayed_revoke |
효력 시각 이후 권한 회수가 늦음 | event·queue·projection owner 확인 |
early_revoke |
효력 시각보다 먼저 권한이 닫힘 | 고객 영향과 정책 계산 확인 |
mapping_error |
plan 또는 feature mapping이 잘못됨 | mapping 버전과 계약 기준 대조 |
stale_read |
원장은 바뀌었지만 cache/read model이 오래됨 | cache invalidation·재조회 |
unknown |
필요한 근거를 조회하지 못함 | 자동 완료 금지, 수동 확인 |
unknown을 match로 바꾸는 fallback은 두지 않는 편이 좋습니다. 검증이 되지 않은 상태를 정상으로 집계하면 접근 권한 누락과 과다 허용을 모두 놓칠 수 있습니다.
확정된 해지 상태와 권한 회수는 별도로 확인합니다
구독 해지 상태가 billing 원장에 저장됐다는 로그는 권한 회수가 완료됐다는 증거가 아닙니다. 외부 event나 내부 명령으로 확정된 해지 상태를 입력으로 삼아, 권한 action 생성·권한 원장 반영·cache 갱신·실제 access 차단을 각각 남겨야 합니다. webhook 수신·signature·replay run 자체의 설계는 이 글의 범위가 아니며, 앞 단계가 확정한 상태와 시각만 사용합니다.
원문: 원문기사 보기
Paddle도 구독 취소를 별도 상태 변경으로 설명하지만, 이벤트 이름·전달 순서·재전송 동작은 공급사마다 다릅니다. 따라서 공급사 event를 내부 상태 전이의 유일한 시계로 쓰지 말고, state_version 또는 내부 sequence를 보존해야 합니다.
원문: 원문기사 보기
이벤트 처리 로그에서 최소한 아래 단계를 분리합니다.
confirmed_cancellation_state
→ cancellation_effective_computed
→ entitlement_revoke_action_created
→ entitlement_ledger_updated
→ cache_or_projection_updated
→ access_probe_closed
각 단계의 시각과 결과가 있어야 delayed_revoke와 stale_read를 구분할 수 있습니다. processed=true, cancelled=true 같은 단일 플래그는 원인 조사에 부족합니다.
예약 해지·즉시 해지·유예를 분리합니다
같은 canceled 상태라도 고객 영향은 다릅니다. 내부 검증표에 cancel_mode를 반드시 포함하십시오.
예약 해지
예약 해지는 다음 갱신을 하지 않되 현재 기간 동안 기능을 유지하는 정책입니다. 이 경우 cancellation_requested_at은 오늘이지만 cancellation_effective_at과 access_end_at은 미래일 수 있습니다.
확인할 항목은 다음과 같습니다.
- 현재 기간 종료 시각이 timezone 변환 뒤에도 같은가
- 기간 종료 전에는 entitlement가 유지되는가
- 기간 종료 event가 늦게 도착해도 이미 지난 기간을 다시 열지 않는가
- 고객 화면의 “해지 예약” 상태와 API 접근 정책이 같은가
- 만료 시점에 자동 회수 action과 audit log가 남는가
즉시 해지
즉시 해지는 cancellation_effective_at이 요청 시각과 가까울 수 있지만, 모든 기능이 동시에 닫힌다고 가정하면 안 됩니다. API와 web은 다른 cache를 사용할 수 있고, 이미 발행된 export나 background job은 별도 처리 정책이 필요할 수 있습니다.
확인할 항목은 다음과 같습니다.
- 권한 회수 action이 한 번만 생성되는가
- 이미 처리한 webhook이 다시 도착해 권한을 재부여하지 않는가
- active session과 API token이 정책대로 처리되는가
- access probe가 기능별로 모두 deny를 확인하는가
- 재활성화 시 이전 회수 event가 새 권한을 덮지 않는가
유예 또는 결제 실패 후 제한
결제 실패나 grace period는 해지와 같은 상태가 아닐 수 있습니다. B2B SaaS 결제 실패·dunning 자동화 체크리스트와 연결해 billing_status, entitlement_status, access_status를 별도 필드로 보십시오.
이 글에서 유예 기간의 길이나 접근 제한 정책을 정하지 않는 이유는 제품·계약·공급사 설정에 따라 다르기 때문입니다. 운영팀이 해야 할 일은 정책을 문서화하고, 정한 시각과 실제 권한 상태가 일치하는지 검증하는 것입니다.
대사 리포트 템플릿
아래 필드는 스프레드시트나 운영 대시보드의 최소 열로 사용할 수 있습니다. 실제 고객 이메일, 카드 정보, 계약 원문은 넣지 말고 내부 ID와 마스킹된 식별자만 사용합니다.
| 영역 | 필드 | 예시 | 판정에 쓰는 이유 |
|---|---|---|---|
| 식별 | account_id / subscription_id |
acct_demo / sub_demo_002 |
고객·구독 연결 |
| 정책 | cancel_mode |
immediate |
종료 기준 선택 |
| 정책 | cancellation_effective_at |
2026-08-03T09:00:00Z |
권한 종료 기대 시각 |
| 권한 | entitlement_key |
reports |
기능 범위 |
| 권한 | entitlement_revoked_at |
2026-08-03T09:07:12Z |
원장 회수 완료 시각 |
| 접근 | access_end_at |
2026-08-03T09:00:00Z |
제품 접근 종료 기준 |
| 관찰 | access_observed_closed_at |
2026-08-03T09:08:01Z |
실제 차단 관찰 |
| 근거 | source_event_id / action_id |
evt_demo / act_demo |
상태 전이 추적 |
| 버전 | state_version / mapping_version |
42 / ent-v7 |
늦은 event·mapping 오류 확인 |
| 결과 | reconciliation_status |
delayed_revoke |
후속 조치 라우팅 |
| 담당 | owner / next_review_at |
billing-ops / timestamp |
미해결 행 관리 |
공개 전후로 확인할 내부 링크
이 글은 기존 글의 내용을 반복하기보다 권한 회수 시점에 초점을 둡니다. 다음 링크는 각 시스템의 경계를 이해하는 용도로만 연결합니다.
- 해지 요청·churn 사유 분류: 구독 취소·churn reason 추적 감사 체크리스트
- entitlement와 feature flag 구분: entitlement·feature flag 변경 추적 감사 체크리스트
- webhook event와 replay 근거: 결제 webhook replay 검증 로그·대사 리포트 템플릿
- 월말 billing 대사: B2B SaaS 구독 청구 월말 대사 체크리스트
직전 replay 대사 글은 event ledger와 provider object를 연결하는 방법을 설명하지만, 이 글은 그 결과로 확정된 cancellation 시각을 실제 접근 권한 종료 시각과 대조하는 후속 검증으로 범위를 좁힙니다. webhook replay와 event ledger·provider object·invoice 대사는 기존 replay 글의 범위이며, 여기서는 확정된 cancellation 시각 이후의 내부 access·entitlement 시각만 대조합니다.
최종 체크리스트
공개 또는 운영 대시보드 반영 전에 아래 질문에 모두 답할 수 있어야 합니다.
- 해지 요청 시각과 해지 효력 시각이 별도 필드로 저장되는가
cancel_mode가 immediate, period_end, grace 등으로 명시돼 있는가access_end_at을 누가 계산했고 어떤 정책 버전이 근거인가entitlement_revoked_at이 action 생성 시각이 아니라 원장 반영 시각인가- API, web, export, workflow 등 주요 access layer를 나눠 확인했는가
- webhook 수신 성공과 권한 회수 완료를 별도 상태로 집계하는가
- 늦은 event나 재활성화가 이전 해지 상태를 덮지 않는가
delayed_revoke,early_revoke,stale_read,unknown을 별도 판정하는가- 검증 실패 행에 담당자와 다음 확인 시각이 있는가
- 실제 고객의 개인정보·결제정보·계약 원문을 리포트에 넣지 않았는가
핵심은 해지 버튼이나 billing 상태가 아니라 고객이 언제까지 무엇을 사용할 수 있었고, 언제부터 무엇이 닫혔는지 재현할 수 있는가입니다. cancellation_effective_at, access_end_at, entitlement_revoked_at을 분리하면 예약 해지의 정상 유지와 실제 회수 지연을 같은 오류로 집계하지 않을 수 있습니다.