iOS 앱에서 상품을 등록하고 판매하는 과정은 꽤나 복잡하다. 그 중에서도 정기구독 자동결제(Auto-Renewable Subscription) 상품을 판매하는 경우 신경써야 할 부분이 매우 많다. 2016년 WWDC에서 애플은 Auto-Renewable Subscription을 모든 카테고리의 앱에 적용가능하도록 허용하기로 했고(기존에는 잡지, 음악 등 특정 컨텐츠에 대해서만 허용되었었음), 해당 타입의 결제를 통해 발생한 매출의 경우 다음 조건을 만족할 경우 앱 판매 수수료를 30% ->15%로 인하 하는 내용에 대해서 발표했다.
Auto-Renewable Subscription에 대해 자세히 설명하기 전에 먼저 앱스토어의 다른 상품 타입들에 대해서도 간략히 정리해보도록 하자.
Auto-Renewable Subscription 상품에 대한 결제 방식은 다른 종류의 IAP 상품들과 동일하니 여기서 자세히 설명하지는 않고 넘어가도록 한다.
정기결제가 진행중인 경우 다음과 같은 절차를 통해서 리뉴얼 결제가 발생한다.
“preflight” check - 만료 10일전에 애플이 알아서 점검 후 아래 항목에 해당하는 문제가 있을경우 유저에게 알림이 전달된다.
만료 24시간 전
리뉴얼 결제가 이루어졌는지 내역 확인
애플에서 발생한 리뉴얼 결제 여부에 대해서 서버나 앱에서 조회는 할 수 있으나 결제시점에 애플로부터 따로 notification을 받을 수 있는 방법은 없다.
따라서 앱의 기동여부와 관계없이 리뉴얼 결제여부를 알아내려면 서버에서 항상 polling을 통해 조회해야한다. 이경우 서버에서 누락되거나 처리가 delay 되는 경우가 발생할 수도있기 때문에 앱 기동시에도 한번더 리뉴얼 결제 여부를 조회하여 처리해줄 필요가 있다. (결국 앱기동시 앱에서 trigger하는 로직, 서버에서 polling으로 trigger하는 로직 둘다 필요함.)
앱에서 확인 하기
[[SKPaymentQueue defaultQueue] addTransactionObserver:]; 를 앱 기동시에 실행하면, 리뉴얼 결제가 발생한 경우 paymentQueue에 결제건이 들어오게된다. 이 트랜잭션을 사용자가 직접 결제 했을때와 동일한 로직으로 처리하면 된다.Polling으로 서버에서 확인 하기
Server notification을 받아서 서버에서 확인하기
위 표에서 많이들 헷갈려 하는 부분이 Restored by the system 과 by your app의 차이이다. by the system 이란 Apple에서 지원하는 StoreKit을 이용해서 Apple server와 통신해서 상품을 restore를 하는 것이고 by your app이란 Apple과의 통신 없이 서비스 서버에서 상품을 restore 해 주는 것이다.
Consumable Item 은 Restore의 대상이 아니다. 게임에서 사용하는 루비 같은 존재이므로 한번 구매하고 쓰지 못했더라도 다른 Device에서 복구해 줄 의무는 없다.
Non-consumable / Auto-Renewable Subscription은 Apple에서 Restore를 지원한다. 자동으로 되는 건 아니고 Application내에 shop 페이지 어딘가에 Restore 버튼을 놓고 Store Kit 을 이용해서 개발해야 한다. Restore 버튼이 없으면 앱스토어 리뷰 과정에서 리젝(reject) 사유가 될 수 있다.
Non-renewable Subscription은 Restore을 해줘야 하는 상품이지만 정작 apple server에서는 restore 지원을 해주지 않는다. 따라서 자체 서비스 서버를 통해 인앱 결제시에 사용자를 식별할 수 있는 정보를 저장하여 restore 기능을 구현해야한다. (restore를 지원한다기 보다는 계정에서 구매 정보를 유지해 주면 되는 형태이다.) 애플 도큐먼트의 아래 글을 보면 명확하다.
It’s your app’s responsibility to make the subscription available on all of the user’s devices and to let users restore the purchase. This product type is often used when your users already have an account on your server that you can use to identify them when restoring content.
얼핏 보면 Non-renewable Subscription 은 로그인 한 유저만 살 수있도록 해도 무방할 것 같다. 그러나 stack over flow에서 Non-renewable Subscription를 구매할 때 Registration을 requirement로 했을 때 스토어 리뷰과정에서 reject 당했다는 사람이 많았다 (참고).
앱스토어에서 리젝을 피하기 위해 다음과 같은 가이드라인을 따르면 된다.
Apple Store Kit이 제공하는 Restore 기능은 해당 기기에 로그인된 Apple ID를 기준으로 동작한다. 이는 서비스 registration id 와는 상관이없다. 따라서 같은 Apple ID를 공유하면서 생길 수 있는 abusing 이슈에 대해서는 서비스 서버에서 따로 방어햐야 한다.
Restore 로직을 짜기 위해서 SKPaymentTransaction class 에 대한 이해가 필요하다.
SKPaymentTransaction 는 상품 구매의 한 단위이다. 이는 한번 구매가 완료 되었을 때 (실패, 성공 상관없이) 하나 생기고 Restore 할 때도 하나가 추가로 생긴다. Restore를 10번 하게되면 10개가 더 생기게 된다.
SKPaymentTransactionObserver protocol을 따르는 핸들러를 만든 후 restore transaction에 대한 이벤트를 받는다.
@interface RestoreViewController : UIViewController <SKPaymentTransactionObserver>
transaction observer 를 등록한다. 모든 작업이 끝난 후 remove 해 주는것을 잊지말자.
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
//...
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
restore button을 유저가 클릭했을 때 아래 함수를 부른다.
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
SKPaymentTransactionObserver에 대한 call back 함수를 작성해 준다. updatedTransactions: 함수는 required 이다. SKPaymentQueue 에 SKTransaction 이 담겨서 들어오는데 SKPaymentTransactionState 를 보고 transaction을 finish 해 주어야 한다. 이 함수는 observer가 잘 걸려있고 restoreCompletedTransactions 를 호출하면 잇달아 자동으로 불린다.
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *aTransaction in transactions)
{
if (aTransaction.transactionState == SKPaymentTransactionStateRestored)
{
[[SKPaymentQueue defaultQueue] finishTransaction:aTransaction];
}
}
}
restore 실패 시 아래 함수가 불리게된다.
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
restore 성공시 paymentQueueRestoreCompletedTransactionsFinished: 함수가 불린다. 여기서 주의 할 점은 restore 된 transaction 또한 새롭게 생성된 transaction이기 때문에 transactionIdentifier 가 크게 의미가 없다는 것이다. 첫 결제의 transaction id는 originalTransaction 라는 property에서 참조하고 있다. 이 originalTransaction 의 transaction id가 중요하다.
“originalTransaction : The contents of this property are undefined except when
transactionStateis set toSKPaymentTransactionStateRestored.”
위 API문서에는 restored 된 transaction 은 항상 originalTransaction 을 가지고 있는 것 처럼 설명되어 있지만, Apple 버그인지는 몰라도 originalTransaction 이 nil로 넘어올 때가 있으니 꼭_ nil check를 하는 것이 중요하다. _
이렇게 해당 애플 아이디로 구매했던 non-consumable 과 auto-renewable subscription의 목록을 가져와서 서비스 서버로 응답해 주면 서버가 실제 그 user 에게 이전에 구매했던 상품들을 다시 matching 시켜주면 된다.
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
NSMutableSet* transactionIdList = [NSMutableSet new];
for (SKPaymentTransaction *aTransaction in queue)
{
if (aTransaction.transactionState == SKPaymentTransactionStateRestored)
{
if (aTransaction.originalTransaction) {
[transactionIdList addObject:aTransaction.originalTransaction.transactionIdentifier];
}
[[SKPaymentQueue defaultQueue] finishTransaction:aTransaction];
}
}
// do something with transactionIdList
}
이제 Auto-Renewable Subscription 상품에 대해 어느정도 개발이 진행되었으니 테스트를 해보도록 하자. 그런데 내가 설정한 상품의 subscription 기간이 1달이라면 다음 정기결제가 일어나는 시점까지 기다려서 테스트 하기가 매우 어려울 것이다. 아직 개발중인 앱의 경우 해당 상품이 앱스토어 상품 리뷰를 통과하지 않았기 때문에 경우 실제 Apple ID로 결제 테스트 하는 것도 불가능 하다. 이때 사용하는 것이 SandBox 계정이다. SandBox 계정은 Apple Developer Center에 가서 추가할 수 있다.
SandBox 결제의 특징들과 몇가지 테스팅 팁을 정리해보았다.
앱 내부에서 정기결제 상태를 확인하거나, 정기결제를 On/Off 할 수 있는지?
오토리뉴얼 서브스크립션의 경우 매 결제마다 Transaction ID가 새로 생성되는지?
유저가 아이튠스화면에서 정기결제를 꺼놓은 경우
애플에서 개발사들의 의견을 반영하여 2017년 7월 18일에 아래 기능들이 추가했다고 발표하였다. 좀더 자세한 내용은 WWDC 2017 Advanced StoreKit 세션을 참고하면 된다.
IAP auto renewal subscription 종합
Server-side Auto Renewable Subscription Receipt Verification
Apple Developer - Receipt Validation
Apple Developer - Subscription
Apple Announcement - Server notifications and enhanced receipts for subscriptions