모든 글
Deep dive8분 소요

환불 후 접근 권한 회수. Apple과 Google이 대신 처리해주지 않는 단계

Apple과 Google 모두, 고객이 구매한 것을 그대로 유지한 채로 환불이 나갈 수 있다. 접근 권한이 자동으로 제거되는 경우는 정확히 언제인지, 서버가 직접 처리해야 하는 경우는 언제인지, 그리고 대부분의 연동이 놓치는 단 하나의 알림이 무엇인지 여기서 정확히 설명한다.

밤에 책상 위에서 빛나는 스마트폰. 환불된 구매에 대한 접근 권한이 아직 회수되지 않은 순간을 보여준다.

핵심 요약

  • Google Play에서는 주문을 환불한다고 해서 그 자체로 사용자의 접근 권한이 제거되지는 않는다. 접근 권한은 Play Console에서 Remove entitlement가 체크되었을 때, 또는 API 호출에서 revoke=true가 전달되었을 때만 제거되며, 이는 기본값이 아니다.
  • revoke 옵션 없이 발급된 환불은 Voided Purchases API에 아예 나타나지 않는다. 이 피드만 읽는 회수 작업은 그런 주문을 전부 조용히 놓치게 된다.
  • Apple은 소모성이 아닌 항목과 구독에 대해서는 라이선스를 자동으로 회수할 수 있지만, 소모성 항목은 사용되는 순간 이미 제공된 것으로 간주되기 때문에 StoreKit에는 이를 회수할 방법이 없다. 잔액 차감은 서버가 직접 처리해야 한다.
  • Apple은 자신이 내준 환불을 취소할 수도 있다. REFUND_REVERSED 알림은 이전에 승인된 환불이 취소되었다는 뜻이며, revocationDate는 다시 null로 돌아가고, 제거했던 접근 권한이나 잔액은 모두 복원해야 한다.
  • Google의 voidedReason 필드는 remorse와 accidental_purchase부터 fraud, friendly_fraud, unacknowledged_purchase까지 아홉 가지 값을 가지므로, 같은 피드가 접근 권한을 회수해야 하는 시점뿐 아니라 어떤 회수가 어뷰징이고 어떤 것이 자사 연동의 버그인지도 알려준다.
  • 환불된 구매가 접근 권한을 계속 유지하는 하루하루는, 이미 돌려준 매출에 대해 컴퓨팅, 스토리지, API 호출 비용을 계속 지불하는 하루다.

환불을 승인하는 것과 접근 권한을 제거하는 것은 별개의 두 단계이며, 그중 하나만 자동으로 일어난다. 처리된 환불이 곧 회수된 구매라고 가정하는 개발자는 두 주요 스토어 모두에서, 서로 다른 방식과 이유로 틀렸다. 자동화가 정확히 어디에서 멈추는지 아는 것이야말로, 몇 달 뒤에야 실제로는 회수되지 않았다는 사실을 발견하는 대신 환불 후 접근 권한을 제대로 회수하는 데 필요한 일이다.

왜 환불은 기본적으로 접근 권한을 회수하지 않는가

Apple과 Google 모두 돈과 접근 권한을 서로 다른 두 가지 작업으로 취급한다. 돈 쪽은 스토어 정책의 영역이다. 환불이 승인되면 그것으로 확정되고 장부가 갱신된다. 접근 권한 쪽은 선택 사항이거나, 개발자의 몫이거나, 혹은 둘 다이며, 이는 플랫폼과 구매 유형에 따라 달라진다. 어느 스토어도 환불이 처리되는 순간 접근 권한이 사라지기를 원한다고 가정하지 않는다. 부분 환불, 선의의 크레딧, 고객 지원 차원의 조치처럼 제품을 아예 회수해서는 안 되는 정당한 경우가 많기 때문이다.

Google Play, 아무도 눈치채지 못하는 체크박스

Play Console에서 주문을 환불하는 것은 두 부분으로 이루어진 양식이다. 환불 금액을 정하고, 이어서 Remove entitlement를 체크할지 별도로 결정한다. 체크하지 않은 채로 두는 것이 대부분의 지원 담당자가 택하는 기본 경로인데, 대부분의 환불 요청이 고객이 구매한 것을 그대로 유지하는 선의의 조치이기 때문이다. 문제는 환불 확인 화면 어디에도, 이것이 최종적이길 의도했던 환불 이후에도 고객이 구독이나 인앱 항목을 계속 유지하게 되는 방법이라는 사실이 나와 있지 않다는 점이다.

API 경로에도 같은 함정이 있다

같은 선택이 코드상에도 존재한다. orders.refund 엔드포인트는 선택적인 revoke 쿼리 파라미터를 받는다. 이 값이 true이면 해당 항목에 대한 접근 권한은 즉시 종료되며, 정기 구독의 경우 앞으로의 모든 결제도 함께 취소된다. 이 파라미터를 생략하면 기본값인 false가 적용되어, 환불은 등록되고 구매 기록은 갱신되지만 사용자는 이전과 완전히 동일하게 제품을 계속 사용할 수 있다.

작업접근 권한 제거 여부Voided Purchases API에 표시되는가
Play Console 환불, Remove entitlement 미체크아니오아니오
Play Console 환불, Remove entitlement 체크예, 즉시
orders.refund API, revoke 파라미터 생략 (기본값 false)아니오아니오
orders.refund API, revoke=true예, 즉시

바로 그 마지막 행이 연동을 함정에 빠뜨리는 부분이다. Voided Purchases API는 Play Console을 수동으로 폴링하지 않고도 회수 파이프라인을 구축할 수 있도록 존재하지만, 실제로 revocation과 함께 무효화된 주문만 나열한다. 체크박스 없이, 또는 revoke=true 없이 처리된 환불은 버그가 아니라 설계상 그 피드에 보이지 않는다.

녹색 표시등이 줄지어 있는 가운데 빨간색 상태 표시등 하나가 켜져 있는 서버 랙, 조용히 내려진 회수 결정을 상징한다

voidedReason 필드 읽는 법 구매가 voided로 표시될 때, voidedReason 필드는 아홉 가지 값으로 그 이유를 알려준다. other, remorse, not_received, defective, accidental_purchase, fraud, friendly_fraud, chargeback, unacknowledged_purchase이다. 이는 단순한 기록 작업이 아니다. remorse와 accidental_purchase는 일반적인 환불이다. fraud, friendly_fraud, chargeback은 단순히 회수하고 잊어버리는 경로 대신 어뷰징 검토 큐로 보낼 가치가 있는데, 같은 계정이 통계적으로 반복할 가능성이 더 높기 때문이다.

이 피드를 직접 활용해 시스템을 구축한다면 운영상 중요한 세부 사항이 하나 있다. purchases.voidedpurchases.list는 하루 6,000쿼리, 임의의 30초 구간에 30쿼리로 제한된다. 촘촘한 루프로 폴링하는 설계는 이 한도에 빠르게 도달한다. 의도된 패턴은 지속적인 폴링이 아니라 페이지네이션 토큰을 사용한 예약된 풀이다.

Apple, App Store는 라이선스는 회수할 수 있지만 소모성 항목은 회수하지 못한다

Apple의 App Store Server Notifications V2는 환불이 확정되는 순간 REFUND 알림을 보낸다. 이는 소모성, 비소모성, 자동 갱신 구독, 비갱신 구독 모두 동일하다. 하지만 이 알림 이후 무슨 일이 일어나는지는 전적으로 구매 유형에 달려 있는데, StoreKit 자체의 회수 메커니즘은 네 가지 유형 중 두 가지만 다루기 때문이다.

소모성 항목이 예외인 이유

비소모성 항목이나 구독에는 Apple의 시스템이 꺼버릴 수 있는 지속적인 권한(entitlement)이 있다. 소모성 항목에는 그것이 없다. 한 번 사용되면 이미 제공된 것으로 취급되며, StoreKit 쪽에는 더 이상 회수할 것이 남아 있지 않다. 고객이 500코인을 구매하고 다 쓴 뒤 환불을 받았다면, Apple의 관점에서 그 코인은 이미 사라진 것이다. 그 코인이 여전히 남아 있는 유일한 장부는 바로 자사의 장부다. REFUND 알림은 해당 원본 트랜잭션 ID를 찾아 자사 기록에서 그 잔액을 차감하라는 단 하나의 신호다. Apple 쪽에서는 아무것도 대신 해주지 않는다.

알림의미서버가 해야 할 일
REFUNDApple이 소모성, 비소모성, 자동 갱신, 또는 비갱신 구매를 환불했다비소모성 및 구독 접근 권한을 즉시 회수한다. 소모성 잔액은 직접 차감한다
REFUND_DECLINEDApple이 고객의 환불 요청을 거부했다아무것도 할 필요 없다. 접근 권한은 전혀 위험에 처하지 않았다
REFUND_REVERSEDApple이 이미 승인했던 환불을 취소했다REFUND 알림 때문에 제거했던 접근 권한이나 잔액을 모두 복원한다
CONSUMPTION_REQUESTApple이 환불을 결정하기 전에 증거를 요구한다12시간 이내에 응답한다. 이는 REFUND 이전에 발생하며, 이후가 아니다

거의 아무도 처리하지 않는 알림

REFUND_REVERSED는 대부분의 연동이 완전히 빠뜨리는 알림이다. Apple이 자기 자신의 결정을 되돌리는 상황을 설명하는데, 이는 처음 설계할 때 대비하는 개발자가 거의 없는 경우이기 때문이다. 이 알림이 도착하면 해당 트랜잭션의 revocationDate는 다시 null로 돌아간다. Apple 입장에서는 그 구매가 다시 유효해진 것이다. 원래의 REFUND가 도착했을 때 서버가 entitlement를 삭제하거나, 구독 기록을 취소하거나, 소모성 잔액을 0으로 만들었다면, REFUND_REVERSED는 그 모든 것을 다시 되돌리라는 신호다.

이 간극을 메우는 방법, 단계별로

  • Google Play에서는, 고객이 접근 권한을 유지해야 하는 특정 지원 사례가 아니라면, Play Console을 통하든 API를 통하든 모든 환불 경로의 기본값을 revoke=true로 설정한다.
  • purchases.voidedpurchases.list는 촘촘한 루프가 아니라 페이지네이션 토큰을 사용해 일정에 따라 폴링하고, 하루 6,000건, 30초당 30건 이내로 유지한다.
  • voidedReason 값이 fraud, friendly_fraud, chargeback인 경우, 자동으로 회수하고 종료하는 대신 검토 큐로 보낸다.
  • Apple에서는 App Store Server Notifications V2를 구독하고, REFUND 알림이 해당 항목을 지목하는 즉시 비소모성 및 구독 접근 권한을 회수한다.
  • 모든 소모성 구매에 대해 원본 트랜잭션 ID를 저장해 두어, REFUND 알림이 도착했을 때 수동 조회 없이 차감할 정확한 잔액을 찾을 수 있도록 한다.
  • REFUND_REVERSED를 명시적으로 처리하고, 모든 회수를 해당 트랜잭션 ID나 주문 ID에 기록해 두어, 취소가 발생했을 때 제거되었던 것을 정확히 복원할 수 있도록 한다.

이 간극이 실제로 초래하는 비용

이것은 고객에 대한 공정성의 문제가 아니라, 돈이 사라진 뒤에도 계속 돌아가는 것이 무엇인지에 관한 문제다. 환불된 구독이 계속 API를 호출하고, 데이터를 저장하고, 컴퓨팅 자원을 소비하는 것은 도덕적인 문제가 아니라 비용 항목이다. 이미 매출을 돌려준 구매에 대해 인프라 비용을 계속 지불하고 있는 것이다. 스토어는 이 비용을 대신 추적해주지 않는데, 스토어 입장에서는 돈이 이동하는 순간 환불이 완료된 것이기 때문이다. 접근 권한 쪽을 관리하는 것은 전적으로 자사의 몫이다.

RefundHalt가 회수를 나중에 처리할 부수적인 작업이 아니라 환불 파이프라인의 일부로 다루는 이유도 여기에 있다. REFUND 이벤트와 접근 권한 변경은 하나의 동작으로 기록되며, REFUND_REVERSED 이벤트는 누군가 고객이 여전히 Apple이 돌려준 무언가로부터 차단되어 있다는 사실을 알아차리기를 기다리지 않고 자동으로 상태를 복원한다.

자주 묻는 질문

Google Play에서 구매를 환불하면 접근 권한이 자동으로 회수되나요.
아니다. 접근 권한은 Play Console에서 Remove entitlement가 체크되어 있거나 orders.refund API 호출에 revoke=true가 포함된 경우에만 제거된다. 둘 다 기본값이 아니므로, 환불이 처리되어도 고객이 계속 제품을 사용할 수 있다.
App Store에서 소모성 인앱 구매가 환불되면 어떻게 되나요.
Apple은 REFUND 알림을 보내지만, 소모성 항목은 사용되는 순간 이미 제공된 것으로 간주되기 때문에 StoreKit에는 이를 회수할 방법이 없다. 개발자의 서버가 원본 트랜잭션을 찾아 수동으로 잔액을 차감해야 한다.
REFUND_REVERSED 알림은 무엇이며, 별도의 처리가 필요한가요.
REFUND_REVERSED는 Apple이 이미 승인했던 환불을 취소했다는 뜻이다. revocationDate는 다시 null로 돌아가고 구매는 다시 유효해진다. 원래의 REFUND로 인해 제거되었던 접근 권한이나 잔액은 모두 복원해야 하지만, 대부분의 연동은 이 알림을 전혀 수신하지 않는다.
환불된 Google Play 주문이 왜 Voided Purchases API에 나타나지 않을 수 있나요.
이 API는 revocation과 함께 무효화된 주문만 나열한다. revoke 옵션 없이 환불이 처리되면 해당 주문은 이 엔드포인트에서 전혀 반환되지 않는다. 따라서 이 API에만 의존하는 파이프라인은 그런 환불을 완전히 놓치게 된다.
Google의 Voided Purchases API는 얼마나 자주 폴링할 수 있나요.
이 엔드포인트는 하루 6,000쿼리, 임의의 30초 구간에 30쿼리로 속도가 제한되어 있어, 지속적인 폴링이 아니라 페이지네이션 토큰을 사용한 예약된 풀을 위해 설계되었다.

출처 및 추가 자료

RefundHalt

App Store와 Google Play 환불 자동 처리

계속 읽기

다음 환불 요청은 이미 오고 있습니다.

이의를 제기하지 못한 환불에 관한 지원 이메일을 하나 더 읽는 시간이면 RefundHalt 설정을 마칠 수 있습니다.