인앱결제, 클라이언트만 믿으면 무너진다
1인 앱 개발자가 출시 직후 가장 자주 발견하는 매출 누수 패턴이 있습니다. 사용자가 "결제했다"는 응답을 받았는데, 실제로는 결제하지 않았거나 우회 결제 도구로 가짜 영수증을 만든 경우입니다.
iOS의 StoreKit, Android의 Google Play Billing은 모두 결제 결과를 클라이언트 SDK가 반환합니다. 이 결과만 믿고 사용자 권한을 부여하면 우회 결제 도구(LocalIAPStore, Lucky Patcher 등)로 100% 뚫립니다. 서버에서 영수증을 다시 검증해야 매출이 안전하게 들어옵니다.
Apple App Store 영수증 검증 (iOS)
iOS는 App Store Server API를 사용해 서버에서 영수증을 직접 검증합니다.
흐름
- 앱이 결제 완료 후
transactionId와signedTransactionInfo(JWS)를 서버로 전송 - 서버가 Apple의
/inApps/v1/transactions/{transactionId}엔드포인트에 요청 - JWS 서명을 Apple 공개키로 검증
- productId, originalTransactionId, expiresDate 확인 후 권한 부여
자주 발생하는 실수
- Sandbox와 Production 영수증 혼동: 개발 중 sandbox 영수증을 production URL로 보내면 21007 오류. 21007 응답을 받으면 sandbox URL로 재시도하는 fallback 필수
- JWS 서명 검증 생략: payload만 디코드하고 signature 확인 안 함 → 위변조 통과
- originalTransactionId가 아닌 transactionId로 사용자 매핑: 환불·재구독 시 새 ID가 발급되어 권한 누락
Google Play 영수증 검증 (Android)
Android는 Google Play Developer API의 purchases.products.get 또는 purchases.subscriptionsv2.get 엔드포인트를 호출합니다.
흐름
- 앱이 결제 완료 후
purchaseToken과productId를 서버로 전송 - 서버가 OAuth 2.0 service account로 인증 후 Google API 호출
- 응답의
purchaseState가 0(Purchased)인지 확인 acknowledged여부 확인 후, 필요 시 acknowledge API 호출 (3일 내 미실행 시 자동 환불)
자주 발생하는 실수
- acknowledge 누락: 결제 후 3일간 acknowledge 안 하면 Google이 자동 환불 처리
- purchaseToken 재사용 검증 누락: 같은 토큰으로 여러 사용자 권한 부여 가능
- service account 키 노출: GitHub에 JSON 키 파일 push → 즉시 무효화 필요
실시간 알림(Webhook) 처리도 필수
영수증 검증만으로 부족합니다. 환불·구독 취소·결제 실패는 사용자가 앱을 켜지 않으면 알 수 없습니다.
- Apple App Store Server Notifications V2: REFUND, DID_FAIL_TO_RENEW 등 30+ 이벤트
- Google Play Real-time Developer Notifications: Pub/Sub 기반, SUBSCRIPTION_CANCELED, SUBSCRIPTION_REVOKED 등
이걸 처리하지 않으면 환불받은 사용자가 계속 프리미엄 기능을 사용하는 상황이 생깁니다.
1인 개발자가 빠지기 쉬운 5가지 보안 함정
- 클라이언트만 믿고 권한 부여 (가장 흔함)
- JWS/서명 검증 생략
- Sandbox·Production fallback 없음
- Webhook 처리 안 함 → 환불 후에도 프리미엄 유지
- service account 키를 코드 저장소에 commit
지금 점검하세요
인앱결제 모듈은 한 번 무너지면 매출 직접 누수입니다. 서버 사이드 영수증 검증, 시크릿 노출, Webhook 엔드포인트 보안을 한꺼번에 자동 진단해야 합니다.
CodeScan 무료 점검으로 결제 시스템과 시크릿 노출, 보안 헤더를 30초 안에 확인하세요.