Apple과 Google 모두 같은 환불을 당신의 서버로 두 번 이상 전달할 수 있으며, 매번 처리한다면 중복된 환불 알림은 당신에게 비용을 치르게 합니다
Apple은 환불 알림을 최대 다섯 번 재시도하고 Google Play는 Pub/Sub의 최소 한 번 전달에 실려 있어, 같은 환불이 당신의 서버로 두 번 이상 도달할 수 있습니다. 여기서는 잔액을 이중으로 차감하거나 API 할당량을 두 번 소진하지 않으면서 중복된 환불 알림을 처리하는 방법을 설명합니다.

핵심 요약
- 당신의 서버가 200에서 206 사이의 HTTP 상태로 응답하지 않는 한, Apple은 App Store Server Notification V2를 다섯 번 재시도합니다. 직전 시도로부터 1, 12, 24, 48, 72시간 후입니다. 첫 시도를 포함하면 한 건의 환불이 최대 여섯 번 도달할 수 있습니다.
- 모든 진짜 Apple 재시도는 같은 notificationUUID를 담고 있으므로, 그 필드가, 거래 id가 아니라, 당신의 중복 제거 키입니다.
- Google Play의 Real-time Developer Notifications는 Cloud Pub/Sub에 실려 있고, 그것은 최소 한 번 전달을 보장하며 순서는 보장하지 않으므로, 같은 메시지가 두 번 도착하거나 순서가 뒤바뀌어 도착할 수 있습니다. Google은 무언가를 처리하기 전에 messageId의 고유성을 확인하라고 말합니다.
- Apple의 주기적인 CONSUMPTION_REQUEST 알림은 재시도가 아닙니다. Apple은 열려 있는 환불 창 동안 새로운 것을 계속 보내며 각각 다른 notificationUUID를 갖기 때문에, notificationUUID로 중복 제거하면 그 하나하나를 올바르게 남깁니다.
- 같은 Apple transactionId가 두 개 이상의 결정을 담을 수 있습니다. 예를 들어 REFUND_DECLINED가 있은 뒤 나중에 REFUND가 이어질 수 있으므로, 거래 id만으로 중복 제거하면 당신에게 필요했던 별개의 이벤트를 버리게 됩니다.
- 4xx나 5xx를 반환해 중복을 거부하면 스토어가 그것을 재시도하게 만들 뿐입니다. 당신 자신의 데이터베이스 내부에서 중복을 제거하고 항상 성공 상태를 반환하세요.
- 멱등하지 않은 환불 핸들러는 두 번째 전달에서 이중으로 처리합니다. 잔액을 두 번 차감하고, 지급을 두 번 되돌리고, 또는 이미 종결한 환불을 다시 확인하여 과금 대상인 Play Developer와 App Store Server API 할당량을 소진합니다.
당신의 서버는 같은 환불 이벤트를 두 번 이상 받게 되며, 두 스토어 모두 일부러 그렇게 설계했습니다. 당신의 엔드포인트가 깔끔하게 응답하지 않을 때 Apple은 App Store Server Notification을 최대 다섯 번 재시도합니다. Google Play는 Real-time Developer Notifications를 Cloud Pub/Sub를 통해 전달하며, 그것은 최소 한 번 전달을 약속하고 순서에 대해서는 아무것도 약속하지 않습니다. 그래서 문제는 중복이 도착하는지 여부가 결코 아닙니다. 문제는 당신의 코드가 같은 환불을 두 번째로 볼 때 무엇을 하는가입니다. 그것을 잘못하면 잔액을 두 번 차감하고, 지급을 두 번 되돌리고, 또는 이미 종결한 환불을 다시 확인하여 과금 대상인 API 할당량을 소진합니다. 여기서는 중복된 환불 알림이 실제로 어떻게 당신에게 도달하는지, 어떤 반복이 진짜 중복이고 어떤 것이 그렇게 보일 뿐인지, 그리고 두 번째 전달이 공짜가 되도록 그것들을 어떻게 처리하는지 설명합니다.
환불 알림은 최소 한 번 전달되며, 그것은 정확히 한 번과 같지 않습니다
두 스토어 모두 전달된 알림을 쏘고 잊는 한 발이 아니라, 이행하려고 계속 시도하는 약속으로 취급합니다. 그것은 신뢰성에는 좋습니다. 배포 중에 놓친 알림도 나중에 당신에게 도달하기 때문입니다. 그것은 정확성에는 함정입니다. 당신이 결국 이벤트를 받도록 보장하는 그 메커니즘이, 때때로 당신이 그것을 두 번 받도록도 보장하기 때문입니다. 당신의 핸들러는 멱등해야 합니다. 즉, 한 건의 환불의 두 번째와 세 번째 전달은 첫 번째가 이미 바꾸지 않은 것을 아무것도 바꾸지 않아야 합니다.
Apple은 사흘에 걸쳐 다섯 번 재시도합니다
Apple이 App Store Server Notification V2를 보낼 때, 그것은 당신의 서버가 200에서 206 범위의 HTTP 상태로 응답하기를 기대합니다. 그 밖의 무엇이든, 4xx이든 5xx이든, 그것은 Apple에게 전달 실패를 알리고, Apple은 재시도합니다. 그 일정은 고정되어 있습니다. 다섯 번의 재시도로, 직전 시도로부터 1, 12, 24, 48, 72시간 후입니다. 첫 시도를 포함하면 한 건의 환불 이벤트는 대략 일주일에 걸쳐 최대 여섯 번 도달할 수 있습니다. 그 재시도 하나하나가 같은 notificationUUID를 담습니다. 그 필드가 당신의 중복 제거 키입니다. 이미 어떤 notificationUUID를 기록했다면, 당신이 들고 있는 그 전달은 반복이며, 올바른 응답은 새로운 것을 아무것도 저장하지 않고 그래도 200을 반환하는 것입니다.
Google Play는 Pub/Sub에 실려 있으며, 최소 한 번을 약속하고 순서에 대해서는 아무 말도 하지 않습니다
Google Play의 Real-time Developer Notifications는 Cloud Pub/Sub 토픽으로 발행됩니다. Pub/Sub의 전달 보장은 최소 한 번이며, 순서 보장은 전혀 하지 않습니다. 그것은 같은 메시지가 당신의 엔드포인트로 두 번 이상 전달될 수 있고, 같은 구매에 대한 두 메시지가 순서가 뒤바뀌어 도착할 수 있다는 뜻입니다. Google 자신의 지침은 명확합니다. base64 data 필드를 풀고, messageId를 읽고, 무언가를 처리하기 전에 그것을 이전에 본 적이 없음을 확인하라는 것입니다. 중복된 messageId는 당신이 건너뛰는 반복입니다. 한 구매에 대한 서로 다른 두 알림은 그래도 같은 레코드에 안착해야 하므로, 저장하는 상태를 purchaseToken에도 키로 두고, 나중 이벤트가 앞선 이벤트가 만든 행을 갱신하도록 하세요.
| 플랫폼 | 전달 모델 | 중복 제거 키 | 성공 신호 | 확인하지 않는 경우 |
|---|---|---|---|---|
| App Store Server Notifications V2 | 최대 6회 시도: 첫 회에 더해 1, 12, 24, 48, 72시간의 5회 재시도 | notificationUUID | HTTP 200에서 206 | Apple은 고정 일정으로 재시도한 뒤 멈춤 |
| Google Play RTDN, Pub/Sub 경유 | 최소 한 번, 순서 보장 없음 | Pub/Sub messageId, purchaseToken으로 엔티티 키 지정 | push에 대해 HTTP 200, 또는 명시적 ack | ack 기한이 지나면 Pub/Sub가 재전송 |
중복이 아닌 반복
당신이 본 적 있는 것처럼 보이는 알림이 모두 재시도인 것은 아닙니다. Apple의 두 가지 동작은 한 구매를 공유하지만 각각 처리되어야 하는 정말로 새로운 이벤트를 보내며, 순진한 중복 제거로 그것들을 뭉개면 당신에게 필요했던 정보를 버리게 됩니다.
Apple은 재시도가 아니라 새로운 CONSUMPTION_REQUEST를 보냅니다
소비형 상품에 대한 열려 있는 환불 요청 동안, Apple은 하나의 CONSUMPTION_REQUEST를 보내고 기다리지 않습니다. 환불이 종결될 때까지 열려 있는 환불 창 전체에 걸쳐 주기적으로 새로운 것을 보냅니다. Apple 직원은 이것들이 재시도가 아님을 확인했으며, 그 단서는 당신이 중복 제거에 쓰는 필드에 있습니다. 새로운 CONSUMPTION_REQUEST 각각은 다른 notificationUUID를 담습니다. 그래서 notificationUUID에 키를 둔 중복 제거는 자동으로 옳은 일을 합니다. 진짜 재시도는 뭉개고, 별개의 프롬프트는 하나하나 남깁니다. 당신이 결코 해서는 안 되는 것은 거래 id와 알림 유형으로 중복을 제거하는 것입니다. 그것은 첫 번째 이후의 모든 CONSUMPTION_REQUEST를 침묵시켜, 버려진 것들에 대해 12시간의 증거 창을 당신에게 잃게 하기 때문입니다.
한 거래가 두 개 이상의 결정을 담을 수 있습니다
단일 transactionId는 그 생애에 걸쳐 두 개 이상의 환불 결과를 낳을 수 있습니다. Apple은 REFUND_DECLINED를 보내고 나중에 같은 거래에 대해 REFUND를 보낼 수 있으며, 개발자들은 하나의 거래 id에 대해 환불 관련 알림을 세 개 이상 받았다고 보고합니다. 각각은 자체 notificationUUID를 가진 별개의 이벤트입니다. 당신의 중복 제거 키가 거래 id라면, 두 번째 결정은 첫 번째의 중복처럼 보이고, 당신은 환불이 결국 승인되었다는 것을 결코 알지 못합니다. 거래 id는 이벤트를 묶습니다. 그것들을 식별하지는 않습니다.

중복이 실제로 당신에게 치르게 하는 비용
환불 알림은 상태 표시등이 아닙니다. 그것은 실제 행동을 촉발합니다. 당신은 접근 권한을 취소하고, 소비형 잔액을 차감하고, 크리에이터 지급을 되돌리고, 상태를 확인하려고 App Store Server API 또는 Play Developer API를 호출합니다. 그중 무엇이든 중복에 대해 두 번째로 실행하면 그 비용은 진짜입니다.
돈의 흐름을 따라가 봅시다. 접근 권한을 두 번 취소하는 것은 무해합니다. 접근 권한은 이미 사라졌기 때문입니다. 잔액을 두 번 차감하는 것은 그렇지 않습니다. 코인 한 묶음을 사고 환불한 사용자는 당신의 지원팀이 나중에 손으로 되돌려야 하는 음수 잔액까지 내몰릴 수 있습니다. 지급을 두 번 되돌리는 것은 당신이 이미 한 번 돌려준 돈을 도로 회수하는 것이며, 이제 당신은 크리에이터에게 사과와 정정을 빚집니다. 그리고 스토어 API에 대해 재처리하는 모든 중복은 Google이 명시적으로 지키라고 경고하는 할당량을 쓰므로, 장애 중의 한바탕 Pub/Sub 재전송은 당신이 가장 감당하기 어려운 바로 그날 당신을 속도 제한으로 밀어넣을 수 있습니다.
환불 증거 창은 Apple 쪽에서 판돈을 키웁니다. 순진한 중복 제거가 Apple이 열려 있는 환불 창 동안 보내는 반복된 CONSUMPTION_REQUEST를 침묵시키면, 당신은 응답해야 했던 그것을 놓칠 수 있으며, 12시간 안에 응답하지 않은 CONSUMPTION_REQUEST는 Apple이 흔히 기본으로 승인하는 환불입니다. 그것은 이중 청구가 아닙니다. 그것은 잃어버린 판매에 더해, 당신이 그 구매를 전달하며 이미 쓴 연산, API 호출, 저장, 지급이며, 그중 무엇도 환불은 돌려주지 않습니다.
| 실패 모드 | 무엇이 잘못되는가 | 무슨 비용이 드는가 |
|---|---|---|
| 중복된 REFUND에 잔액을 다시 차감 | 사용자의 소비형 잔액이 음수가 됨 | 대사를 위한 수작업 지원 시간과 나쁜 고객 경험 |
| 지급을 두 번 되돌림 | 당신은 이미 한 번 돌려준 돈을 도로 회수함 | 크리에이터 정정과 회계 정리 |
| 스토어 API에 대해 재처리 | 중복 호출이 Play Developer 또는 App Store Server API 할당량을 소진 | 재전송을 유발한 장애 중의 속도 제한 |
| CONSUMPTION_REQUEST를 과도하게 중복 제거 | 별개의 환불 프롬프트를 가짜 중복으로 버림 | 12시간 창을 놓쳐 Apple이 기본으로 환불을 승인 |
이중 처리 없이 중복된 환불 알림을 처리하는 방법
패턴은 두 스토어에서 같고, 키만 다릅니다. 전달을 기록하고, 처리하기 전에 키를 확인하고, 한 번만 처리하고, 그리고 항상 스토어에게 받았다고 알리세요.
- 거래가 아니라 스토어의 전달 id로 중복을 제거하세요. Apple에는
notificationUUID, Google Play에는 Pub/SubmessageId를 쓰세요. 동시에 발생한 중복이 이중 처리 대신 경쟁에서 지도록, 고유 제약과 함께 저장하세요. - 다운스트림 행동을 그 자체의 관점에서 멱등하게 만드세요. 전달 id에 키를 두면 재처리는 멈추지만, 취소, 차감, 되돌림이 먼저 현재 상태를 확인하고 두 번 실행해도 안전하도록 그 효과 자체도 작성하세요.
- 먼저 영속화하고, 그다음 확인하세요. 200을 반환하거나 Pub/Sub 메시지를 ack하기 전에 이벤트를 당신의 데이터베이스에 기록하세요. 먼저 ack했는데 기록이 실패하면, 스토어는 그 메시지를 전달된 것으로 여겨 다시는 보내지 않으며, 이제 당신은 그것을 영영 잃었습니다.
- 항상 성공 상태를 반환하세요. 중복에 대해서도요. Apple에는 200에서 206, Google에는 Pub/Sub push에 대해 200입니다. 반복을 오류로 거부하면 스토어가 그것을 재시도하게 만들 뿐입니다.
- 엔티티로 묶고, 이벤트로 식별하세요. 저장하는 구매 상태를
purchaseToken또는originalTransactionId에 키로 두어 순서가 뒤바뀐 전달이 한 행을 갱신하게 하되, 각notificationUUID나messageId는 그 자체의 이벤트로 취급하세요. 한 구매가 정당하게 여럿을 낳기 때문입니다.
당신의 환불 webhook을 신뢰하기 전의 짧은 체크리스트
- Apple 전달은
notificationUUID로 중복 제거되며, 반복은 새로운 것을 아무것도 기록하지 않지만 그래도 200을 반환한다. - Google Play 전달은 Pub/Sub
messageId로 중복 제거되며, 어떤 처리보다도 먼저 확인된다. - 구매 상태는
purchaseToken또는originalTransactionId에 키가 지정되어, 순서가 뒤바뀐 이벤트가 한 레코드에 안착한다. - 모든 환불 부작용, 취소, 차감, 되돌림은 두 번 이상 실행해도 안전하다.
- 당신의 핸들러는 확인하기 전에 이벤트를 기록하며, 결코 그 뒤가 아니다.
- 반복되는 CONSUMPTION_REQUEST는 중복이 아니라 별개의 프롬프트로 취급되어, 열려 있는 환불 창이 하나도 버려지지 않는다.
중복을 일부러 당신 자신의 webhook으로 통과시키고, 그것이 두 번째에 아무것도 바꾸지 않는 것을 지켜보세요. 그것이 테스트의 전부입니다. 두 번 두들겨도 안전한 환불 핸들러는, 스토어가 여섯 번 두들기기로 정한 그 순간부터 당신이 걱정을 그만둘 수 있는 종류입니다.
자주 묻는 질문
- 왜 제 서버는 같은 App Store 환불 알림을 두 번 이상 받나요?
- 당신의 서버가 200에서 206 사이의 HTTP 상태로 응답하지 않는 한, Apple이 App Store Server Notification V2를 최대 다섯 번, 직전 시도로부터 1, 12, 24, 48, 72시간 후에 재시도하기 때문입니다. 모든 재시도는 같은 notificationUUID를 담으므로, 그것을 알아보고 건너뛸 수 있습니다.
- App Store Server Notifications를 중복 제거하려면 어떤 필드를 써야 하나요?
- notificationUUID를 쓰세요. 진짜 재시도는 항상 같은 notificationUUID를 반복하는 반면, 정말로 새로운 모든 이벤트는, 새로운 CONSUMPTION_REQUEST 하나하나를 포함해, 서로 다른 것을 받으므로, notificationUUID로 중복 제거하면 별개의 이벤트를 버리지 않고 반복을 건너뜁니다.
- 반복되는 CONSUMPTION_REQUEST 알림은 제가 무시해야 할 중복인가요?
- 아니요. Apple은 열려 있는 환불 창 동안 새로운 CONSUMPTION_REQUEST 알림을 주기적으로 보내며, Apple 직원은 이것들이 재시도가 아님을 확인합니다. 각각 자체 notificationUUID를 가지므로, 하나도 빠짐없이 처리하세요. 그것들을 버리면 Apple이 응답하도록 주는 12시간 창을 놓칠 위험이 있습니다.
- Google Play의 Real-time Developer Notifications는 어떻게 중복 제거하나요?
- 각 알림에서 Pub/Sub messageId를 읽고, 처리하기 전에 이미 처리한 것들과 대조해 확인하세요. Pub/Sub는 최소 한 번 전달하며 같은 메시지를 두 번 이상 보낼 수 있기 때문입니다. Google은 중복 처리와 API 할당량 낭비를 피하기 위해 이것을 명시적으로 권장합니다.
- 중복된 환불 알림을 거부하려고 오류를 반환해야 하나요?
- 아니요. 4xx나 5xx를 반환하면 스토어에게 전달 실패를 알리므로 어쨌든 재시도합니다. 당신 자신의 데이터베이스 내부에서 중복을 제거하고 항상 성공 상태를 반환하세요. Apple에는 HTTP 200에서 206, Google Play push에는 200 확인입니다.
출처 및 추가 자료
- Apple Developer: App Store Server Notifications V2
- Apple Developer: Responding to App Store Server Notifications
- Apple Developer: notificationUUID
- Apple Developer Forums: App Store Server Notifications V2 retry schedule and CONSUMPTION_REQUEST clarification
- Android Developers: Real-time developer notifications reference
- Android Developers: Purchase lifecycle and RTDNs
- Google Cloud: Pub/Sub subscriber and at-least-once delivery
RefundHalt
App Store와 Google Play 환불 자동 처리
계속 읽기
Family Sharing 환불은 결제 한 건을 되돌리지만, 다른 다섯 명은 여전히 앱을 사용할 수 있습니다. 그들을 차단할 수 있는 것은 오직 당신의 서버뿐입니다
Family Sharing 환불은 결제 한 건을 되돌리지만, 최대 다섯 명의 가족 구성원이 유료 기능을 계속 사용할 수 있습니다. Apple은 REVOKE를 보내고, 그 접근을 종료하는 것은 당신의 서버가 할 일이라고 기대합니다. 가족 공유 환불이 어떻게 작동하는지, 그리고 한 건이 얼마의 비용이 드는지 설명합니다.
환불 처리는 조용히 망가진다. 그러니 실제 고객이 먼저 겪기 전에 sandbox에서 인앱 구매 환불을 테스트하라
환불 처리는 고객이 이미 떠난 뒤에야 실행되므로, 그 안의 버그는 실제로 돈을 잃기 전까지 보이지 않은 채로 남습니다. 두 스토어 모두 먼저 테스트 환경에서 환불을 발생시킬 수 있게 해줍니다. 여기서는 환불이 진짜가 되기 전에 App Store와 Google Play에서 인앱 구매 환불을 테스트하는 방법을 설명합니다.