iOS 인앱 정기결제(IAP Auto-renewable Subscription) 튜토리얼

iOS 앱에서 상품을 등록하고 판매하는 과정은 꽤나 복잡하다. 그 중에서도 정기구독 자동결제(Auto-Renewable Subscription) 상품을 판매하는 경우 신경써야 할 부분이 매우 많다. 2016년 WWDC에서 애플은 Auto-Renewable Subscription을 모든 카테고리의 앱에 적용가능하도록 허용하기로 했고(기존에는 잡지, 음악 등 특정 컨텐츠에 대해서만 허용되었었음), 해당 타입의 결제를 통해 발생한 매출의 경우 다음 조건을 만족할 경우 앱 판매 수수료를 30% ->15%로 인하 하는 내용에 대해서 발표했다.

  • 수수료 인하 조건
    • 해당유저가 1년이상 결제를 유지한 경우 1년 이후 결제분의 수수료를 15%로 인하 ( 2016년 6월 13일 이후 부터 적용)
    • 사용자가 중간에 정기 결제를 취소했더라도 60일 이내에 다시 정기결제를 시작한경우 해당 유저의 결제 기간은 계속 누적된다. (60일을 넘을 경우 리셋)
    • 이번 발표 전 기존 결제 기간도 카운트 된다. (Prior days of paid service are counted.) 즉, 이미 1년간 결제해온 사용자가 6월 13일 이후에 결제할 경우 수수료는 15%로 줄어든다.

Auto-Renewable Subscription에 대해 자세히 설명하기 전에 먼저 앱스토어의 다른 상품 타입들에 대해서도 간략히 정리해보도록 하자.

인앱 결제 (In-App Purchase) 종류

  • Consumable products: 결제 후 사용하면 일회성으로 소비되는 상품. 예를 들어 voice talk credit이나 게임상에서 사는 게임 머니 등이 있다.
  • Non-consumable products: 한 번 사면 무한정 가지고 있을 수 있는 상품. unlock한 게임 레벨, 책 컨텐츠 , 카메라 앱에서 구매한 스티커나 프레임등이 여기에 해당된다.
  • Auto-renewable subscription: 정기적으로 결제가 일어나는 상품으로 일정 기간 동안만 컨텐츠 이용이 가능하다. active한 기간동안 모든 device에서 접근 가능해야 하며 이는 restore 가 되어야 함을 뜻한다. non-consumable과는 다르게 expiration date가 존재하며 만료 전 ios 시스템에서 자동으로 renewal을 시도한다. 음악 정기결제, 잡지 정기 결제 등이 해당된다.
  • Non-renewable subscriptions: 일정 기간 동안만 사용 가능한 상품으로 자동갱신이 일어나지 않는다.

인앱 결제 구현하기

Auto-Renewable Subscription 상품에 대한 결제 방식은 다른 종류의 IAP 상품들과 동일하니 여기서 자세히 설명하지는 않고 넘어가도록 한다.

인앱 정기 결제시 리뉴얼 결제 절차

정기결제가 진행중인 경우 다음과 같은 절차를 통해서 리뉴얼 결제가 발생한다.

  • “preflight” check – 만료 10일전에 애플이 알아서 점검 후 아래 항목에 해당하는 문제가 있을경우 유저에게 알림이 전달된다.
    • 고객의 등록된 신용카드 정보가 유효한지
    • 고객이 첫 정기결제를 한 시점 이래로 가격이 변동되지 않았는지
    • 해당 프로덕트가 삭제되지 않았는지
  • 만료 24시간 전
    • 애플이 자동으로 renew 를 위한 결제 시도를 진행. 너무 많이 실패시 시도 중단.
    • 앱의 기동 여부와 관계 없이 진행된다.
    • 앱스토어는 lapse를 피하기 위해서 만료시간보다 살짝 일찍 결제 진행됨
    • 그래도 lapse가 생기는 경우 발생
      • 유저의 결제정보가 올바르지 않은경우 리뉴얼결제실패 → 만료기간이 지난후에 다시 정상적인 결제정보가 입력된경우 lapse 발생
        • 유저가 정기결제를 꺼놓았다가 만료기간이 지난후 다시 정기결제 켠 경우 lapse 발생
  • 리뉴얼 결제가 이루어졌는지 내역 확인
    • 애플에서 발생한 리뉴얼 결제 여부에 대해서 서버나 앱에서 조회는 할 수 있으나 결제시점에 애플로부터 따로 notification을 받을 수 있는 방법은 없다.
    • 따라서 앱의 기동여부와 관계없이 리뉴얼 결제여부를 알아내려면 서버에서 항상 polling을 통해 조회해야한다. 이경우 서버에서 누락되거나 처리가 delay 되는 경우가 발생할 수도있기 때문에 앱 기동시에도 한번더 리뉴얼 결제 여부를 조회하여 처리해줄 필요가 있다. (결국 앱기동시 앱에서 trigger하는 로직, 서버에서 polling으로 trigger하는 로직 둘다 필요함.)
    • 앱에서 확인 하기
      • [[SKPaymentQueue defaultQueue] addTransactionObserver:]; 를 앱 기동시에 실행하면, 리뉴얼 결제가 발생한 경우 paymentQueue에 결제건이 들어오게된다. 이 트랜잭션을 사용자가 직접 결제 했을때와 동일한 로직으로 처리하면 된다.
      • 일반적으로 앱에서 locally 영수증 유효성을 검사하는것 보다 서버에서 애플서버로 요청을 날려 영수증 유효성을 조회하는것이 더 안전하다.
      • 따라서 결제 내용과 영수증을 자체 서버로 보낸 후, 자체 서버에서 애플서버로 다시한번 verfyReceipt를 하는 절차로 구성하면 된다. (아래 서버에서 확인 파트와 동일하게 처리)
    • 서버에서 확인 하기
      • 서버에서 주기적으로 batch작업을 통해 만료일이 가까워진 결제건에대해 애플서버에 아래와같이 polling을 해야한다.
      • 자체 서버에서 보관중인 original 결제건의 receipt를 이용하여 아래 URL에 조회를 해야한다.
        • sandbox: https://sandbox.itunes.apple.com/verifyReceipt
        • production: https://buy.itunes.apple.com/verifyReceipt
      • 해당 URL 조회시 마지막 receipt 정보 + latest receipt 정보가 같이 리턴되서 돌아온다. 두가지가 다를경우 정상적으로 리뉴얼이 된것으로 보면 된다. 돌아온 receipt안에 정기결제 만료일, 취소일자 등의 주요 정보들이 있으니 항상 이 정보를 기준으로 모든 작업들을 처리하면 된다.

인앱 결제 내역 복원하기 (Restore In-App Purchase Products)

iap-types

위 표에서 많이들 헷갈려 하는 부분이 Restored by the systemby 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 당했다는 사람이 많았다 (참고).

앱스토어에서 리젝을 피하기 위해 다음과 같은 가이드라인을 따르면 된다.

  • Non-renewable Subscription 을 구매할 때 registration 과정은 optional 이어야 한다. (즉, 로그인 하지 않은 user도 구매가능 하도록 허용해야한다.)
  • 다만 로그인 하지 않은 유저는 다른 디바이스에서 동일 앱을 사용 할 때나, 앱을 지웠다 다시 깔았을 때 해당 유저와 결제 내역을 연결할 정보가 없어서 restore가 불가능하다. 따라서 Non-renewable Subscription 상품을 구매하려고 할 때 이를 명시해서 “로그인을 하지 않으면 다른 device에서 restore 할 수 없습니다. 구매하시겠습니까?” 등의 메세지를 보여준다.
  • 로그인 한 유저가 구매한 Non-renewable Subscription은 별도의 restore 과정없이 어떤 device에서 로그인 해도 보존 되어야 한다.

Code in Action: 인앱 결제 내역 복원하기

Apple Store Kit이 제공하는 Restore 기능은 해당 기기에 로그인된 Apple ID를 기준으로 동작한다. 이는 서비스 registration id 와는 상관이없다. 따라서 같은 Apple ID를 공유하면서 생길 수 있는 abusing 이슈에 대해서는 서비스 서버에서 따로 방어햐야 한다.

Restore 로직을 짜기 위해서 SKPaymentTransaction class 에 대한 이해가 필요하다.
SKPaymentTransaction 는 상품 구매의 한 단위이다. 이는 한번 구매가 완료 되었을 때 (실패, 성공 상관없이) 하나 생기고 Restore 할 때도 하나가 추가로 생긴다. Restore를 10번 하게되면 10개가 더 생기게 된다.

  1. SKPaymentTransactionObserver protocol을 따르는 핸들러를 만든 후 restore transaction에 대한 이벤트를 받는다.
    @interface RestoreViewController : UIViewController <SKPaymentTransactionObserver>
  2. transaction observer 를 등록한다. 모든 작업이 끝난 후 remove 해 주는것을 잊지말자.

  3. restore button을 유저가 클릭했을 때 아래 함수를 부른다.

  4. SKPaymentTransactionObserver에 대한 call back 함수를 작성해 준다. updatedTransactions: 함수는 required 이다. SKPaymentQueue 에 SKTransaction 이 담겨서 들어오는데 SKPaymentTransactionState 를 보고 transaction을 finish 해 주어야 한다. 이 함수는 observer가 잘 걸려있고 restoreCompletedTransactions 를 호출하면 잇달아 자동으로 불린다.

  5. restore 실패 시 아래 함수가 불리게된다.

  6. restore 성공시 paymentQueueRestoreCompletedTransactionsFinished: 함수가 불린다. 여기서 주의 할 점은 restore 된 transaction 또한 새롭게 생성된 transaction이기 때문에 transactionIdentifier 가 크게 의미가 없다는 것이다. 첫 결제의 transaction id는 originalTransaction 라는 property에서 참조하고 있다. 이 originalTransaction 의 transaction id가 중요하다.

    “originalTransaction : The contents of this property are undefined except when transactionState is set to SKPaymentTransactionStateRestored.”

위 API문서에는 restored 된 transaction 은 항상 originalTransaction 을 가지고 있는 것 처럼 설명되어 있지만, Apple 버그인지는 몰라도 originalTransaction 이 nil로 넘어올 때가 있으니 꼭_ nil check를 하는 것이 중요하다. _

이렇게 해당 애플 아이디로 구매했던 non-consumable 과 auto-renewable subscription의 목록을 가져와서 서비스 서버로 응답해 주면 서버가 실제 그 user 에게 이전에 구매했던 상품들을 다시 matching 시켜주면 된다.

예제코드

애플 샌드박스 계정을 통한 결제 테스트 (Test Using Sandbox)

이제 Auto-Renewable Subscription 상품에 대해 어느정도 개발이 진행되었으니 테스트를 해보도록 하자. 그런데 내가 설정한 상품의 subscription 기간이 1달이라면 다음 정기결제가 일어나는 시점까지 기다려서 테스트 하기가 매우 어려울 것이다. 아직 개발중인 앱의 경우 해당 상품이 앱스토어 상품 리뷰를 통과하지 않았기 때문에 경우 실제 Apple ID로 결제 테스트 하는 것도 불가능 하다. 이때 사용하는 것이 SandBox 계정이다. SandBox 계정은 Apple Developer Center에 가서 추가할 수 있다.

SandBox 결제의 특징들과 몇가지 테스팅 팁을 정리해보았다.

  • Renewal Time : SandBox를 이용하면 5분마다 renewal이 일어난다. 그리고 결제 transaction은 2분정도 먼저 일어난다. renewal은 최대 6회까지 일어난다. 대부분 6회까지 일어나지만 간혹 3-4회에서 더이상 갱신 안 될때도 있다. Apple Document 참고
  • Apple ID : 앱에서 결제/restore 테스트를 하기에 앞서 Apple Store에서 로그아웃을 해 줘야 테스트 계정이 꼬이지 않는다.
  • 한번 결제/restore 를 한 후 에도 이상한 페이지에서 계속 login prompt가 뜬다면 finishTransaction: 함수를 제대로 불러주지 않은 것이니 코드를 체크해 본다.
  • 한번 정기결제가 되었던 샌드박스 유저에 대해서는 해당 정기결제를 취소할 방법이 없기때문에(샌드박스 유저는 iOS설정페이지의 구독관리 페이지에 접근 불가능)다시 상품을 구매해도 오토리뉴얼이 일어나지 않는다. 이 경우에도 매번 신규 샌드박스 아이디를 만들어서 사용하는 방법 밖에 없다. 참고
  • 결제 횟수나 리스토어 횟수가 너무 많은 경우 테스트가 잘 되지 않으니 새로운 sand box계정을 생성해서 테스트 한다.
  • 가끔 SandBox 결제서버가 죽어서 아무 이유없이 결제가 안될때가 있다. 이럴때는 기다렸다가 그냥 시간이 지난 후 다시 해보는 것이 좋다.
  • TestFlight를 통해서도 Sandbox테스트가 가능하다. TestFlight에 초대를 받은 계정은 실제 Apple계정이지만 이 계정으로 결제 테스트 진행시 SandBox처럼 테스트 결제가 된다.
    • TestFlight를 통해 설치한 베타앱에서 테스트플라이트 계정으로 IAP구매를 할경우 실제 결제창과 동일하게 나타남(Sandbox 표시 없음) 하지만 결제를 진행해도 실결제는 일어나지 않는다.

인앱 정기 결제 FAQ 정리

  • 앱 내부에서 정기결제 상태를 확인하거나, 정기결제를 On/Off 할 수 있는지?
    • 불가능하다. 다음 링크를 통해서 유저를 아이튠스 구독 페이지로 보내서 켜고끄는 방법밖에 없다.
    • https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/manageSubscriptions
  • 오토리뉴얼 서브스크립션의 경우 매 결제마다 receipt가 새로 생성되는지?
    • 매 결제시 receipt가 새로 생성된다.
    • 서버쪽에서 마지막에 받았던 receipt으로 verify조회시 마지막 receipt 정보 + latest receipt 정보가 같이 리턴되서 돌아온다. 두가지가 다를경우 정상적으로 리뉴얼이 된것으로 보면 된다.
  • 유저가 아이튠스화면에서 정기결제를 꺼놓은 경우
    • 서버나 앱에서 정기결제를 끌때 notification callback 있는지? – 없다. 오직 receipt의 expire date로 확인해야한다.
    • 만료기일 경과한 이후에 다시 정기결제를 켤 경우?
      • 미결제 공백 기간(lapse)은 이용기간으로 치지않고, 정기결제를 켜면서 결제를 다시 시작한시점부터 서브스크립션 시작것으로 간주된다.
  • 결제 취소 여부 판단하는 방법은?
    • receipt내의 Cancellation Date를 보고 취소여부 판단한다. Cancellation 필드가 있으면 만료일자와 관계없이 취소된 subscription이다.

참고자료

IAP auto renewal subscription 종합

IAP 정기 결제 단점 정리

Apple Developer – Subscription

Apple Developer – Receipt Validation

Server-side Auto Renewable Subscription Receipt Verification

  • uc5d8ub9acuc0e4uc778

    uac10uc0acud569ub2c8ub2e4 ub9ceuc740 ub3c4uc6c0uc774 ub418uc5c8uc2b5ub2c8ub2e4.