Android In-App Billing 구현하기 (IAB Version 3)

android_logo

Google Play의 In-App Billing(IAB)은 앱내구매 요청을 하거나 관리하는데에 직관적이고 간단한 인터페이스를 제공합니다. 이번 글에서는 당신의 어플리케이션에서 In-App Billing 버전 3를 이용하여 어떻게 요청을 만드는지 기초적인 내용을 정리합니다. 이 내용은 기본적으로 In-App Billing의 프로세스에 대해 어느정도 이해하고 있는 상태라고 가정하고 작성되었습니다.

어플리케이션에서 IAB를 구현하기 위해서는 다음과 같은 구현 작업이 필요합니다.

  1. In-App Billing 라이브러리를 당신의 프로젝트에 추가합니다.
  2. AndroidManifest.xml 파일을 업데이트합니다.
  3. ServiceConnection을 생성하고 IInAppBillingService에 바인딩합니다.
  4. 당신의 어플리케이션에서 IInAppBillingService로 In-App Billing 요청을 날립니다.
  5. Google Play로부터 도작한 In-App Billing 응답을 처리합니다.

AIDL 파일을 프로젝트에 추가하기

IInAppBillingService.aidl 파일은 In-App Billing 버전 3 서비스로의 요청이 정의되어있는 Android Interface Definition Language(AIDL)입니다. 당신은 이 인터페이스를 사용하여 IPC(Inter-Process Communication) 메소드 호출을 함으로써 구매 요청을 할 수 있습니다. 프로젝트에 이 AIDL파일을 추가하기 위해서는 다음을 수행해 주시면 됩니다.

  1. IInAppBillingService.aidl 파일을 당신의 안드로이드 프로젝트로 복사합니다.
    • 프로젝트의 src/com/android/vending/billing 위치안에 복사합니다.
  2. 정상적으로 프로젝트에 추가되었다면 빌드 시(Ant, Gradle) /gen 디렉토리 안에 IInAppBillingService.java 파일이 생성됩니다.

AndroidManifest 파일을 수정하기

In-App Billing 요청은 당신의 어플리케이션과 Google Play 서버간의 모든 통신을 처리하는 Google Play 어플리케이션을 필요로합니다. 이 Google Play 어플리케이션을 이용하기 위해서는 당신의 어플리케이션에 적절한 퍼미션이 추가되어있어야 합니다. com.android.vending.BILLING 퍼미션을 AndroidManifest.xml에 추가함으로써 구매 요청이 가능하게 됩니다. 당신의 어플리케이션이 이 퍼미션을 설정하지 않았다면 모든 구매 요청은 Google Play로부터 거부될것이며 에러를 발생하게 됩니다. In-App Billing 요청을 위한 AndroidManifest.xml에 추가해야 하는 설정은 다음과 같습니다.

<uses-permission android:name="com.android.vending.BILLING" />

ServiceConnection 생성하기

Google Play와의 통신을 위해 당신의 어플리케이션은 반드시 ServiceConnection을 가지고 있어야 합니다. 최소한의 구현을 위해 다음과 같은 구현을 추가할 필요가 있습니다.

  • IInAppBillingService에 바인딩하기
  • Google Play 어플리케이션으로의 IPC 메소드 호출을 통한 구매 요청을 날리기
  • 각각의 구매 요청에 대한 동기(Synchronous) 응답 메시지를 처리하기

IInAppBillingService에 바인딩하기

Google Play의 In-App Billing 서비스와의 커넥션을 맺기 위해 ServiceConnection을 구현하여 당신의 Activity를 IInAppBillingService에 바인딩 해주어야 합니다. onServiceDisconnected와 onServiceConnected 메소드를 오버라이딩 하여 커넥션이 연결된 후 IInAppBillingService 인스턴스를 가져올 수 있습니다.

IInAppBillingService mService;

ServiceConnection mServiceConn = new ServiceConnection() {
   @Override
   public void onServiceDisconnected(ComponentName name) {
       mService = null;
   }

   @Override
   public void onServiceConnected(ComponentName name, 
      IBinder service) {
       mService = IInAppBillingService.Stub.asInterface(service);
   }
};

당신의 Activity의 onCreate 메소드안에서 위에서 구현한 ServiceConnection 인스턴스를 가진 Intent를 파라미터로 하여 bindService 메소드를 호출합니다.

@Override
public void onCreate(Bundle savedInstanceState) {    
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);        
    bindService(new 
        Intent("com.android.vending.billing.InAppBillingService.BIND"),
                mServiceConn, Context.BIND_AUTO_CREATE);

정상적으로 Google Play 서비스와 연결이 이루어졌다면 mService를 통해 통신할 수있게 됩니다.

구매를 진행하는 Activity가 종료된다면 In-App Billing 서비스를 종료(Unbind)해주어야 합니다. 만약 바인딩을 종료해 주지 않을 경우 서비스 커넥션은 계속해서 유지되고 디바이스의 퍼포먼스에 영향을 끼칠 수 있습니다. 다음의 예시는 onDestroy 메소드를 오버라이딩하여 mServiceConn을 명시적으로 Unbind 해주는것을 보여주고 있습니다.

@Override
public void onDestroy() {
    super.onDestroy();
    if (mService != null) {
        unbindService(mServiceConn);
    }   
}

In-App Billing 요청 생성하기

어플리케이션이 Google Play와 한번 연결되면 당신은 앱내 판매되는 상품의 구매 요청을 할 수 있게 됩니다. Google Play는 체크아웃 인터페이스를 유저에게 제공하여 적절한 구매 방법을 제공하게 됩니다. 그러므로 당신의 어플리케이션에서 직접적으로 구매를 처리할 필요는 없습니다. 아이템이 정상적으로 구매가 되면 Goole Play는 아이템에 대한 소유권을 획득했는지 확인하며 그 아이템이 소진(Consume)되기전까지 같은 아이템이 중복 구매되는것을 방지해줍니다. 당신은 당신의 어플리케이션에서 구매한 상품의 소진을 어떻게 할것인지를 컨트롤할 수 있습니다. 또한 Google Play에 요청하여 유저에 의해 생성된 구매 리스트를 빠르게 받아올 수 있습니다. 이것은 매우 유용한데 예를 들어 당신의 어플리케이션이 실행되는 시점에 유저의 이전에 실패한 구매 요청을 복원하여 처리할 수 있습니다.

구매 가능한 아이템 목록 가져오기

In-App Billing 버전 3 API를 사용하여 Google Play로 부터 상품의 상세 정보를 가져올 수 있습니다. In-App Billing 서비스에 요청을 하기 위해서는 우선 상품의 아이디 목록을 가지고 있는 Bundle을 생성하여야 합니다.

ArrayList<String> skuList = new ArrayList<String> ();
skuList.add("premiumUpgrade");
skuList.add("gas");
Bundle querySkus = new Bundle();
querySkus.putStringArrayList(“ITEM_ID_LIST”, skuList);

Google Play로 부터 정보를 받아오기 위해서는 getSkuDetails 메소드를 호출하면 됩니다. 여기에 사용되는 파라미터는 IAB API 버전을 의미하는 3과 당신의 어플리케이션의 패키지명, 그리고 위에서 생성한 Bundle을 사용합니다. getSkuDetails 메소드를 호출할 때에는 메인 쓰레드에서 실행하면 안된다는것을 유의해주시기 바랍니다.

Bundle skuDetails = mService.getSkuDetails(3, 
   getPackageName(), "inapp", querySkus);

요청이 성공한다면 BILLING_RESPONSE_RESULT_OK (0) 응답 코드를 가지고 있는 Bundle이 반환됩니다. 모든 응답 코드를 확인하시려면 이 [링크]를 참조하시기 바랍니다.

요청의 결과는 String ArrayList가 DETAILS_LIST라는 키로 담겨져 오게 됩니다. 각각의 정보는 JSON 포맷으로 이루어져 입니다. 상품 상세정보의 각각의 타입에 대한 정의를 보실려면 [이곳]을 참고하시기 바랍니다. 다음의 예제는 위의 코드를 통해 받은 응답 Bundle로부터 가격을 꺼내보는 예시입니다.

int response = skuDetails.getInt("RESPONSE_CODE");
if (response == 0) {
   ArrayList<String> responseList
      = skuDetails.getStringArrayList("DETAILS_LIST");

   for (String thisResponse : responseList) {
      JSONObject object = new JSONObject(thisResponse);
      String sku = object.getString("productId");
      String price = object.getString("price");
      if (sku.equals("premiumUpgrade")) mPremiumUpgradePrice = price;
      else if (sku.equals("gas")) mGasPrice = price;
   }
}

구매 요청하기

당신의 어플리케이션에서 구매를 시작하기 위해서는 In-App Billing 서비스의 getBuyIntent 메소드를 호출해주면 됩니다. In-App Billing API 버전을 의미하는 3과 패키지명이 마찬가지로 사용되며 구매하려는 상품의 아이디와 구매의 타입(inapp 또는 subs)과 마지막으로 developerPayload 문자열이 포함됩니다. 이 추가적인 developerPayload 파라미터는 Google Play가 구매 정보에 함께 포함하여 그대로 되돌려주게 됩니다.

Bundle buyIntentBundle = mService.getBuyIntent(3, getPackageName(),
   sku, "inapp", "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ");

요청이 성공한다면 BILLING_RESPONSE_RESULT_OK (0) 응답 코드를 가지고 있는 Bundle이 반환됩니다. 이 Bundle에는 BUY_INTENT라는 이름으로 구매 플로우를 시작하는데 사용되는 PendingIntent가 들어있습니다.

PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");

이 구매 트랜젝션을 완료하기 위해서는 위에서 얻은 PendingIntent를 이용하여  startIntentSenderForResult 메소드를 호출하여야 합니다. 다음의 예제에서 Request Code로 1001을 사용하였습니다.

startIntentSenderForResult(pendingIntent.getIntentSender(),
   1001, new Intent(), Integer.valueOf(0), Integer.valueOf(0),
   Integer.valueOf(0));

Google Play는 당신의 어플리케이션의 onActivityResult 메소드로 PendingIntent 형태의 응답을 보내주게 됩니다. onActivityResult 메소드는 Activity.RESULT_OK (1) 또는 Activity.RESULT_CANCELED (0) 를 Result Code로 받게 됩니다. 여기서 Intent 형태의 응답을 받게 됩니다. 여기에 담겨있는 정보들의 타입을 확인하려면 [이곳]을 참고하시기 바랍니다.

구매 데이터는 응답 Intent안에 INAPP_PURCHASE_DATA라는 이름의 키로 JSON 포맷 형태의 문자열로 저장되어있습니다.

'{ 
   "orderId":"12999763169054705758.1371079406387615", 
   "packageName":"com.example.app",
   "productId":"exampleSku",
   "purchaseTime":1345678900000,
   "purchaseState":0,
   "developerPayload":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
   "purchaseToken":"rojeslcdyyiapnqcynkjyyjh"
 }'

위의 내용을 종합하여 보면 다음과 같은 코드가 됩니다. RESPONSE_CODE, INAPP_PURCHASE_DATA, INAPP_DATA_SIGNATURE값을 받아오는것을 확인할 수 있습니다.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { 
   if (requestCode == 1001) {           
      int responseCode = data.getIntExtra("RESPONSE_CODE", 0);
      String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
      String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE");

      if (resultCode == RESULT_OK) {
         try {
            JSONObject jo = new JSONObject(purchaseData);
            String sku = jo.getString("productId");
            alert("You have bought the " + sku + ". Excellent choice, 
               adventurer!");
          }
          catch (JSONException e) {
             alert("Failed to parse purchase data.");
             e.printStackTrace();
          }
      }
   }
}

보안을 위한 조언 : 구매 요청을 보낼 때 구매 요청을 식별할 수 있는 유니크한 문자열 토큰을 생성하여 developerPayload에 담아 보내시기 바랍니다. 랜덤하게 생성된 토큰 형태의 이 문자열을 사용하면 Google Play로부터의 응답을 받을 때 orderId와 developerPayload문자열 값을 통해 응답 데이터가 진짜인지 확인할 수 있습니다. 추가적인 보안을 위해 구매 확인을 위한 자체적인 보안 서버를 구축하기를 권장합니다. 이 서버를 통해 유니크한 값인 orderId가 이전에 이미 처리된적이 없는지 확인할 수 있고 developerPayload 문자열은 이전에 요청에 내가 담아 보낸 값과 동일한지를 확인할 수 있습니다.

구매한 아이템 정보를 가져오기

IAB 버전 3 서비스의 getPurchases 메소드를 호출함으로써 어플리케이션으로부터 사용자가 생성한 구매 정보를 가져올 수 있습니다. IAB 버전을 의미하는 3과 패키지명과 구매타입(inapp 또는 subs)을 파라미터로 사용합니다.

Bundle ownedItems = mService.getPurchases(3, getPackageName(), "inapp", null);

Google Play 서비스는 현재의 디바이스에 로그인되어있는 유저의 어카운트가 생성한 구매의 정보만을 반환합니다. 요청이 성공한다면 응답 코드가 0인 Bundle을 반환합니다. 이 Bundle은 상품의 아이디들과 각각의 구매에 대한 상세 정보를 담은 리스트를 가지고 있습니다.

퍼포먼스 향상을 위해 In-App Billding 서비스는 getPurchases가 처음 호출될 때 유저가 보유하고 있는 상품의 정보를 최대 700개 까지만 반환합니다. 만약 유저가 굉장히 많은 수의 상품을 구매하였다면 Google Play는 아직 더 많은 구매한 상품이 남아있다는 의미를 가진 INAPP_CONTINUATION_TOKEN 키를 함께 보내주게 됩니다. 이 값이 존재할 경우 이 값을 포함하여 getPurchases를 한번 더 요청하여 남은 구매 상품의 정보를 가져오면 됩니다. getPurchases 메소드의 4번째 인자에 INAPP_CONTINUATION_TOKEN 값을 담아서 요청하면 됩니다.

int response = ownedItems.getInt("RESPONSE_CODE");
if (response == 0) {
   ArrayList<String> ownedSkus =
      ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
   ArrayList<String>  purchaseDataList =
      ownedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
   ArrayList<String>  signatureList =
      ownedItems.getStringArrayList("INAPP_DATA_SIGNATURE");
   String continuationToken = 
      ownedItems.getString("INAPP_CONTINUATION_TOKEN");

   for (int i = 0; i < purchaseDataList.size(); ++i) {
      String purchaseData = purchaseDataList.get(i);
      String signature = signatureList.get(i);
      String sku = ownedSkus.get(i);

      // 구매한 상품의 정보를 이용하여 무언가를 처리하면 됩니다.
      // e.g. 유저가 보유한 상품의 리스트를 업데이트
   } 

   // 만약 continuationToken != null 이라면 getPurchases를 한번더 호출합니다.
   // INAPP_CONTINUATION_TOKEN 토큰값을 사용하여 이후의 데이터를 받아올 수 있습니다.
}

구매를 소진 (Consuming)

당신은 In-App Billing 버전 3 API를 사용하여 Google Play의 구매한 앱내 상품에 대해 소유권을 가지고 있는지를 추적할 수 있습니다. 한번 앱내 상품이 구매되면 그것이 소유되었다(owned)고 간주되며 똑같은 상품을 또 구매할 수 없게 됩니다. 반드시 앱내 구매한 상품에 대해 소진 요청을 Google Play에 보내야 하며 Google Play는 해당 상품을 다시 구매 가능한 상태로 전환합니다.

중요 : 관리되는 제품은 소진이 가능하며 구독 상품은 불가능합니다.

상품의 소진을 기록하기 위해 In-App Billing 서비스에 purchaseToken 문자열을 담아 consumePurchase 메소드를  호출해야 합니다. 이 purchaseToken은 구매 요청이 성공했을 때 받았던 응답의 INAPP_PURCHASE_DATA 안에 담겨있습니다. 예를 들어 소진할 상품의 식별자로 pruchaseToken값을 token이라는 변수에 담아 호출해 보았습니다.

int response = mService.consumePurchase(3, getPackageName(), token);

주의 : consumePurchase 메소드를 메인 쓰레드에서 호출하지 않도록 주의해 주시기 바랍니다.

응답인 response가 0일 경우 정상적으로 소진 처리가 되었다고 판단하시면 됩니다. 이제 구매한 상품에 대해 유저에게 무엇을 제공할 것인지에 대한 처리는 당신의 몫입니다. 예를 들어 유저가 게임 화폐를 구매하였다면 유저의 인벤토리안의 보유 금액을 업데이트 하여야 할것입니다.

보안을 위한 조언 : 유저에 구매한 상품의 정보를 업데이트 하기 이전에 반드시 소진 요청을 먼저 하시기 바랍니다. 즉, 소진 요청이 정상적으로 성공한 뒤에 유저에게 상품 지급 처리를 하시는것을 권장합니다.

구독 상품의 구현

구독의 구현은 상품의 타입을 subs로 해야한다는것을 제외하면 상품의 구매 플로우와 매우 비슷합니다. 구매 결과는 마찬가지로 Activity의 onActivityResult 메소드에서 받으며 앱내 상품 구매의 경우와 동일하게 처리됩니다.

Bundle bundle = mService.getBuyIntent(3, "com.example.myapp",
   MY_SKU, "subs", developerPayload);

PendingIntent pendingIntent = bundle.getParcelable(RESPONSE_BUY_INTENT);
if (bundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) {
   // Google Play UI를 띄워 구매 플로우를 시작합니다.
   // 결과는 onActivityResult() 를 통해 반환됩니다.
   startIntentSenderForResult(pendingIntent, RC_BUY, new Intent(),
       Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0));
}

구매된 구독을 활성화(Active)하기 위해서는 subs 파라미터를 포함한 getPurchases 메소드를 이용하면 됩니다.

Bundle activeSubs = mService.getPurchases(3, "com.example.myapp",
                   "subs", continueToken);

이 요청은 유저가 소유한 모든 활성화된 구독 상품의 정보를 Bundle에 담아 반환합니다. 한번 만료(expire)된, 리뉴얼되지 않은 구독 상품의 경우 더이상 Bundle을 통해 반한되지 않습니다.

참고