환불을 처리하기 전에 App Store 서버 알림을 검증하세요. 누구나 웹훅 URL로 요청을 보낼 수 있습니다
환불 알림은 서명을 확인하기 전까지는 하나의 HTTPS 요청에 불과합니다. App Store 서버 알림과 Google Play RTDN 메시지를 검증하는 방법, 각 스토어가 무엇에 서명하는지, 그리고 위조된 환불이나 잘못 거부된 환불이 어떤 비용을 초래하는지 설명합니다.

핵심 요약
- Apple은 모든 버전 2 App Store 서버 알림을 signedPayload로 보냅니다. 이는 헤더, 페이로드, 서명의 세 Base64URL 부분으로 이루어진 JSON Web Signature입니다.
- App Store 서버 알림의 JWS 헤더에는 인증서 세 개로 이루어진 x5c 인증서 체인이 들어 있습니다. 서명용 리프 인증서, Apple Worldwide Developer Relations 확장 1.2.840.113635.100.6.2.1을 가진 Apple 중간 인증서, 그리고 Apple 루트 인증서입니다.
- Apple의 App Store Server Library는 SignedDataVerifier로 알림을 검증합니다. 인증서 체인, 번들 ID, environment, 그리고 프로덕션에서는 appAppleId를 확인하며, 인증서 폐기 여부를 온라인으로 확인할 수도 있습니다.
- Google Play 실시간 개발자 알림은 Google Play가 서명하지 않습니다. 푸시 구독에서는 Pub/Sub가 Authorization 헤더에 서명된 OpenID Connect 토큰을 첨부할 수 있으며, 서버는 그 서명과 audience, email 클레임을 확인합니다.
- Google에 따르면 RTDN은 구매 상태가 바뀌었다는 사실만 알려 줍니다. 따라서 어떤 환불 알림이든 안전하게 대응하는 방법은 접근 권한을 회수하기 전에 Google Play Developer API로 확인하는 것입니다.
- Apple은 17.0.0.0/8 서브넷에서 App Store 서버 알림을 보내지만, IP 허용 목록은 엔드포인트에 접근할 수 있는 대상을 좁힐 뿐입니다. 메시지가 Apple에서 왔음을 증명하는 것은 서명입니다.
- 진짜 메시지를 잘못 거부하는 검증기는 위조 메시지를 받아들이는 검증기만큼 비용이 큽니다. 놓친 CONSUMPTION_REQUEST의 응답 기한은 12시간이고, Google Play 지불 거절 검토의 기한은 24시간이기 때문입니다.
웹훅 URL을 알게 된 사람이라면 누구나 그곳으로 환불을 보낼 수 있습니다. 이것이 모든 App Store 서버 알림과 모든 Google Play 실시간 개발자 알림 뒤에 숨은 불편한 사실입니다. 검증하기 전까지 그 메시지는 JSON이 담긴 HTTPS POST 요청에 불과합니다. 요청에 REFUND라고 적혀 있다는 이유만으로 서버가 구독을 취소하고, 크레딧을 회수하고, 계정을 정지한다면, 그 권한을 curl 명령을 작성할 줄 아는 누구에게나 넘겨준 셈입니다.
두 스토어 모두 확인할 방법을 제공합니다. Apple은 알림 자체에 서명하므로 Apple 루트 인증서만 있으면 App Store 서버 알림을 검증할 수 있습니다. Google Play는 다른 방식을 택합니다. 메시지 자체는 서명되지 않지만 Pub/Sub 전송에는 서명을 붙일 수 있고, 어차피 Google은 모든 변경 사항을 API로 확인하라고 안내합니다. 아래에서는 각 방식이 무엇을 증명하는지, 그리고 어느 방향으로든 잘못했을 때 어떤 비용이 드는지 살펴봅니다.
각 스토어가 실제로 서명하는 것
| 질문 | App Store Server Notifications V2 | Google Play RTDN |
|---|---|---|
| 메시지 자체가 서명되어 있나요? | 예, signedPayload는 JWS | 아니요, data 필드는 base64로 인코딩된 JSON |
| 발신자를 증명하는 수단 | Apple 루트까지 이어지는 x5c 인증서 체인 | 푸시 시 Pub/Sub OIDC 토큰, 또는 직접 인증한 풀 |
| 중첩된 서명 객체 | signedTransactionInfo와 signedRenewalInfo, 각각 별도의 JWS | 없음 |
| 앱과 대조할 필드 | bundleId, appAppleId, environment | packageName |
| 발신 네트워크 | 17.0.0.0/8, 샌드박스와 프로덕션 | Google Cloud Pub/Sub |
| 처리 전 확인 | 선택, 서명된 데이터 자체가 기록 | 필수, Google은 Developer API 호출을 요구 |
| 전송 방식 | HTTPS, TLS 1.2 이상 | HTTPS 푸시, 또는 클라이언트 라이브러리 풀 |
App Store 서버 알림을 검증하는 방법
Apple 문서는 signedPayload를 마침표로 구분된 세 개의 Base64URL 인코딩 구성 요소, 즉 헤더, 페이로드, 서명으로 이루어진 문자열이며 JSON Web Signature 사양에 따라 서명된다고 설명합니다. 페이로드를 디코딩하는 것은 쉽습니다. 이를 신뢰하려면 네 가지 확인이 필요합니다.
- 디코딩한 헤더에서 x5c 배열을 읽습니다. Apple은 그 순서를 문서화해 두었습니다. 먼저 서명 키가 담긴 인증서, 그다음 Apple 중간 인증서, 마지막으로 Apple 루트 인증서입니다.
- 이 체인을 Apple PKI 사이트에서 직접 다운로드한 Apple 루트 인증서, 실무에서는 Apple Root CA - G3에 대해 검증합니다. 메시지 안에 담겨 온 루트 인증서는 절대 신뢰하지 마세요. 위조자는 그 자리에 어떤 인증서든 넣을 수 있습니다.
- 중간 인증서에 Apple Worldwide Developer Relations 확장 1.2.840.113635.100.6.2.1이 있는지 확인한 뒤, 리프 인증서의 공개 키로 JWS 서명을 검증합니다.
- 디코딩한 데이터를 내 앱과 대조합니다. bundleId, 프로덕션에서의 appAppleId, 그리고 environment입니다. Apple 서명이 유효하더라도 다른 개발자의 앱에 대한 것이거나 프로덕션 엔드포인트로 들어온 샌드박스 구매라면, 그것은 여전히 내 환불이 아닙니다.
그런 다음 내부 내용에 대해서도 똑같이 합니다. data 객체에는 signedTransactionInfo와 signedRenewalInfo가 들어 있으며, 각각 자체 x5c 체인을 가진 별도의 JWS입니다. 바깥 봉투는 검증하면서 트랜잭션은 검증 없이 읽는 REFUND 핸들러는 중간에 빈틈을 남긴 것입니다.
직접 작성하지 말고 Apple의 라이브러리를 사용하세요
Apple은 Swift, Java, Python, Node용 App Store Server Library를 제공합니다. 이 라이브러리의 SignedDataVerifier는 루트 인증서, 온라인 확인 플래그, environment, 번들 ID, 그리고 프로덕션의 경우 appAppleId를 받습니다. Node 소스를 보면 무엇을 강제하는지 알 수 있습니다. 체인에 정확히 세 개의 인증서가 있을 것, Apple 리프 확장 1.2.840.113635.100.6.11.1과 중간 인증서 확장, 인증서 유효 기간, 그리고 일치하는 번들 ID, 앱 ID, environment입니다. 온라인 확인을 켜면 리프 인증서와 중간 인증서가 폐기되었는지 OCSP 응답자에게 조회하기도 합니다.

IP 허용 목록만으로 충분할까요?
아닙니다. Apple은 서버에 허용 목록이 필요하다면 17.0.0.0/8 서브넷을 추가하라고 안내하며, 이는 샌드박스와 프로덕션 모두에 적용됩니다. 이렇게 하면 잡음은 줄어듭니다. 하지만 메시지가 진짜임을 증명하지는 못하며, Apple의 네트워크가 알지 못하는 사이에 바뀌는 순간 작동하지 않게 됩니다. 실제 관문은 서명 확인으로 두세요.
Google Play 환불 알림을 검증하는 방법
Google Play는 내가 소유한 Pub/Sub 주제에 RTDN 메시지를 게시하며, 그 주제에서 Pub/Sub 게시자 역할을 가져야 하는 것은 google-play-developer-notifications@system.gserviceaccount.com 서비스 계정뿐입니다. 이후 구독은 두 가지 방식 중 하나로 메시지를 전달하며, 각 방식에는 고유한 증명 수단이 있습니다.
- 풀 구독: 서버가 Pub/Sub에 인증된 연결을 열고 메시지를 가져옵니다. 공개 엔드포인트가 없으므로 외부인이 요청을 보낼 곳이 없습니다.
- 인증을 켠 푸시 구독: Pub/Sub가 JSON Web Token에 서명하고 이를 Authorization 헤더에 Bearer 토큰으로 담아 보냅니다. Google 가이드는 토큰의 서명을 검증하고 email과 audience 클레임이 설정한 값과 일치하는지 확인하라고 안내하며, 샘플 코드는 email_verified가 true일 것도 요구합니다. 토큰은 발급된 지 최대 1시간이 지난 것일 수 있습니다.
- 인증을 켜지 않은 푸시 구독: 해당 URL에 접근할 수 있는 누구나 RTDN 형태의 메시지를 보낼 수 있습니다.
Google이 API로 확인하라고 하는 이유
완벽하게 인증된 RTDN이라도 전체 내용을 담고 있지는 않습니다. Google 설정 가이드는 이러한 알림이 구매 상태가 바뀌었다는 사실만 알려 주며, 전체 상태를 얻으려면 Google Play Developer API를 호출해야 한다고 분명히 밝힙니다. voidedPurchaseNotification의 경우, 무엇이든 회수하기 전에 Voided Purchases API에서 해당 구매를 찾거나 현재 상태를 가져와야 한다는 뜻입니다. API 호출은 내 서비스 계정으로 인증되므로, 그 응답은 위조자가 꾸며낼 수 없습니다.
위조되거나 거부된 환불이 초래하는 금전적 비용
위조된 환불 알림을 믿으면 유료 고객을 잃습니다. 아직 돈을 받아 둔 사용자를 차단하게 되고, 그 사용자는 지원 티켓을 열며, 일부는 스토어에 가서 내 시스템이 이미 승인한 척했던 환불을 실제로 요청합니다. 위조된 구매나 갱신은 반대 방향의 비용을 초래합니다. 존재하지도 않는 매출을 근거로 GPU 연산, 유료 서드파티 API 호출, 스토리지, 크리에이터 정산금을 제공하게 됩니다.
반대쪽 실패는 더 조용하고, 대개 더 큽니다. 잘못된 appAppleId, 프로덕션의 샌드박스 설정, 오래된 루트 인증서 때문에 진짜 메시지를 거부하는 검증기는 내 증거가 중요한 두 가지 환불 흐름을 놓치게 만듭니다. Apple은 CONSUMPTION_REQUEST 이후 12시간 이내에 소비 데이터를 제출하라고 요청합니다. Google은 orders.reviewrefund를 통해 지불 거절 검토에 답할 시간으로 24시간을 주며, 2026년 8월 3일 이후 접수된 주문의 경우 Google Play 지불 거절에서 지면 구매 가격에서 Play 서비스 수수료를 뺀 금액에 더해 구매자 은행이 부과하는 수수료까지 부담하게 됩니다.
| 실패 | 무슨 일이 일어나나 | 비용 |
|---|---|---|
| 위조된 REFUND 수락 | 유료 고객이 접근 권한을 잃음 | 지원 업무 시간과 스스로 불러온 실제 환불 요청 |
| 위조된 구매 수락 | 유료 기능을 무료로 이용 | 매출 없이 쓰인 연산, API 호출, 스토리지, 정산금 |
| 진짜 CONSUMPTION_REQUEST 거부 | Apple이 내 데이터 없이 결정 | 해당 매출과 이를 제공하는 데 이미 쓴 모든 비용 |
| 진짜 지불 거절 검토 거부 | Google이 내 증거 없이 결정 | 2026년 8월 3일 이후 주문의 구매 가격과 은행 수수료 |
검증 버그 때문에 기한을 놓치지 마세요
- 검증하기 전에 원본 요청을 저장해 두세요. 그러면 거부된 메시지도 완전히 사라지지 않습니다.
- 모든 검증 실패에 알림을 설정하세요. 위조 메시지 한 건은 잡음입니다. 모든 메시지가 한꺼번에 실패한다면 내 버그입니다.
- Apple에서는 거부한 메시지를 다시 처리할 수 있습니다. Get Notification History는 지난 180일 동안의 원본 signedPayload를 반환하므로, 검증기를 고친 뒤 저장해 두었거나 복구한 페이로드를 다시 통과시키면 됩니다.
- Google에서는 대기 중인 지불 거절 검토가 해당 알림에만 존재하므로, 푸시 audience 설정 오류는 곧 놓친 검토가 됩니다. 변경할 때마다 Play Console의 Send Test Message로 테스트하세요.
RefundHalt는 두 스토어의 알림을 모두 검증하고, 어떤 변경이든 일어나기 전에 스토어 API로 환불을 확인하며, CONSUMPTION_REQUEST와 지불 거절 검토에 기한 내에 응답합니다. 그래서 가짜 메시지 때문에 고객을 잃지 않고, 진짜 메시지도 놓치지 않습니다.
자주 묻는 질문
- App Store 서버 알림은 어떻게 검증하나요?
- signedPayload를 JSON Web Signature로 검증합니다. 헤더에서 x5c 인증서 체인을 읽고, Apple PKI 사이트에서 다운로드한 Apple 루트 인증서에 대해 체인을 검증하고, 리프 인증서의 키로 서명을 검증한 다음, bundleId, appAppleId, environment가 내 앱과 일치하는지 확인합니다. Apple의 App Store Server Library는 이 모든 작업을 SignedDataVerifier에서 수행합니다.
- App Store 서버 알림을 검증할 때 어떤 키를 사용하나요?
- App Store Connect의 키는 필요하지 않습니다. 공개 키는 JWS 헤더 안 x5c 체인의 첫 번째 인증서에 들어 있으며, 직접 확보한 Apple 루트 인증서까지 체인을 검증한 후에만 신뢰할 수 있습니다. 앱 내 구입 키는 Apple API를 호출하기 위한 것이지 알림을 확인하기 위한 것이 아닙니다.
- Google Play 실시간 개발자 알림은 서명되어 있나요?
- 아니요. Google Play는 RTDN 메시지 자체에 서명하지 않습니다. 인증된 푸시 구독에서는 Pub/Sub가 Authorization 헤더에 서명된 OpenID Connect 토큰을 보내며 이를 검증하면 됩니다. 또한 Google은 알림을 받을 때마다 Google Play Developer API를 호출해 구매의 전체 상태를 가져오라고 안내합니다.
- 알림 검증에 실패하면 서버가 오류를 반환해야 하나요?
- 먼저 원본 요청을 저장한 다음 결정하세요. Apple은 40x 또는 50x 응답을 받으면 버전 2 알림을 이전 시도로부터 1, 12, 24, 48, 72시간 후에 다시 보냅니다. 실패 원인이 내 설정이라면 Get Notification History로 180일 동안 원본 signedPayload를 복구할 수 있고, 수정한 뒤 다시 검증할 수 있습니다.
- Apple의 IP 범위만 허용해도 알림을 검증해야 하나요?
- 예. Apple은 허용 목록을 쓰는 서버에 17.0.0.0/8 서브넷을 허용하라고 요청하지만, 이는 연결할 수 있는 대상을 제한할 뿐 메시지가 진짜임을 증명하지는 않습니다. 알림을 Apple과 내 앱에 연결해 주는 확인은 JWS 서명입니다.
출처 및 추가 자료
- Apple Developer: signedPayload
- Apple Developer: JWSDecodedHeader
- Apple Developer: Enabling App Store Server Notifications
- Apple Developer: Responding to App Store Server Notifications
- Apple on GitHub: App Store Server Library (Node)
- Android Developers: Get ready (Real-time developer notifications setup)
- Google Cloud: Authentication for push subscriptions
- Play Console Help: Updates to refund protection and chargeback cost responsibility
RefundHalt
App Store와 Google Play 환불 자동 처리
계속 읽기
과도한 환불 요청은 환불액보다 더 큰 손실을 부릅니다. 두 앱 스토어 모두 이를 품질 신호로 보기 때문입니다
Apple 가이드라인 5.6.4는 과도한 환불 요청을 앱이 품질 기준에 미달한다는 징후로 꼽고, Google의 개발자 배포 계약은 환불률을 근거로 신규 개발자의 앱을 삭제할 수 있게 합니다. 각 스토어가 무엇이라고 규정하는지, 삭제되면 무엇을 잃는지, 위험을 어떻게 피하는지 정리했습니다.
환불 알림을 놓치면 이의를 제기할 수 있는 유일한 환불을 잃을 수 있지만, 제때 확인하면 두 스토어 모두 되찾을 수 있습니다
서버가 환불 알림을 놓치면, 증거를 요청하는 두 가지 환불 절차가 여러분 없이 마감됩니다. Apple과 Google Play가 어떻게 재시도하는지, 놓친 환불 알림을 얼마나 과거까지 복구할 수 있는지, 그리고 복구의 빈틈은 어디인지 정리했습니다.