Save the date! Android Dev Summit is coming to Mountain View, CA on November 7-8, 2018.

인앱 결제 구현

Google Play에서 인앱 결제 서비스는 Google Play를 사용하여 인앱 결제 요청을 보내고 인앱 결제 트랜잭션을 관리하기 위한 편리하고 간단한 인터페이스를 제공합니다. 아래의 정보는 Version 3 API를 사용하여 애플리케이션에서 인앱 결제 서비스를 호출하는 기본적인 방법을 설명한 내용입니다.

참고: 전체 구현을 둘러보고 애플리케이션 테스트 방법을 확인하려면 인앱 상품 판매 교육 과정을 살펴보세요. 이 교육 과정에서는 기본 액티비티에서 인앱 결제 호출을 수행할 수 있도록, 연결 설정, 결제 요청 보내기, Google Play의 응답 처리, 그리고 백그라운드 스레딩 관리와 관련된 주요 작업을 처리하기 위한 편의 클래스를 비롯하여, 완전한 샘플 인앱 결제 애플리케이션을 제공합니다.

시작하기 전에 인앱 결제 개요를 읽어보고 개념을 완전히 익혀야 인앱 결제를 쉽게 구현할 수 있습니다.

애플리케이션에 인앱 결제를 구현하려면 다음 작업을 수행해야 합니다.

  1. 인앱 결제 라이브러리를 프로젝트에 추가합니다.
  2. AndroidManifest.xml 파일을 업데이트합니다.
  3. ServiceConnection을 생성하여 IInAppBillingService에 바인딩합니다.
  4. 애플리케이션에서 IInAppBillingService로 인앱 결제 요청을 보냅니다.
  5. Google Play의 인앱 결제 응답을 처리합니다.

프로젝트에 AIDL 파일 추가

IInAppBillingService.aidl은 In-app Billing Version 3 서비스에 대한 인터페이스를 정의하는 AIDL(Android Interface Definition Language) 파일입니다. 이 인터페이스를 통해 IPC 메서드를 호출하여 결제를 요청합니다.

AIDL 파일을 얻는 방법은 다음과 같습니다.

  1. Android SDK Manager를 엽니다.
  2. SDK Manager에서 Extras 섹션을 펼칩니다.
  3. Google Play Billing Library를 선택합니다.
  4. Install packages를 클릭하여 다운로드를 완료합니다.

IInAppBillingService.aidl 파일은 <sdk>/extras/google/play_billing/에 설치됩니다.

프로젝트에 AIDL을 추가하는 방법은 다음과 같습니다.

  1. 먼저 Android 프로젝트에 Google Play Billing Library를 다운로드합니다.
    1. Tools > Android > SDK Manager를 선택합니다.
    2. Appearance & Behavior > System Settings > Android SDK에서 SDK Tools 탭을 선택하여 Google Play Billing Library를 선택하고 다운로드합니다.
  2. 다음으로, IInAppBillingService.aidl 파일을 프로젝트에 복사합니다.
    • Android Studio를 사용 중인 경우에는 다음 절차를 따르세요.
      1. Project 도구 창에서 src/main으로 이동합니다.
      2. File > New > Directory를 선택하고 New Directory 창에 aidl을 입력한 후 OK를 선택합니다.
      3. File > New > Package를 선택하고 New Package 창에 com.android.vending.billing을 입력한 후 OK를 선택합니다.
      4. 운영체제 파일 탐색기를 사용하여 <sdk>/extras/google/play_billing/으로 이동하고 IInAppBillingService.aidl 파일을 복사하여 프로젝트의 com.android.vending.billing 패키지에 붙여넣습니다.
    • Android Studio 이외의 환경에서 개발 중인 경우에는 다음 절차를 따르세요. /src/com/android/vending/billing 디렉터리를 만들고 IInAppBillingService.aidl 파일을 이 디렉터리로 복사합니다. AIDL 파일을 프로젝트에 넣고 Gradle 도구를 사용하여 프로젝트를 빌드하면 IInAppBillingService.java 파일이 생성됩니다.
  3. 애플리케이션을 빌드합니다. 프로젝트의 /gen 디렉터리에 IInAppBillingService.java라는 이름으로 생성된 파일이 보일 것입니다.

앱의 매니페스트 업데이트

인앱 결제는 애플리케이션과 Google Play 서버 간의 모든 통신을 처리하는 Google Play 애플리케이션에 의존합니다. Google Play 애플리케이션을 사용하려면 적절한 권한을 요청해야 합니다. 이를 위해서는 AndroidManifest.xml 파일에 com.android.vending.BILLING 권한을 추가하면 됩니다. 애플리케이션이 인앱 결제 권한을 선언하지 않지만 결제 요청 보내기를 시도할 경우, Google Play는 요청을 거부하고 오류로 응답합니다.

앱에 필요한 권한을 부여하려면 AndroidManifest.xml 파일에 다음 행을 추가하세요.

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

ServiceConnection 생성

애플리케이션에 ServiceConnection이 있어야 애플리케이션과 Google Play 사이의 메시지 교환이 원활하게 이루어집니다. 애플리케이션이 최소한 다음 작업은 수행해야 합니다.

  • IInAppBillingService에 바인딩합니다.
  • Google Play 애플리케이션에 (IPC 메서드 호출로서) 결제 요청을 보냅니다.
  • 각 결제 요청과 함께 반환되는 동기 응답 메시지를 처리합니다.

IInAppBillingService에 바인딩

Google Play에서 인앱 결제 서비스와의 연결을 설정하려면 ServiceConnection을 구현하여 액티비티를 IInAppBillingService에 바인딩합니다. 연결이 설정된 후 onServiceDisconnectedonServiceConnected 메서드를 재정의하여 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);
   }
};

액티비티의 onCreate 메서드에서 bindService 메서드를 호출하여 바인딩을 수행합니다. 인앱 결제 서비스를 참조하는 Intent와 앞에서 생성한 ServiceConnection의 인스턴스를 메서드에 전달하고, 인텐트의 대상 패키지 이름을 Google Play 앱의 패키지 이름인 com.android.vending으로 명시적으로 설정합니다.

주의: 결제 트랜잭션의 보안을 확보하기 위해, 아래 예시에 나타낸 것처럼 setPackage()를 사용하여 인텐트의 대상 패키지 이름을 항상 명시적으로 com.android.vending으로 설정하십시오. 패키지 이름을 명시적으로 설정하면 오직 Google Play 앱만이 개발자가 만든 앱의 결제 요청을 처리할 수 있으므로, 다른 앱이 이런 결체 요청을 가로채지 못합니다.

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

이제 mService 참조를 사용하여 Google Play 서비스와 통신할 수 있습니다.

중요: Activity 작업을 마쳤으면 인앱 결제 서비스에서 꼭 바인딩 해제해야 한다는 사실을 기억하세요. 바인딩 해제하지 않으면 개방된 서비스 연결로 인해 기기의 성능이 저하될 수 있습니다. 다음 예시에서는 액티비티의 onDestroy 메서드를 재정의하여 mServiceConn이라는 인앱 결제에 대한 서비스 연결에서 바인딩 해제 작업을 수행하는 방법을 보여줍니다.

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

IInAppBillingService에 바인딩하는 서비스 연결의 전체 구현에 대해서는 인앱 상품 판매 교육 과정과 관련 샘플을 참조하세요.

인앱 결제 요청하기

애플리케이션이 Google Play에 연결된 후 인앱 상품에 대한 구매 요청을 시작할 수 있습니다. Google Play는 결제 방법을 입력할 수 있도록 사용자용 결제 인터페이스를 제공하므로, 애플리케이션이 결제 트랜잭션을 직접 처리할 필요가 없습니다. 아이템 구매가 이루어지면, Google Play는 사용자가 그 아이템의 소유권을 보유하고 있음을 인식하고 아이템을 소비할 때까지는 사용자가 같은 상품 ID로 다른 아이템을 구매하지 못하게 합니다. 애플리케이션에서 아이템 소비 방식을 관리하고 Google Play에 해당 아이템을 다시 구매할 수 있도록 전환하라고 알릴 수 있습니다. 또한, Google Play를 쿼리하여 사용자에 의해 이루어진 구매 목록을 빠르게 검색할 수 있습니다. 예를 들어, 사용자가 앱을 시작할 때 사용자의 구매를 복원하고 싶을 경우에 이 기능이 유용합니다.

구매할 수 있는 아이템 쿼리

애플리케이션에서는 In-app Billing Version 3 API를 사용하여 Google Play에서 아이템 세부정보를 쿼리할 수 있습니다. 인앱 결제 서비스로 요청을 전달하려면 먼저 "ITEM_ID_LIST" 키가 있는 상품 ID의 String ArrayList를 포함한 Bundle을 만듭니다. 여기서 각 문자열은 구매 가능한 아이템의 상품 ID입니다.

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

Google Play에서 이 정보를 검색하려면 In-app Billing Version 3 API에서 getSkuDetails 메서드를 호출하고 In-app Billing API 버전(“3”), 호출하는 앱의 패키지 이름, 구매 유형(“inapp”), 앞서 생성한 Bundle을 이 메서드에 전달합니다.

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

요청에 성공하는 경우, 반환되는 Bundle에는 BILLING_RESPONSE_RESULT_OK의 응답 코드(0)가 있습니다.

경고: 기본 스레드에서 getSkuDetails 메서드를 호출하면 안 됩니다. 이 메서드를 호출하면 기본 스레드를 차단할 수도 있는 네트워크 요청이 트리거됩니다. 대신 별도의 스레드를 만들어 그 스레드 내에서 getSkuDetails 메서드를 호출하세요.

Google Play에서 가능한 응답 코드를 전부 보려면 인앱 결제 참조를 확인하세요.

쿼리 결과는 DETAILS_LIST 키와 함께 String ArrayList에 저장됩니다. 구매 정보는 JSON 형식으로 String에 저장됩니다. 반환되는 제품 세부정보의 유형을 보려면 인앱 결제 참조를 확인하세요.

아래 예시에서는 이전의 스니펫에서 반환된 skuDetails 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;
   }
}

아이템 구매

앱에서 구매 요청을 시작하려면 인앱 결제 서비스에 대해 getBuyIntent 메서드를 호출합니다. In-app Billing API 버전("3"), 호출하는 앱의 패키지 이름, 구매할 아이템의 상품 ID, 구매 유형(“inapp” 또는 "subs"), developerPayload String을 메서드에 전달합니다. developerPayload String은 Google Play가 구매 정보와 함께 되돌려 보내도록 하려는 추가 인수를 지정하는 데 사용됩니다.

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

요청에 성공할 경우 반환되는 Bundle에는 응답 코드 BILLING_RESPONSE_RESULT_OK(0)와 구매 흐름을 시작하기 위해 사용할 수 있는 PendingIntent가 있습니다. Google Play에서 보내줄 가능성이 있는 응답 코드를 전부 보려면 인앱 결제 참조를 확인하세요. 다음으로, 키 BUY_INTENT로 응답 Bundle에서 PendingIntent를 추출합니다.

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

구매 트랜잭션을 완료하려면 startIntentSenderForResult 메서드를 호출하고 앞서 생성한 PendingIntent를 사용합니다. 아래 예시에서는 요청 코드에 임의의 값 1001을 사용하고 있습니다.

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

Google Play는 PendingIntent에 대한 응답을 애플리케이션의 onActivityResult 메서드로 보냅니다. onActivityResult 메서드의 결과 코드는 Activity.RESULT_OK(1) 또는 Activity.RESULT_CANCELED(0)가 됩니다. 응답 Intent에 반환되는 주문 유형 정보를 보려면 인앱 결제 참조를 확인하세요.

주문의 구매 데이터는 응답 IntentINAPP_PURCHASE_DATA 키에 매핑되는 JSON 형식의 String이며, 예를 들면 다음과 같습니다.

'{
   "orderId":"GPA.1234-5678-9012-34567",
   "packageName":"com.example.app",
   "productId":"exampleSku",
   "purchaseTime":1345678900000,
   "purchaseState":0,
   "developerPayload":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
   "purchaseToken":"opaque-token-up-to-1000-characters"
 }'

참고: Google Play에서는 구매를 위한 토큰이 생성됩니다. 이 토큰은 최대 1,000자에 이를 수 있는 불투명한 문자 시퀀스입니다. 구매 소비에 설명되어 있는 것처럼, 예컨대 구매를 소비할 때 이 전체 토큰을 다른 메서드로 전달합니다. 이 토큰을 단축하거나 잘라내지 마세요. 전체 토큰을 저장하고 반환해야 합니다.

이전 예시에서 계속 이어지는 내용으로, 응답 Intent에서 응답 코드, 구매 데이터, 서명을 받습니다.

@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();
          }
      }
   }
}

보안 권장 사항: 구매 요청을 보낼 때 구매 요청을 고유하게 식별하는 String 토큰을 생성하고 이 토큰을 developerPayload에 포함하세요. 임의로 생성된 문자열을 토큰으로 사용할 수 있습니다. Google Play에서 구매 응답을 받을 때 반환되는 데이터 서명, orderId, developerPayload String을 확인하세요. 보안 강화를 위해 자체 보안 서버를 점검해야 합니다. orderId가 이전에 처리한 적이 없는 고유한 값이고 developerPayload String이 이전에 구매 요청과 함께 보낸 토큰과 일치하는지 확인하세요.

구매 아이템 쿼리

사용자가 앱에서 수행한 구매에 대한 정보를 검색하려면 In-app Billing Version 3 서비스에서 getPurchases 메서드를 호출합니다. In-app Billing API 버전("3"), 호출하는 앱의 패키지 이름, 구매 유형(“inapp” 또는 "subs")을 메서드에 전달합니다.

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

Google Play 서비스는 현재 기기에 로그인되어 있는 사용자 계정을 통해 이루어진 구매만 반환합니다. 요청에 성공할 경우 반환되는 Bundle의 응답 코드는 0입니다. 응답 Bundle에는 상품 ID 목록, 각 주문에 대한 주문 세부정보의 목록, 각 주문을 위한 서명도 포함됩니다.

성능 개선을 위해, 인앱 결제 서비스는 getPurchase가 먼저 호출될 때 사용자가 소유한 상품을 최대 700개까지만 반환합니다. 사용자가 소유한 상품 수가 많을 경우 Google Play는 응답 Bundle에 키 INAPP_CONTINUATION_TOKEN으로 매핑되는 String 토큰을 포함하여 검색할 수 있는 상품이 더 있음을 나타냅니다. 그런 다음, 애플리케이션은 후속 getPurchases 호출을 수행하고 이 토큰을 인수로 전달할 수 있습니다. Google Play는 사용자가 소유한 모든 상품이 앱으로 전송 완료될 때까지 응답 Bundle에 연속 토큰을 계속 반환합니다.

getPurchases에서 반환되는 데이터에 대한 자세한 내용은 인앱 결제 참조를 확인하세요. 다음 예제에서는 응답에서 이 데이터를 검색할 수 있는 방법을 보여 줍니다.

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_LIST");
   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);

      // do something with this purchase information
      // e.g. display the updated list of products owned by user
   }

   // if continuationToken != null, call getPurchases again
   // and pass in the token to retrieve more items
}

구매 소비

In-app Billing Version 3 API를 사용하여 Google Play에서 구매한 인앱 상품의 소유권을 추적할 수 있습니다. 일단 인앱 상품에 대한 구매가 이루어지면 해당 상품은 "소유된" 상태로 간주되어 Google Play에서 구매할 수 없게 됩니다. 인앱 상품에 대한 소비 요청을 보내야 Google Play에서 해당 상품을 다시 구매할 수 있는 상태가 됩니다.

중요: 관리되는 인앱 상품은 소비 가능하지만, 구독은 그렇지 않습니다.

앱에서 소비 메커니즘을 사용하는 방법은 개발자의 손에 달려 있습니다. 일반적으로, 개발자는 사용자가 여러 번 구매하고 싶어할 수 있는 일시적 혜택(예: 게임 머니 또는 장비)을 받기 위한 소비 메커니즘을 구현하고자 할 것입니다. 개발자로서는, 사용자가 한 번 구매하면 영구적인 효과를 누릴 수 있는 인앱 상품(예: 프리미엄 업그레이드)에 대한 소비는 어지간하면 구현하고 싶지 않을 것입니다.

구매 소비를 기록하려면 consumePurchase 메서드를 인앱 결제 서비스로 보내고 제거할 구매를 식별하는 purchaseToken String 값을 전달합니다. purchaseToken은 Google Play 서비스에서 구매 요청에 성공한 후 INAPP_PURCHASE_DATA String을 통해 반환하는 데이터 중 일부입니다. 아래 예시에서는 token 변수에서 purchaseToken으로 식별되는 상품의 소비 내역을 기록합니다.

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

경고: 기본 스레드에서 consumePurchase 메서드를 호출하면 안 됩니다. 이 메서드를 호출하면 기본 스레드를 차단할 수도 있는 네트워크 요청이 트리거됩니다. 대신 별도의 스레드를 만들어 그 스레드 내에서 consumePurchase 메서드를 호출하세요.

사용자에게 인앱 상품을 프로비저닝하는 방식을 관리하고 추적할 책임은 개발자에게 있습니다. 예를 들어, 사용자가 게임 머니를 구매한 경우 플레이어의 인벤토리를 구매한 게임 머니 금액으로 업데이트해야 합니다.

보안 권장 사항: 사용자에게 소비성 인앱 구매 혜택을 프로비저닝하기 전에 먼저 소비 요청을 보내야 합니다. Google Play에서 성공적인 소비 응답을 받은 후에 아이템을 프로비저닝합니다.

구독 구현

구독에 대한 구매 흐름을 시작하는 것은 상품에 대한 구매 흐름을 시작하는 것과 비슷하지만, 상품 유형을 "subs"로 설정해야 한다는 점이 다릅니다. 인앱 상품의 경우에서와 정확히 똑같이, 액티비티의 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) {
   // Start purchase flow (this brings up the Google Play UI).
   // Result will be delivered through onActivityResult().
   startIntentSenderForResult(pendingIntent, RC_BUY, new Intent(),
       Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0));
}

활성 구독을 쿼리하려면 이때도 상품 유형 매개변수를 "subs"로 설정하여 getPurchases 메서드를 사용하세요.

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

이렇게 호출하면 사용자가 소유한 모든 활성 구독을 포함한 Bundle이 반환됩니다. 구독이 만료되고 갱신하지 않으면 반환되는 Bundle에 더 이상 구독이 표시되지 않습니다.

애플리케이션 보안

애플리케이션으로 전송되는 트랜잭션 정보의 무결성을 보장하기 위해, Google Play는 구매주문서(PO)에 대한 응답 데이터를 포함한 JSON 문자열에 서명합니다. Google Play는 Developer Console에서 애플리케이션과 연결된 개인 키를 사용하여 서명을 생성합니다. Developer Console은 각 애플리케이션에 대한 RSA 키 쌍을 생성합니다.

참고:이 키 쌍의 공개 키 부분을 찾으려면 Developer Console에서 애플리케이션의 세부정보를 연 후 Services & APIs를 클릭하고 Your License Key for This Application이라는 제목이 붙은 필드를 살펴보세요.

Google Play에서 생성되는 Base64 인코딩 RSA 공개 키는 바이너리 인코딩된 X.509 subjectPublicKeyInfo DER SEQUENCE 형식으로 되어 있습니다. 이는 Google Play 라이선스에 사용되는 것과 동일한 공개 키입니다.

애플리케이션이 이렇게 서명된 응답을 수신하면 RSA 키 쌍의 공개 키 부분을 사용하여 서명을 확인할 수 있습니다. 서명 확인을 수행함으로써 무단 변경되었거나 스푸핑된 응답을 탐지할 수 있습니다. 애플리케이션에서 이런 서명 확인 단계를 수행할 수 있지만, 애플리케이션이 보안 원격 서버에 연결하는 경우에는 그 서버에서 서명 확인을 수행하는 것이 좋습니다.

보안과 디자인의 모범 사례에 대한 자세한 내용은 보안 및 디자인을 참조하세요.