Google Play 결제 라이브러리 사용

이 문서에서는 Google Play 결제 라이브러리를 이용하여 앱에 Google Play 결제를 추가하는 방법을 설명합니다. 특히 일회성 제품, 리워드 제품, 구독 등 모든 인앱 상품 유형에 일반적인 Google Play 결제 기능을 추가하는 방법을 다룹니다. 인앱 상품 관련 기능을 앱에 추가하는 방법을 알아보려면 이 페이지 끝에 나열된 문서를 읽어보세요.

이 페이지를 읽기 전에 다음과 같은 작업을 해야 합니다.

  1. Google Play 결제 개요를 읽고 중요한 개념과 용어를 익힙니다.
  2. Google Play Console을 사용하여 인앱 상품을 구성합니다.

코드 스니펫 정보

이 가이드에서는 TrivialDrive v2 샘플 앱의 코드 스니펫을 사용합니다. 이 샘플에서는 Play 결제 라이브러리를 사용해 드라이브 게임용 인앱 상품을 구현하는 방법을 보여줍니다. 이 앱에서는 구매할 수 있는 상품을 표시하고 구매 절차를 시작하며 제품 소비를 기록하는 방법과 앱에 Google Play 결제를 추가하기 위해 알아야 할 기타 모든 것을 보여줍니다. 그림 1은 이 앱의 시작 화면을 나타낸 것입니다.

그림 1. Trivial Drive 앱의 시작 화면

앱에 Google Play 결제를 추가하는 단계

앱에 Google Play 결제를 추가하려면 다음 섹션의 단계를 따르세요.

앱의 종속성 업데이트

build.gradle 파일의 종속성 섹션에 다음 행을 추가합니다.

dependencies {
    ...
    implementation 'com.android.billingclient:billing:2.0.0'
}

Google Play 결제 라이브러리의 최신 버전을 사용 중인지 확인하려면 Google Play 결제 라이브러리 출시 노트를 참조하세요.

Google Play에 연결

Google Play 결제 요청을 하려면 먼저 다음 작업을 통해 Google Play에 연결을 설정해야 합니다.

  1. newBuilder()를 호출하여 BillingClient의 인스턴스를 만듭니다. 또한 PurchasesUpdatedListener에 관한 참조를 전달하는 setListener()를 호출하여 앱 및 Google Play 스토어에서 시작한 구매에 관한 업데이트를 받아야 합니다.

  2. Google Play 연결을 설정합니다. 설정 프로세스는 비동기적이며, 클라이언트 설정이 완료되고 추가로 요청할 준비가 되면 콜백을 받을 수 있도록 BillingClientStateListener를 구현해야 합니다.

  3. 클라이언트 연결이 끊어졌을 때 onBillingServiceDisconnected() 콜백 메서드를 재정의하고 끊어진 Google Play 연결을 처리할 수 있도록 자체 재시도 정책을 구현합니다. 예를 들어 Google Play 스토어 서비스가 백그라운드에서 업데이트되는 경우 BillingClient의 연결이 끊어질 수 있습니다. 이 경우 추가 요청을 하기 전에 BillingClient에서 startConnection() 메서드를 호출하여 연결을 다시 시작해야 합니다.

다음 코드 샘플은 연결을 시작하고 사용할 준비가 되었는지 테스트하는 방법을 나타냅니다.

Kotlin

lateinit private var billingClient: BillingClient
...
billingClient = BillingClient.newBuilder(context).setListener(this).build()
billingClient.startConnection(object : BillingClientStateListener {
   override fun onBillingSetupFinished(billingResult: BillingResult) {
       if (billingResult.responseCode == BillingResponse.OK) {
           // The BillingClient is ready. You can query purchases here.
       }
   }
   override fun onBillingServiceDisconnected() {
       // Try to restart the connection on the next request to
       // Google Play by calling the startConnection() method.
   }
})

자바

private BillingClient billingClient;
...
billingClient = BillingClient.newBuilder(activity).setListener(this).build();
billingClient.startConnection(new BillingClientStateListener() {
    @Override
    public void onBillingSetupFinished(BillingResult billingResult) {
        if (billingResult.getResponseCode() == BillingResponse.OK) {
            // The BillingClient is ready. You can query purchases here.
        }
    }
    @Override
    public void onBillingServiceDisconnected() {
        // Try to restart the connection on the next request to
        // Google Play by calling the startConnection() method.
    }
});

인앱 상품 세부정보 쿼리

인앱 상품을 구성할 때 만든 고유 제품 ID는 Google Play에 인앱 상품 세부정보를 비동기적으로 쿼리할 때 사용됩니다. Google Play에 인앱 상품 세부정보를 쿼리하려면 querySkuDetailsAsync()를 호출하세요. 이 메서드를 호출할 때 제품 ID 문자열 목록과 SkuType을 지정하는 SkuDetailsParams의 인스턴스를 전달합니다. SkuType은 일회성 제품이나 리워드 제품의 SkuType.INAPP 또는 구독의 SkuType.SUBS일 수 있습니다.

비동기 작업의 결과를 처리하려면 SkuDetailsResponseListener 인터페이스를 구현하는 리스너도 지정해야 합니다. 그러면 다음 샘플 코드에서와 같이 쿼리가 끝나면 리스너에게 알리는 onSkuDetailsResponse()를 재정의할 수 있습니다.

Kotlin

val skuList = ArrayList<String>()
skuList.add("premium_upgrade")
skuList.add("gas")
val params = SkuDetailsParams.newBuilder()
params.setSkusList(skuList).setType(SkuType.INAPP)
billingClient.querySkuDetailsAsync(params.build(), { billingResult, skuDetailsList ->
    // Process the result.
})

자바

List<String> skuList = new ArrayList<> ();
skuList.add("premium_upgrade");
skuList.add("gas");
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(SkuType.INAPP);
billingClient.querySkuDetailsAsync(params.build(),
    new SkuDetailsResponseListener() {
        @Override
        public void onSkuDetailsResponse(BillingResult billingResult,
                List<SkuDetails> skuDetailsList) {
            // Process the result.
        }
    });

앱에서는 이러한 목록을 APK와 번들로 묶거나 자체 보안 백엔드 서버에서 쿼리하여 자체 제품 ID 목록을 유지해야 합니다.

응답 코드를 검색하려면 BillingResult#getResponseCode()를 호출합니다. 요청이 성공하는 경우 응답 코드는 BillingResponse.OK입니다. Google Play의 가능한 다른 응답 코드의 목록은 BillingClient.BillingResponse를 참조하세요.

오류가 발생하면 BillingResult#getDebugMessage()를 사용해 연관된 오류 메시지를 볼 수 있습니다.

Google Play 결제 라이브러리는 SkuDetails 개체의 List에 쿼리 결과를 저장합니다. 그러면 목록에 있는 각 SkuDetails 개체에서 다양한 메서드를 호출하여 제품 가격이나 설명과 같이 인앱 상품에 관한 정보를 볼 수 있습니다. 이용 가능한 제품 세부정보를 보려면 SkuDetails 클래스의 메서드 목록을 참조하세요.

다음 예는 이전 코드 스니펫에서 반환한 SkuDetails 개체를 사용해 인앱 상품의 가격을 검색하는 방법을 나타냅니다.

Kotlin

if (result.responseCode == BillingResponse.OK && skuDetailsList != null) {
    for (skuDetails in skuDetailsList) {
        val sku = skuDetails.sku
        val price = skuDetails.price
        if ("premium_upgrade" == sku) {
            premiumUpgradePrice = price
        } else if ("gas" == sku) {
            gasPrice = price
        }
    }
}

자바

if (result.getResponseCode() == BillingResponse.OK && skuDetailsList != null) {
   for (SkuDetails skuDetails : skuDetailsList) {
       String sku = skuDetails.getSku();
       String price = skuDetails.getPrice();
       if ("premium_upgrade".equals(sku)) {
           premiumUpgradePrice = price;
       } else if ("gas".equals(sku)) {
           gasPrice = price;
       }
   }
}

제품 가격은 사용자가 거주 중인 국가에 따라 다르므로 사용자가 제품을 구매하기 전에 제품 가격을 검색하는 단계는 중요합니다. Trivial Drive 앱에서는 그림 2에서와 같이 모든 인앱 상품을 목록으로 표시합니다.

그림 2. Trivial Drive 인앱 상품 화면

일관된 혜택

할인된 SKU를 제공하는 경우 Google Play에서는 SKU의 원래 가격을 반환하므로 사용자에게 할인을 받고 있음을 보여줄 수 있습니다. getPrice()를 사용해 사용자에게 할인된 가격을 표시하고 getOriginalPrice()를 사용해 상품의 원래 가격을 표시하는 것이 좋습니다.

SkuDetails에는 원래 SKU 가격을 검색하는 다음 2개의 메서드가 포함되어 있습니다.

인앱 상품의 구매 사용 설정

일부 Android 스마트폰에는 구독과 같은 특정 상품 유형을 지원하지 않는 Google Play 스토어 앱의 이전 버전이 포함되어 있을 수 있습니다. 따라서 앱에서 결제 절차를 시작하기 전에 isFeatureSupported()를 호출하여 판매하려는 제품을 기기에서 지원하는지 확인하세요. 상품 유형의 목록은 BillingClient.FeatureType을 참조하세요.

앱에서 구매 요청을 시작하려면 UI 스레드에서 launchBillingFlow() 메서드를 호출합니다. 상품의 제품 ID(skuId) 및 상품 유형(일회성 제품이나 리워드 제품인 경우 SkuType.INAPP, 구독인 경우 SkuType.SUBS)과 같이 관련 데이터가 포함된 BillingFlowParams 개체에 관한 참조를 전달하여 구매를 완료합니다. BillingFlowParams의 인스턴스를 가져오려면 BillingFlowParams.Builder 클래스를 사용하세요.

Kotlin

// Retrieve a value for "skuDetails" by calling querySkuDetailsAsync().
val flowParams = BillingFlowParams.newBuilder()
        .setSkuDetails(skuDetails)
        .build()
val responseCode = billingClient.launchBillingFlow(activity, flowParams)

자바

// Retrieve a value for "skuDetails" by calling querySkuDetailsAsync().
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
        .setSkuDetails(skuDetails)
        .build();
int responseCode = billingClient.launchBillingFlow(flowParams);

launchBillingFlow() 메서드를 호출하면 시스템에서 Google Play 구매 화면을 표시합니다. 그림 3은 일회성 제품의 구매 화면을 나타낸 것입니다.

그림 3. 일회성 제품의 Google Play 구매 화면

그림 4는 구독의 구매 화면을 나타낸 것입니다.

그림 4. 구독의 Google Play 구매 화면

launchBillingFlow() 메서드는 BillingClient.BillingResponse에 표시된 여러 응답 코드 중 하나를 반환합니다. Google Play에서는 onPurchasesUpdated() 메서드를 호출하여 PurchasesUpdatedListener 인터페이스를 구현하는 리스너에 구매 작업의 결과를 제공합니다. 리스너는 앞서 Google Play에 연결 섹션에서 설명된 것처럼 setListener() 메서드를 사용해 지정됩니다.

가능한 응답 코드를 처리하려면 onPurchasesUpdated() 메서드를 구현해야 합니다. 다음 코드 스니펫은 onPurchasesUpdated() 메서드를 재정의하는 방법을 나타냅니다.

Kotlin

override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
   if (billingResult.responseCode == BillingResponse.OK && purchases != null) {
       for (purchase in purchases) {
           handlePurchase(purchase)
       }
   } else if (billingResult.responseCode == BillingResponse.USER_CANCELED) {
       // Handle an error caused by a user cancelling the purchase flow.
   } else {
       // Handle any other error codes.
   }
}

자바

@Override
void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
    if (billingResult.getResponseCode() == BillingResponse.OK
            && purchases != null) {
        for (Purchase purchase : purchases) {
            handlePurchase(purchase);
        }
    } else if (billingResult.getResponseCode() == BillingResponse.USER_CANCELED) {
        // Handle an error caused by a user cancelling the purchase flow.
    } else {
        // Handle any other error codes.
    }
}

구매에 성공하면 그림 5의 화면과 유사한 Google Play 성공 화면이 표시됩니다.

그림 5. Google Play 성공 화면

구매에 성공하면 사용자 및 사용자가 구매한 인앱 상품의 제품 ID를 나타내는 고유 식별자인 구매 토큰도 생성됩니다. 앱에서는 구매 토큰을 로컬에 저장하거나, 이상적으로는 구매 토큰을 보안 백엔드 서버로 전달하여 사기로부터 보호하고 구매를 확인하는 데 사용되도록 할 수 있습니다. 구매 토큰은 모든 일회성 제품 및 리워드 제품마다 고유합니다. 하지만 구독은 한 번 구매하면 정기적인 결제 기간에 자동으로 갱신되므로, 구매의 구매 토큰은 각 결제 기간 동안 동일하게 유지됩니다.

또한 사용자는 주문 ID 또는 거래의 고유 ID가 포함된 거래 영수증을 이메일로 받습니다. 또한 일회성 제품을 구매할 때마다 고유 주문 ID가 포함된 이메일을 받으며 구독을 처음 구매하고 이후 반복하여 자동 갱신될 때도 같은 이메일을 받습니다. 주문 ID를 사용해 Google Play Console에서 환불을 관리할 수 있습니다. 자세한 내용은 앱 주문 및 구독 보기 및 환불을 참조하세요.

구매 확인

Google Play에서는 앱 내부(인앱)나 앱 외부에서 제품 구매를 지원합니다. 사용자가 제품을 구매하는 위치와 상관없이 Google Play에서 일관된 구매 환경을 보장하려면 사용자에게 자격을 부여한 후 최대한 빨리 Google Play 결제 라이브러리를 통해 수신된 모든 구매를 확인해야 합니다. 3일 이내에 구매를 확인하지 않으면 사용자에게 자동으로 환불되고 Google Play에서 구매를 취소합니다. 대기 중인 거래에서 구매가 PENDING 상태인 경우에는 3일의 구매 확인 기간이 적용되지 않으며, SUCCESS 상태가 되면 적용되기 시작합니다.

다음 메서드 중 하나를 사용해 구매를 확인할 수 있습니다.

  • 소비성 제품인 경우 클라이언트 API에 있는 consumeAsync()를 사용합니다.
  • 소비성 제품이 아닌 경우 클라이언트 API에 있는 acknowledgePurchase()를 사용합니다.
  • 서버 API에서 새로운 acknowledge() 메서드를 사용할 수도 있습니다.

구독의 경우 새 구매 토큰이 포함된 모든 구매를 확인해야 합니다. 즉, 모든 초기 구매, 계획 변경, 재가입은 확인을 받아야 하지만 이후 갱신은 확인할 필요가 없습니다. 구매의 확인 필드를 확인하면 구매에 확인이 필요한지 파악할 수 있습니다.

Purchase 개체에는 구매가 확인되었는지 나타내는 isAcknowledged() 메서드가 포함되어 있습니다. 또한 서버측 API에는 Product.purchases.get()Product.subscriptions.get()용 확인 부울 값이 포함되어 있습니다. 구매를 확인하기 전에 이러한 메서드를 사용하여 구매가 이미 확인되었는지 파악할 수 있습니다.

다음 예는 구독 구매를 확인하는 방법을 나타냅니다.

Kotlin

val client: BillingClient = ...
val acknowledgePurchaseResponseListener: AcknowledgePurchaseResponseListener = ...

fun handlePurchase() {
    if (purchase.state === PurchaseState.PURCHASED) {
        // Grant entitlement to the user.
        ...

        // Acknowledge the purchase if it hasn't already been acknowledged.
        if (!purchase.isAcknowledged) {
            val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                    .setPurchaseToken(purchase.purchaseToken)
                    .build()
            client.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener)
        }
     }
}

자바

BillingClient client = ...
AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = ...

void handlePurchase(Purchase purchase) {
    if (purchase.getState() == PurchaseState.PURCHASED) {
        // Grant entitlement to the user.
        ...

        // Acknowledge the purchase if it hasn't already been acknowledged.
        if (!purchase.isAcknowledged()) {
            AcknowledgePurchaseParams acknowledgePurchaseParams =
                AcknowledgePurchaseParams.newBuilder()
                    .setPurchaseToken(purchase.getPurchaseToken())
                    .build();
            client.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
        }
    }
}

대기 중인 거래 지원

Google Play 결제 솔루션을 구현할 때는 자격을 부여하기 전에 추가 작업이 필요한 구매를 지원해야 합니다. 예를 들어 사용자가 오프라인 상점에서 현금으로 인앱 상품을 구매할 수도 있습니다. 즉, 이 거래는 앱 외부에서 완료됩니다. 이 시나리오에서는 사용자가 거래를 완료한 후에만 자격을 부여해야 합니다.

대기 중인 구매를 사용 설정하려면 앱을 초기화할 때 enablePendingPurchases()를 호출하세요. enablePendingPurchases()를 호출하지 않으면 Google Play 결제 라이브러리를 인스턴스화할 수 없습니다.

Purchase.getState() 메서드를 사용하면 구매 상태가 PURCHASED인지 또는 PENDING인지 확인할 수 있습니다. 상태가 PURCHASED인 경우에만 자격을 부여해야 합니다. 상태 변경은 다음과 같이 확인할 수 있습니다.

  1. 앱을 시작할 때 BillingClient.queryPurchases()를 호출하여 사용자와 연관된 소비되지 않은 제품의 목록을 가져온 다음 반환된 각 Purchase 개체에서 getState()를 호출합니다.
  2. onPurchasesUpdated() 메서드를 구현하여 Purchase 개체의 변경에 응답합니다.

다음 예는 대기 중인 거래를 처리할 수 있는 방법을 나타냅니다.

Kotlin

fun handlePurchase(purchase: Purchase) {
    if (purchase.state == PurchaseState.PURCHASED) {
        // Grant the item to the user, and then acknowledge the purchase
    } else if (purchase.state == PurchaseState.PENDING) {
        // Here you can confirm to the user that they've started the pending
        // purchase, and to complete it, they should follow instructions that
        // are given to them. You can also choose to remind the user in the
        // future to complete the purchase if you detect that it is still
        // pending.
    }
}

자바

void handlePurchase(Purchase purchase) {
    if (purchase.getState() == PurchaseState.PURCHASED) {
        // Acknowledge purchase and grant the item to the user
    } else if (purchase.getState() == PurchaseState.PENDING) {
        // Here you can confirm to the user that they've started the pending
        // purchase, and to complete it, they should follow instructions that
        // are given to them. You can also choose to remind the user in the
        // future to complete the purchase if you detect that it is still
        // pending.
    }
}

라이선스 테스터로 대기 중인 거래 테스트

대기 중인 거래는 라이선스 테스터를 사용해 테스트할 수 있습니다. 라이선스 테스터는 2개의 테스트 신용카드 외에도 몇 분 후에 자동으로 완료되거나 취소되는 지연된 결제 방법을 위한 2개의 테스트 도구에 액세스할 수 있습니다.

애플리케이션을 테스트하는 동안 애플리케이션에서 이 두 도구 중 하나를 사용할 때 구매 직후 자격을 부여하거나 구매를 확인하지 않는지 확인해야 합니다. 자동으로 완료되는 테스트 도구를 사용해 구매할 때는 구매가 완료된 후 애플리케이션에서 자격을 부여하고 구매를 확인하는지 확인해야 합니다.

개발자 페이로드 첨부

구매에 임의의 문자열 또는 개발자 페이로드를 첨부할 수 있습니다. 하지만 개발자 페이로드는 구매가 확인되거나 소비된 경우에만 첨부할 수 있습니다. 이는 구매 절차를 시작할 때 페이로드를 지정할 수 있는 AIDL의 개발자 페이로드와 다른 요소입니다.

소비성 제품의 경우 다음 예에서와 같이 consumeAsync()에서 개발자 페이로드 필드가 포함된 ConsumeParams 개체를 받습니다.

Kotlin

val client: BillingClient = ...
val listener: ConsumeResponseListener = ...

val consumeParams =
    ConsumeParams.newBuilder()
        .setPurchaseToken(/* token */)
        .setDeveloperPayload(/* payload */)
        .build()

client.consumeAsync(consumeParams, listener)

자바

BillingClient client = ...
ConsumeResponseListener listener = ...

ConsumeParams consumeParams =
    ConsumeParams.newBuilder()
        .setPurchaseToken(/* token */)
        .setDeveloperPayload(/* payload */)
        .build();

client.consumeAsync(consumeParams, listener);

소비성 제품이 아닌 경우 다음 예에서와 같이 acknowledgePurchase()에서 개발자 페이로드 필드가 포함된 AcknowledgePurchaseParams 개체를 받습니다.

Kotlin

val client: BillingClient = ...
val listener: AcknowledgePurchaseResponseListener = ...

val acknowledgePurchaseParams =
    AcknowledgePurchaseParams.newBuilder()
        .setPurchaseToken(/* token */)
        .setDeveloperPayload(/* payload */)
        .build()

client.acknowledgePurchase(acknowledgePurchaseParams, listener)

자바

BillingClient client = ...
AcknowledgePurchaseResponseListener listener = ...

AcknowledgePurchaseParams acknowledgePurchaseParams =
    AcknowledgePurchaseParams.newBuilder()
        .setPurchaseToken(/* token */)
        .setDeveloperPayload(/* payload */)
        .build();

client.acknowledgePurchase(acknowledgePurchaseParams, listener);

개발자 페이로드에 액세스하려면 해당하는 Purchase 개체에서 getDeveloperPayload()를 호출하세요.

구매 인증

사용자에게 구매한 제품의 액세스 권한을 부여하기 전에 onPurchasesUpdated()에서 앱이 받는 구매 세부정보를 인증해야 합니다.

서버에서 구매 인증

서버에서 구매 인증 로직을 구현하여 APK를 리버스 엔지니어링하고 인증 로직을 끄려는 공격자로부터 앱을 보호할 수 있습니다. 보안 백엔드 서버에서 구매 세부정보를 인증하려면 다음 단계를 완료하세요.

  1. 앱에서 구매 토큰과 사용자 계정 사용자 인증 정보를 보안 백엔드 서버로 보냅니다. 보안 백엔드 서버에서는 인증을 완료한 후 구매를 사용자와 연결해야 합니다.

  2. 앱에서 토큰을 가져오면 다음과 같이 진행됩니다.

    1. Google Play Developer API의 구독 및 구매 부분을 사용해 GET 요청을 실행하여 Google Play에서 구매 세부정보(일회성 제품 또는 리워드 제품인 경우 Purchases.products, 구독인 경우 Purchases.subscriptions)를 검색합니다. GET 요청에는 앱 패키지 이름, 제품 ID, 토큰(구매 토큰)이 포함됩니다.

    2. Google Play에서 구매 세부정보를 반환합니다.

    3. 보안 백엔드 서버에서 주문 ID가 이전 구매를 나타내지 않는 고유한 값인지 확인합니다.

    4. 보안 백엔드 서버는 1단계에서 받은 사용자 계정 사용자 인증 정보를 사용해 구매가 이뤄진 앱 인스턴스의 사용자와 구매 토큰을 연결합니다.

    5. (선택사항) 구독의 유효성을 검사 중이고 구독이 업그레이드 또는 다운그레이드되는 중이거나 구독이 만료되기 전에 사용자가 다시 구독한 경우 linkedPurchaseToken 필드를 확인합니다. Purchases.subscriptions 리소스의 linkedPurchaseToken 필드에는 이전 또는 '원본' 구매의 토큰이 포함되어 있습니다. linkedPurchaseToken에 관한 자세한 내용은 Purchases.subscriptions를 참조하세요.

    6. 인앱 상품이 사용자에게 제공됩니다.

기기에서 구매 인증

자체 서버를 운영할 수 없어도 Android 앱 내에서 구매 세부정보의 유효성을 검사할 수 있습니다.

Google Play에서는 애플리케이션으로 전송되는 거래 정보의 무결성을 보장하기 위해 구매의 응답 데이터가 포함된 JSON 문자열에 서명합니다. 또한 Google Play는 Play Console에서 애플리케이션과 연결된 비공개 키를 사용해 이 서명을 생성합니다. Play Console에서는 각 애플리케이션의 RSA 키 쌍을 생성합니다. 이 응답 JSON을 Purchase 클래스 내 getOriginalJson() 메서드를 사용해 가져옵니다.

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

애플리케이션에서 이렇게 서명된 응답을 받으면 RSA 키 쌍의 공개 키 부분을 사용해 서명을 확인할 수 있습니다. 서명 확인을 통해 조작되었거나 위장된 응답을 감지할 수 있습니다.

공격자가 보안 프로토콜 및 기타 애플리케이션 구성요소를 리버스 엔지니어링하기 어렵도록 Google Play 공개 키와 Google Play 결제 코드를 난독화해야 합니다. 최소한 코드에서 Proguard와 같은 난독화 도구를 실행하는 것이 좋습니다. Proguard를 사용해 코드를 난독화하려면 Proguard 구성 파일에 다음 행을 추가해야 합니다.

-keep class com.android.vending.billing.**

Google Play 공개 키와 Google Play 결제 코드를 난독화한 후 앱에서 구매 세부정보의 유효성을 검사하도록 할 수 있습니다. 앱에서 서명을 인증할 때 앱의 키가 관련 서명에 포함된 JSON 데이터에 서명했는지 확인하세요.

구매 내역을 최신 상태로 유지

사용자가 구매한 내역을 추적하지 못할 수 있습니다. 다음은 앱에서 구매 내역을 추적하지 못할 수 있으며 구매 쿼리가 중요한 두 가지 시나리오입니다.

서버 중단 처리

  1. 사용자가 드라이브 게임의 추가 가스와 같은 일회성 제품을 구입합니다.
  2. 앱에서 확인을 위해 보안 백엔드 서버에 구매 토큰을 보냅니다.
  3. 서버가 일시적으로 다운됩니다.
  4. 앱에서 서버가 다운되었음을 인식하고 사용자에게 구매에 문제가 발생했다고 알립니다.
  5. Android 앱에서 보안 백엔드 서버에 구매 토큰 전송을 다시 시도하고 서버가 복원되는 즉시 구매를 완료합니다.
  6. 앱에서 콘텐츠를 사용할 수 있게 합니다.

여러 기기 처리

  1. 사용자가 자신의 Android 스마트폰에서 구독을 구입합니다.
  2. 앱에서 확인을 위해 보안 백엔드 서버에 구매 토큰을 보냅니다.
  3. 서버에서 구매 토큰을 확인합니다.
  4. 앱에서 콘텐츠를 사용할 수 있게 합니다.
  5. 사용자가 Android 태블릿으로 전환하여 구독을 사용합니다.
  6. 새 기기의 앱에서 업데이트된 구매 목록을 쿼리합니다.
  7. 앱에서 구독을 인식하고 태블릿에 구독 액세스 권한을 부여합니다.

캐시된 구매 쿼리

사용자가 앱에서 진행한 구매 정보를 검색하려면 다음 예에서와 같이 BillingClient에서 구매 유형(SkuType.INAPP 또는 SkuType.SUBS)으로 queryPurchases()를 호출합니다.

Kotlin

val purchasesResult: PurchasesResult =
        billingClient.queryPurchases(SkuType.INAPP)

자바

PurchasesResult purchasesResult = billingClient.queryPurchases(SkuType.INAPP);

Google Play에서 기기에 로그인된 사용자 계정이 구매한 내역을 반환합니다. 요청이 성공하면 Play 결제 라이브러리에서 Purchase 개체의 List에 쿼리 결과를 저장합니다.

목록을 검색하려면 PurchasesResult에서 getPurchasesList()를 호출하세요. 그런 다음 Purchase 개체에서 다양한 메서드를 호출하여 구매 상태나 시간과 같이 상품에 관한 정보를 볼 수 있습니다. 사용할 수 있는 제품 세부정보 유형을 보려면 Purchase 클래스의 메서드 목록을 참조하세요.

코드에서 queryPurchases()를 두 번 이상 호출해야 합니다.

  • 앱이 마지막으로 중지된 이후 사용자가 구매한 모든 내역을 복원할 수 있도록 앱이 시작할 때마다 queryPurchases()를 호출합니다.
  • 앱이 백그라운드에 있을 때 사용자가 구매(예: Google Play 스토어 앱에서 프로모션 코드 사용)할 수 있으므로 onResume() 메서드에서 queryPurchases()를 호출합니다.

시작할 때와 다시 시작할 때 queryPurchases()를 호출하면 앱이 실행되지 않았을 때 사용자가 진행했을 수 있는 모든 구매 및 사용 내역을 앱에서 찾을 수 있습니다. 또한 앱이 실행 중일 때 사용자가 구매했는데 어떤 이유로든 앱에서 사용자의 구매를 놓친 경우 다음에 활동이 재개되고 queryPurchases()를 호출하면 앱에서 구매 내역을 파악할 수 있습니다.

최근 구매 내역 쿼리

queryPurchases() 메서드는 네트워크 요청을 시작하지 않고 Google Play 스토어 앱의 캐시를 사용합니다. 각 제품 ID와 관련하여 사용자의 최근 구매 내역을 확인해야 하는 경우 queryPurchaseHistoryAsync()를 사용해 구매 유형과 PurchaseHistoryResponseListener를 전달하여 쿼리 결과를 처리할 수 있습니다.

queryPurchaseHistoryAsync()PurchaseHistory 개체를 반환합니다. 이 개체에는 구매가 만료, 취소 또는 소비된 경우에도 사용자가 최근 완료한 구매에 관한 정보가 제품 ID별로 포함되어 있습니다. queryPurchases()는 로컬 캐시를 사용하므로 가능한 경우 queryPurchaseHistoryAsync() 대신 사용하세요. queryPurchaseHistoryAsync()를 사용하면 사용자가 구매 목록을 업데이트할 수 있도록 새로고침 버튼과 결합할 수 있습니다.

다음 코드는 onPurchaseHistoryResponse() 메서드를 어떻게 재정의할 수 있는지 나타냅니다.

Kotlin

billingClient.queryPurchaseHistoryAsync(SkuType.INAPP, { billingResult, purchasesList ->
   if (billingResult.responseCode == BillingResponse.OK && purchasesList != null) {
       for (purchase in purchasesList) {
           // Process the result.
       }
   }
})

자바

billingClient.queryPurchaseHistoryAsync(SkuType.INAPP,
                                         new PurchaseHistoryResponseListener() {
    @Override
    public void onPurchaseHistoryResponse(BillingResult billingResult,
                                          List<Purchase> purchasesList) {
        if (billingResult.getResponseCode() == BillingResponse.OK
                && purchasesList != null) {
            for (Purchase purchase : purchasesList) {
                // Process the result.
            }
         }
    }
});

다음 단계

사용자가 제품을 구입할 수 있도록 한 후에는 제품별 시나리오를 다루는 방법을 알아야 합니다.