두 플랫폼 모두에서 결제 취소와 접근 권한 종료는 별개의 이벤트이며, 이 중 하나만 자동으로 처리됩니다. 구글은 entitlement 제거 기능을 기본값이 off인 옵션 뒤에 숨겨둡니다. 애플은 라이선스는 처리하지만 누군가 이미 사용한 소모성 상품(consumable)에는 손을 댈 수 없습니다. 서버가 정리하지 못한 부분은 더 이상 존재하지 않는 매출을 기반으로 계속 실행됩니다.
핵심 요약
Play 주문을 환불해도 별도로 지정하지 않는 한 고객의 접근 권한은 그대로 유지됩니다. Play Console에서 "Remove entitlement"에 체크하거나, API 호출에 revoke=true를 포함해야 합니다. 둘 중 어느 것도 저절로 일어나지 않습니다.
revoke 옵션 없이 처리된 환불은 Voided Purchases API에 아예 나타나지 않습니다. 이 피드만을 기반으로 구축한 접근 권한 회수 작업은 이런 건들을 소리 없이 전부 놓치게 됩니다.
비소모성 상품과 구독의 라이선스는 애플이 대신 회수해 줄 수 있습니다. 반면 소모성 상품은 사용되는 순간 '전달 완료'로 간주되어 StoreKit이 회수할 대상 자체가 없어지므로, 잔액 차감은 여러분의 서버가 직접 처리해야 합니다.
애플은 때때로 자체적으로 승인한 환불을 취소하기도 합니다. REFUND_REVERSED 알림은 이미 승인된 환불이 취소되었다는 의미이며, revocationDate가 다시 null로 돌아가고, 여러분이 회수했던 모든 것을 원상 복구해야 합니다.
구글의 voidedReason에는 remorse, accidental_purchase부터 fraud, friendly_fraud, unacknowledged_purchase에 이르기까지 총 9가지 값이 존재하며, 이 하나의 피드가 접근 권한을 차단하라는 지시와 함께 어떤 건이 악용으로 의심되는지도 함께 알려줍니다.
환불된 구매 건이 접근 권한을 계속 유지하는 한, 여러분은 이미 구매자에게 돌려준 매출로 해당 건의 컴퓨팅 자원, 스토리지, API 호출 비용을 계속 부담하고 있는 셈입니다.
환불 후에도 기본적으로 접근 권한이 유지되는 이유
두 회사 모두 결제와 entitlement(이용 권한)를 별개의 작업으로 취급합니다. 결제는 스토어 정책을 따르며, 승인되는 즉시 환불이 확정되고 장부에 반영됩니다. entitlement는 다른 문제로, 경우에 따라 선택 사항이거나, 여러분이 직접 처리해야 하거나, 플랫폼과 상품 유형에 따라 두 경우 모두에 해당하기도 합니다. 어느 스토어도 돈을 돌려주는 것이 곧 접근 권한 종료를 의미한다고 전제하지 않습니다. 부분 환불, 선의의 크레딧, 고객 지원 차원의 조치 등 수많은 정당한 상황에서는 고객이 구매한 것을 계속 보유하도록 의도되어 있기 때문입니다.
Google Play: 눈에 띄지 않는 체크박스
Play Console는 환불 과정을 두 가지 결정으로 나눕니다. 먼저 금액을 정하고, 그다음 완전히 별도로 "Remove entitlement"에 체크할지 여부를 정합니다. 대부분의 지원 담당자는 이를 건드리지 않는데, 실제로 대부분의 환불 요청이 선의에서 비롯되며 고객이 구매한 것을 계속 보유하도록 되어 있다는 점을 고려하면 합리적인 선택입니다. 확인 화면이 전혀 언급하지 않는 사실은, 바로 이 건드리지 않은 체크박스로 인해 여러분이 최종적이라고 생각했던 환불 이후에도 누군가 구독이나 인앱 아이템을 계속 보유한 채 떠날 수 있다는 점입니다.
API에도 똑같은 함정이 있습니다
코드 레벨에서도 동일한 갈림길이 존재합니다. orders.refund 엔드포인트에는 선택적인 revoke 쿼리 파라미터가 있으며, 이를 true로 설정하면 즉시 해당 아이템에 대한 접근 권한이 종료되고, 정기 구독인 경우 향후의 모든 결제도 함께 취소됩니다. 이 파라미터를 생략하면 기본값인 false가 적용되어, 환불은 처리되고 구매 기록은 변경되지만 고객은 이전과 완전히 동일하게 이용을 계속합니다.
Play Console에서 "Remove entitlement"에 체크하지 않고 환불하면 접근 권한이 그대로 유지됩니다. 해당 주문은 Voided Purchases API에도 전혀 나타나지 않습니다.
Play Console에서 "Remove entitlement"에 체크하고 환불하면 접근 권한이 즉시 종료됩니다. 해당 주문은 Voided Purchases API에 나타납니다.
orders.refund를 호출하면서 revoke 파라미터를 생략하면 기본값인 false가 적용되어 접근 권한이 그대로 유지됩니다. 이번에도 Voided Purchases API에는 아무것도 나타나지 않습니다.
orders.refund를 revoke=true로 호출하면 접근 권한이 즉시 종료되며, 해당 주문이 Voided Purchases API에 나타납니다.
Voided Purchases API만 읽는 접근 권한 회수 작업은 revoke 플래그 없이 처리된 환불을 절대 접하지 못합니다. 기록이 늦게 오는 것이 아니라, 아예 오지 않는 것이며, 해당 주문은 마치 환불이 일어나지 않은 것처럼 이 피드 바깥에 남아 있게 됩니다.
바로 이 마지막 경우가 연동을 깨뜨리는 원인입니다. Play Console을 수동으로 지켜보지 않고도 접근 권한 회수 파이프라인을 구축하는 것이 바로 Voided Purchases API의 존재 이유이지만, 이 API의 범위는 실제로 revocation(권한 회수)과 함께 무효 처리된 주문으로 한정됩니다. 체크박스 없이, 또는 revoke=true 없이 처리된 환불은 결함이 아니라 설계상 이 피드에서 보이지 않는 것입니다.
voidedReason 제대로 이해하기
무효 처리된 구매 건에는 사유를 설명하는 voidedReason이 함께 전달되며, 가능한 값은 other, remorse, not_received, defective, accidental_purchase, fraud, friendly_fraud, chargeback, unacknowledged_purchase 등 총 9가지입니다. 이를 단순한 기록용 데이터로만 취급한다면 그 가치를 낭비하는 셈입니다. remorse와 accidental_purchase는 특별할 것 없는 환불을 나타냅니다. 반면 fraud, friendly_fraud, chargeback은 자동으로 권한을 회수하고 종료 처리하기보다 부정행위 검토 대기열로 보내야 할 사유입니다. 이런 값을 발생시키는 계정은 통계적으로 재발 비율이 더 높기 때문입니다.
이 피드를 직접 활용해 시스템을 구축하는 사람이라면 반드시 알아야 할 운영상의 숫자가 하나 있습니다. purchases.voidedpurchases.list는 하루 6,000건, 그리고 30초 구간당 30건의 쿼리만 허용합니다. 촘촘한 폴링 루프를 사용하면 이 한도가 금방 소진됩니다. 페이지네이션 토큰을 활용한 예약 실행(scheduled pull)이 원래 의도된 설계입니다.
애플: 라이선스는 회수할 수 있지만 소모성 상품은 그렇지 않습니다
App Store Server Notifications V2는 환불이 확정되는 즉시 REFUND 알림을 전송하며, 이는 소모성 상품, 비소모성 상품, 자동 갱신 구독, 비갱신 구독을 구분 없이 모두 포함합니다. 하지만 그다음에 해야 할 일은 상품 유형에 따라 완전히 달라지는데, 이 네 가지 유형 중 StoreKit 자체의 revocation(권한 회수) 체계에 속하는 것은 단 두 가지뿐이기 때문입니다.
소모성 상품이 이 체계에서 제외되는 이유
지속적인 entitlement는 비소모성 상품과 구독에만 해당하며, 애플의 시스템이 이를 꺼버릴 수 있습니다. 소모성 상품에는 그런 것이 없습니다. 소모성 상품은 사용되는 순간 전달 완료로 표시되어, StoreKit 입장에서는 회수할 대상이 남지 않습니다. 코인 500개를 구매해 모두 사용한 뒤 환불을 받은 고객이 있다면, 애플의 관점에서 이 고객은 이미 모든 것을 받은 상태입니다. 그 코인이 여전히 존재하는 것으로 기록된 장부는 오직 여러분의 장부뿐입니다. 이 REFUND 알림은 일치하는 원본 거래 ID(original transaction ID)를 찾아 잔액을 직접 차감하라는 유일한 신호이며, 애플 쪽에서는 아무 것도 대신해주지 않기 때문입니다.
REFUND는 소모성, 비소모성, 자동 갱신, 비갱신 구매 건 중 하나가 환불되었다는 의미입니다. 비소모성 상품과 구독의 접근 권한은 즉시 회수하고, 소모성 잔액은 직접 차감해야 합니다.
REFUND_DECLINED는 애플이 고객의 환불 요청을 거부했다는 의미입니다. 접근 권한이 애초에 위협받지 않았으므로 아무것도 할 필요가 없습니다.
REFUND_REVERSED는 애플이 이미 승인했던 환불이 취소되었다는 의미입니다. 원래의 REFUND 알림으로 인해 회수했던 것을 모두 원상 복구해야 합니다.
CONSUMPTION_REQUEST는 애플이 결정을 내리기 전에 여러분의 증빙 자료를 원한다는 의미입니다. 12시간 이내에 응답해야 하며, 이 알림은 REFUND 이후가 아니라 이전에 도착한다는 점에 유의하세요.
거의 모두가 놓치는 알림
REFUND_REVERSED는 연동 구현 시 흔히 빠뜨리는 알림인데, 이는 애플이 자신의 결정을 스스로 번복하는 경우를 나타내며, 처음부터 이런 상황을 염두에 두고 설계하는 경우는 거의 없기 때문입니다. 이 알림이 도착하면 revocationDate가 다시 null로 돌아가며, 이는 애플이 이제 해당 구매를 다시 완전히 유효한 것으로 간주한다는 뜻입니다. 원래의 REFUND 처리 시 서버가 수행했던 작업들, 즉 entitlement 삭제, 구독 기록 취소, 소모성 잔액 0으로 만들기 등은 모두 되돌려야 합니다.
취소된 환불을 새로운 구매와 비슷할 것이라고 기대하지 마세요. 그렇지 않기 때문입니다. 적용된 제재, 회수된 접근 권한, 사용 완료로 표시된 잔액, REFUND_REVERSED는 바로 이런 것들을 정확히 되돌리라는 지시이며, 이를 다시 상기시켜 줄 후속 알림은 나중에 오지 않습니다.
단계별로 공백 메우기
Play Console과 API를 포함한 모든 환불 경로에서 revoke=true를 기본값으로 설정하고, 고객이 접근 권한을 유지해야 할 특별한 사유가 있는 지원 케이스에 한해서만 예외를 적용하세요.
purchases.voidedpurchases.list는 촘촘한 루프 대신 페이지네이션 토큰을 활용해 예약된 일정으로 호출하고, 하루 6,000건, 30초당 30건 이하로 유지하세요.
voidedReason 값이 fraud, friendly_fraud, chargeback인 경우에는 자동으로 권한을 회수하고 종료 처리하는 대신 검토 대기열로 보내세요.
애플에서는 App Store Server Notifications V2를 구독하고, REFUND 알림이 해당 건을 식별하는 즉시 비소모성 상품과 구독의 접근 권한을 회수하세요.
모든 소모성 상품 구매 건에 원본 거래 ID(original transaction ID)를 저장해 두어, REFUND 알림이 도착했을 때 누군가 수동으로 찾아보지 않아도 차감할 정확한 잔액을 바로 파악할 수 있도록 하세요.
REFUND_REVERSED를 의도적으로 구현하고, 모든 접근 권한 회수 내역을 해당 거래 ID 또는 주문 ID와 함께 기록해, 취소가 발생했을 때 회수했던 것을 정확히 복구할 수 있도록 하세요.
이 공백이 실제로 초래하는 비용
여기서 문제는 고객에 대한 공정성이 아닙니다. 문제는 매출이 사라진 뒤에도 계속 돌아가는 것들입니다. 환불된 구독이 여전히 여러분의 API를 호출하고, 여전히 데이터를 보유하고, 여전히 컴퓨팅 자원을 소모하는 것은 도덕적인 문제가 전혀 아닙니다. 이는 청구서의 문제입니다. 인프라 비용은 이미 돌려준 매출로 구매된 건에 대해 지불된 것입니다. 어느 스토어도 이 비용을 여러분을 대신해 추적해주지 않습니다. 그들의 입장에서는 돈이 이동하는 순간 환불이 끝난 것이기 때문입니다. 접근 권한 쪽을 관리하는 것은 전적으로 여러분이 부담해야 할 몫입니다.
그래서 Refund Sensor는 접근 권한 회수를 환불 파이프라인에 나중에 덧붙이는 것이 아니라 그 일부로 취급합니다. REFUND 이벤트와 그에 해당하는 접근 권한 변경은 하나의 단일 작업으로 기록되며, REFUND_REVERSED 이벤트는 누군가 며칠 전 애플이 이미 돌려준 것을 고객이 여전히 이용하지 못하고 있다는 사실을 알아차릴 때까지 기다리지 않고 스스로 이전 상태를 복원합니다.
이 규칙들이 문서화된 공식 자료
자주 묻는 질문
그렇지 않습니다. 접근 권한은 Play Console에서 "Remove entitlement"에 체크했거나 orders.refund 호출에 revoke=true가 포함된 경우에만 종료됩니다. 둘 다 기본값이 아니기 때문에, 환불은 처리되었지만 고객은 계속 제품을 사용하는 경우가 흔히 발생합니다.
REFUND 알림은 도착하지만, 소모성 상품은 사용되는 순간 전달 완료로 간주되기 때문에 StoreKit이 이를 회수할 수 없습니다. 원본 거래를 찾아 잔액을 차감하는 작업은 여러분의 서버가 직접 수행해야 합니다.
애플이 이미 승인했던 환불이 취소되었다는 의미입니다. revocationDate가 다시 null로 돌아가고, 해당 구매는 다시 유효한 것으로 간주됩니다. 원래의 REFUND로 인해 회수했던 접근 권한이나 잔액은 모두 복구해야 하며, 실제로 많은 연동 시스템이 이 알림 자체를 아예 구독하지 않습니다.
이 엔드포인트는 revocation(권한 회수)과 함께 무효 처리된 주문만 반환하기 때문입니다. revoke 옵션 없이 처리된 환불은 포함되지 않으므로, 이 피드에만 의존하는 파이프라인은 해당 건들을 전부 놓치게 됩니다.
하루 6,000건의 쿼리, 그리고 30초 구간당 최대 30건으로 제한됩니다. 이 엔드포인트는 지속적인 폴링이 아니라 페이지네이션 토큰을 활용한 예약 실행(scheduled pull)을 위해 설계되었습니다.



