앱에 Google Play 결제 라이브러리 통합

이 주제에서는 제품 판매를 시작하기 위해 Google Play 결제 라이브러리를 앱에 통합하는 방법을 설명합니다.

구매 진행 과정

다음은 일회성 구매 또는 정기 결제의 일반적인 구매 흐름입니다.

  1. 사용자에게 구입할 수 있는 항목을 보여줍니다.
  2. 사용자가 구매를 수락할 수 있도록 구매 흐름을 시작합니다.
  3. 서버에서 구매를 인증합니다.
  4. 사용자에게 콘텐츠를 제공합니다.
  5. 콘텐츠 전송을 확인합니다. 소비성 제품의 경우 사용자가 항목을 다시 구매할 수 있도록 구매 제품을 소비합니다.

정기 결제는 취소될 때까지 자동으로 갱신됩니다. 정기 결제는 다음 상태를 거칠 수 있습니다.

  • 활성: 사용자가 콘텐츠 사용에 문제가 없는 양호한 상태이며 정기 결제에 액세스할 수 있습니다.
  • 취소됨: 사용자가 정기 결제를 취소했지만 만료 시까지 계속 액세스할 수 있습니다.
  • 유예 기간 중: 사용자에게 결제 문제가 발생했지만 Google에서 결제 수단을 다시 시도하는 동안 사용자가 계속 액세스할 수 있습니다.
  • 보류 중: 사용자에게 결제 문제가 발생하여 Google에서 결제 수단을 다시 시도하는 동안 사용자가 더 이상 액세스할 수 없습니다.
  • 일시중지됨: 사용자가 액세스를 일시중지했으며 다시 시작할 때까지 액세스할 수 없습니다.
  • 만료됨: 사용자가 정기 결제를 취소했으며 정기 결제 액세스 권한을 잃었습니다. 만료 시 사용자가 이탈한 것으로 간주합니다.

Google Play 연결 초기화

Google Play 결제 시스템과 통합하는 첫 번째 단계는 Google Play 결제 라이브러리를 앱에 추가하고 연결을 초기화하는 것입니다.

Google Play 결제 라이브러리 종속 항목 추가

다음과 같이 앱의 build.gradle 파일에 Google Play 결제 라이브러리 종속 항목을 추가합니다.

Groovy

dependencies {
    def billing_version = "6.2.0"

    implementation "com.android.billingclient:billing:$billing_version"
}

Kotlin

dependencies {
    val billing_version = "6.2.0"

    implementation("com.android.billingclient:billing:$billing_version")
}

Kotlin을 사용한다면 Google Play 결제 라이브러리 KTX 모듈에 Kotlin 확장 프로그램과 코루틴 지원이 포함되어 있으므로 Google Play 결제 라이브러리를 사용할 때 직관적인 Kotlin을 작성할 수 있습니다. 프로젝트에서 이러한 확장 프로그램을 포함하려면 다음과 같이 앱의 build.gradle 파일에 다음 종속 항목을 추가합니다.

Groovy

dependencies {
    def billing_version = "6.2.0"

    implementation "com.android.billingclient:billing-ktx:$billing_version"
}

Kotlin

dependencies {
    val billing_version = "6.2.0"

    implementation("com.android.billingclient:billing-ktx:$billing_version")
}

BillingClient 초기화

Google Play 결제 라이브러리의 종속 항목을 추가한 후에는 BillingClient 인스턴스를 초기화해야 합니다. BillingClient는 Google Play 결제 라이브러리와 앱의 나머지 부분 간의 통신을 위한 기본 인터페이스입니다. BillingClient는 여러 일반적인 결제 작업을 위한 편의 메서드(동기식 및 비동기식 모두 포함)를 제공합니다. 단일 이벤트에 관한 여러 개의 PurchasesUpdatedListener 콜백이 발생하는 것을 피할 수 있도록 한 번에 활성 BillingClient 연결을 하나만 열어 두는 것이 좋습니다.

BillingClient를 생성하려면 newBuilder()를 사용합니다. newBuilder()에 모든 컨텍스트를 전달할 수 있으며 BillingClient는 이 컨텍스트를 사용하여 애플리케이션 컨텍스트를 가져옵니다. 따라서 메모리 누수를 걱정할 필요가 없습니다. 구매 관련 업데이트를 수신하려면 setListener()를 호출하여 PurchasesUpdatedListener에 대한 참조도 전달해야 합니다. 이 리스너는 앱의 모든 구매 관련 업데이트를 수신합니다.

Kotlin

private val purchasesUpdatedListener =
   PurchasesUpdatedListener { billingResult, purchases ->
       // To be implemented in a later section.
   }

private var billingClient = BillingClient.newBuilder(context)
   .setListener(purchasesUpdatedListener)
   .enablePendingPurchases()
   .build()

Java

private PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
    @Override
    public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
        // To be implemented in a later section.
    }
};

private BillingClient billingClient = BillingClient.newBuilder(context)
    .setListener(purchasesUpdatedListener)
    .enablePendingPurchases()
    .build();

Google Play에 연결

BillingClient를 만든 후 Google Play에 연결해야 합니다.

Google Play에 연결하려면 startConnection()을 호출합니다. 연결 프로세스는 비동기적이며, 클라이언트 설정이 완료되고 추가로 요청할 준비가 되면 BillingClientStateListener를 구현하여 콜백을 수신해야 합니다.

또한 Google Play와 연결이 끊어진 문제를 처리하려면 재시도 로직을 구현해야 합니다. 재시도 로직을 구현하려면 onBillingServiceDisconnected() 콜백 메서드를 재정의해야 합니다. 그리고 추가 요청을 하기 전에 BillingClientstartConnection() 메서드를 호출하여 Google Play에 다시 연결하는지 확인해야 합니다.

다음 예는 연결을 시작하고 사용 준비가 되었는지 테스트하는 방법을 보여줍니다.

Kotlin

billingClient.startConnection(object : BillingClientStateListener {
    override fun onBillingSetupFinished(billingResult: BillingResult) {
        if (billingResult.responseCode ==  BillingResponseCode.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.
    }
})

Java

billingClient.startConnection(new BillingClientStateListener() {
    @Override
    public void onBillingSetupFinished(BillingResult billingResult) {
        if (billingResult.getResponseCode() ==  BillingResponseCode.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.
    }
});

구매 가능한 제품 표시

Google Play에 연결했다면 구매 가능한 제품을 쿼리하여 사용자에게 표시할 준비가 된 것입니다.

제품 세부정보 쿼리는 현지화된 제품 정보를 반환하므로 사용자에게 제품을 표시하기에 앞서 진행해야 하는 중요한 단계입니다. 정기 결제의 경우 제품 디스플레이가 모든 Play 정책을 준수해야 합니다.

인앱 상품 세부정보를 쿼리하려면 queryProductDetailsAsync()를 호출합니다.

비동기 작업의 결과를 처리하려면 ProductDetailsResponseListener 인터페이스를 구현하는 리스너도 지정해야 합니다. 그런 후 다음 예에서와 같이 쿼리가 완료되면 리스너에 알리는 onProductDetailsResponse()를 재정의할 수 있습니다.

Kotlin

val queryProductDetailsParams =
    QueryProductDetailsParams.newBuilder()
        .setProductList(
            ImmutableList.of(
                Product.newBuilder()
                    .setProductId("product_id_example")
                    .setProductType(ProductType.SUBS)
                    .build()))
        .build()

billingClient.queryProductDetailsAsync(queryProductDetailsParams) {
    billingResult,
    productDetailsList ->
      // check billingResult
      // process returned productDetailsList
}

Java

QueryProductDetailsParams queryProductDetailsParams =
    QueryProductDetailsParams.newBuilder()
        .setProductList(
            ImmutableList.of(
                Product.newBuilder()
                    .setProductId("product_id_example")
                    .setProductType(ProductType.SUBS)
                    .build()))
        .build();

billingClient.queryProductDetailsAsync(
    queryProductDetailsParams,
    new ProductDetailsResponseListener() {
        public void onProductDetailsResponse(BillingResult billingResult,
                List<ProductDetails> productDetailsList) {
            // check billingResult
            // process returned productDetailsList
        }
    }
)

제품 세부정보를 쿼리할 때는 ProductType과 Google Play Console에서 생성한 제품 ID 문자열 목록을 함께 지정하는 QueryProductDetailsParams 인스턴스를 전달합니다. ProductType은 일회성 제품의 경우 ProductType.INAPP, 정기 결제의 경우 ProductType.SUBS가 될 수 있습니다.

Kotlin 확장 프로그램을 사용하여 쿼리

Kotlin 확장 프로그램을 사용하는 경우 queryProductDetails() 확장 함수를 호출하여 인앱 상품 세부정보를 쿼리할 수 있습니다.

queryProductDetails()는 Kotlin 코루틴을 활용하므로 별도의 리스너를 정의할 필요가 없습니다. 대신 쿼리가 완료될 때까지 함수가 정지되고 그 후에 결과를 처리할 수 있습니다.

suspend fun processPurchases() {
    val productList = listOf(
        QueryProductDetailsParams.Product.newBuilder()
            .setProductId("product_id_example")
            .setProductType(BillingClient.ProductType.SUBS)
            .build()
    )
    val params = QueryProductDetailsParams.newBuilder()
    params.setProductList(productList)

    // leverage queryProductDetails Kotlin extension function
    val productDetailsResult = withContext(Dispatchers.IO) {
        billingClient.queryProductDetails(params.build())
    }

    // Process the result.
}

드물지만 일부 기기는 Google Play 서비스 버전이 오래되어 ProductDetailsqueryProductDetailsAsync()를 지원하지 않습니다. 이 경우를 적절하게 지원하려면 Play 결제 라이브러리 5 이전 가이드에서 이전 버전과의 호환성 기능을 사용하는 방법을 알아보세요.

결과 처리

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

판매할 항목을 제공하기 전에 사용자가 그 항목을 이미 소유하고 있지 않은지 확인합니다. 사용자의 항목 라이브러리에 소비성 항목이 여전히 있다면 사용자가 항목을 다시 구매하기 전에 먼저 항목을 소비해야 합니다.

정기 결제를 제공하기 전에 사용자가 이미 정기 결제하지 않았는지 확인합니다. 또한 다음을 참고하세요.

  • queryProductDetailsAsync()는 정기 결제 제품 세부정보와 정기 결제당 최대 50개의 혜택을 반환합니다.
  • queryProductDetailsAsync()는 사용자가 대상이 되는 혜택만 반환합니다. 사용자가 사용할 수 없는 혜택을 구매하려고 하면(예: 앱에서 운영하지 않는 오래된 혜택 목록을 표시하는 경우) Play는 사용자에게 혜택을 사용할 수 없다고 알립니다. 대신 사용자는 기본 요금제 구매를 선택할 수 있습니다.

구매 흐름 시작

앱에서 구매 요청을 시작하려면 앱의 기본 스레드에서 launchBillingFlow() 메서드를 호출합니다. 이 메서드는 queryProductDetailsAsync() 호출에서 얻은 관련 ProductDetails 객체가 포함된 BillingFlowParams 객체를 참조합니다. BillingFlowParams 객체를 생성하려면 BillingFlowParams.Builder 클래스를 사용하세요.

Kotlin

// An activity reference from which the billing flow will be launched.
val activity : Activity = ...;

val productDetailsParamsList = listOf(
    BillingFlowParams.ProductDetailsParams.newBuilder()
        // retrieve a value for "productDetails" by calling queryProductDetailsAsync()
        .setProductDetails(productDetails)
        // For One-time product, "setOfferToken" method shouldn't be called.
        // For subscriptions, to get an offer token, call ProductDetails.subscriptionOfferDetails()
        // for a list of offers that are available to the user
        .setOfferToken(selectedOfferToken)
        .build()
)

val billingFlowParams = BillingFlowParams.newBuilder()
    .setProductDetailsParamsList(productDetailsParamsList)
    .build()

// Launch the billing flow
val billingResult = billingClient.launchBillingFlow(activity, billingFlowParams)

Java

// An activity reference from which the billing flow will be launched.
Activity activity = ...;

ImmutableList<ProductDetailsParams> productDetailsParamsList =
    ImmutableList.of(
        ProductDetailsParams.newBuilder()
             // retrieve a value for "productDetails" by calling queryProductDetailsAsync()
            .setProductDetails(productDetails)
            // For one-time products, "setOfferToken" method shouldn't be called.
            // For subscriptions, to get an offer token, call
            // ProductDetails.subscriptionOfferDetails() for a list of offers
            // that are available to the user.
            .setOfferToken(selectedOfferToken)
            .build()
    );

BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
    .setProductDetailsParamsList(productDetailsParamsList)
    .build();

// Launch the billing flow
BillingResult billingResult = billingClient.launchBillingFlow(activity, billingFlowParams);

launchBillingFlow() 메서드는 BillingClient.BillingResponseCode에 나열된 몇 가지 응답 코드 중 하나를 반환합니다. 이 결과를 검토하여 구매 흐름을 시작하는 동안 발생한 오류가 없는지 확인합니다. OKBillingResponseCode는 성공적으로 시작되었음을 나타냅니다.

launchBillingFlow() 호출에 성공하면 시스템은 Google Play 구매 화면을 표시합니다. 그림 1은 정기 결제 구매 화면을 보여줍니다.

Google Play 구매 화면에 구매할 수 있는 정기 결제가 표시됨
그림 1. Google Play 구매 화면에 구매할 수 있는 정기 결제가 표시됨

Google Play는 onPurchasesUpdated()를 호출하여 PurchasesUpdatedListener 인터페이스를 구현하는 리스너에 구매 작업 결과를 전송합니다. 리스너는 클라이언트를 초기화할 때 setListener() 메서드를 사용하여 지정됩니다.

가능한 응답 코드를 처리하려면 onPurchasesUpdated()를 구현해야 합니다. 다음 예는 onPurchasesUpdated()를 재정의하는 방법을 보여줍니다.

Kotlin

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

Java

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

구매 성공 시 그림 2와 유사한 Google Play 구매 성공 화면이 생성됩니다.

Google Play의 구매 성공 화면
그림 2. Google Play의 구매 성공 화면

또한 구매 성공 시 사용자가 구매한 인앱 상품의 사용자 및 제품 ID를 나타내는 고유 식별자인 구매 토큰도 생성됩니다. 앱은 구매 토큰을 로컬에 저장할 수 있습니다. 하지만, 구매를 인증하고 사기로부터 보호할 수 있는 보안 백엔드 서버로 토큰을 전달하는 것이 좋습니다. 이 프로세스는 다음 섹션에서 자세히 설명합니다.

또한 사용자는 주문 ID 또는 거래의 고유 ID가 포함된 거래 영수증을 이메일로 받습니다. 사용자는 일회성 제품을 구매할 때마다 그리고 최초 정기 결제 구매 및 후속 구매가 자동 갱신될 때마다 고유 주문 ID가 포함된 이메일을 받습니다. Google Play Console에서 주문 ID를 사용하여 환불을 관리할 수 있습니다.

맞춤설정된 가격 표시

유럽 연합의 사용자에게 앱을 배포할 수 있는 경우 setIsOfferPersonalized() 메서드를 사용하여 상품 가격이 자동화된 의사결정을 통해 맞춤설정되었음을 사용자에게 알려야 합니다.

가격이 사용자에게 맞춤설정되었음을 나타내는 Google Play 구매 화면
그림 3. 가격이 사용자에게 맞춤설정되었음을 나타내는 Google Play 구매 화면

소비자 권리 지침의 제6조(1)(ea) CRD(2011/83/EU)에 따라 사용자에게 제공하는 가격이 맞춤설정되었는지 확인해야 합니다.

setIsOfferPersonalized()는 불리언 입력값을 사용합니다. true인 경우 Play UI에는 공개 정보가 포함됩니다. false인 경우 UI에서 공개 정보를 생략합니다. 기본값은 false입니다.

자세한 내용은 소비자 고객센터를 참고하세요.

구매 처리

사용자가 구매를 완료하면 앱에서 구매를 처리해야 합니다. 대부분의 경우 앱은 PurchasesUpdatedListener를 통해 구매 알림을 받습니다. 하지만 구매 가져오기에 설명된 대로 앱이 BillingClient.queryPurchasesAsync()를 호출하여 구매를 인식하는 경우도 있습니다.

또한 보안 백엔드에 실시간 개발자 알림 클라이언트가 있는 경우 개발자에게 새 구매를 알리는 subscriptionNotification 또는 oneTimeProductNotification(대기 중인 구매에만 해당)을 수신하여 새 구매를 등록할 수 있습니다. 이러한 알림을 받은 후 Google Play Developer API를 호출하여 전체 상태를 가져오고 자체 백엔드 상태를 업데이트합니다.

앱은 다음과 같은 방식으로 구매를 처리해야 합니다.

  1. 구매를 인증합니다.
  2. 사용자에게 콘텐츠를 제공하고 콘텐츠 전송을 확인합니다. 선택적으로, 사용자가 항목을 다시 구입할 수 있도록 항목을 소비됨으로 표시합니다.

구매를 인증하려면 먼저 구매 상태PURCHASED인지 확인합니다. 구매가 PENDING이라면 대기 중인 거래 처리에 설명된 대로 구매를 처리해야 합니다. onPurchasesUpdated() 또는 queryPurchasesAsync()에서 수신한 구매의 경우 앱이 자격을 부여하기 전에 구매를 추가로 인증하여 정당성을 확인해야 합니다. 구매를 적절하게 인증하는 방법을 알아보려면 자격을 부여하기 전에 구매 확인을 참고하세요.

구매를 인증했다면 앱에서 사용자에게 자격을 부여할 준비가 된 것입니다. 구매와 연결된 사용자 계정은 인앱 상품 구매의 경우 Purchases.products:get에서 반환된 ProductPurchase.obfuscatedExternalAccountId로, 서버 측에서의 정기 결제의 경우 Purchases.subscriptions:get에서 반환된 SubscriptionPurchase.obfuscatedExternalAccountId로, 또는 구매가 이뤄질 때 setObfuscatedAccountId로 설정된 경우 클라이언트 측에서 Purchase.getAccountIdentifiers()obfuscatedAccountId로 식별할 수 있습니다.

자격을 부여한 후 앱에서 구매를 확인해야 합니다. 이 확인은 구매와 관련된 자격을 부여했음을 Google Play에 알려줍니다.

자격을 부여하고 구매를 확인하는 프로세스는 구매가 소비성인지, 비소비성인지 아니면 정기 결제인지에 따라 다릅니다.

소비성 제품

소비성 제품의 경우 앱에 보안 백엔드가 있으면 Purchases.products:consume을 사용하여 안정적으로 구매를 소비하는 것이 좋습니다. consumptionState 호출 결과에서 Purchases.products:get을 확인하여 구매가 아직 소비되지 않았는지 확인합니다. 앱이 백엔드가 없는 클라이언트 전용인 경우 Google Play 결제 라이브러리의 consumeAsync()를 사용합니다. 두 방법 모두 확인 요구사항을 충족하며 앱에서 사용자에게 자격을 부여했음을 나타냅니다. 또한 이러한 방법을 사용하면 앱에서 입력 구매 토큰에 해당하는 일회성 제품을 재구매할 수 있습니다. consumeAsync()를 사용하면 ConsumeResponseListener 인터페이스를 구현하는 객체도 전달해야 합니다. 이 객체는 소비 작업의 결과를 처리합니다. 작업 완료 시 Google Play 결제 라이브러리가 호출하는 onConsumeResponse() 메서드를 재정의할 수 있습니다.

다음 예는 관련 구매 토큰을 사용하여 Google Play 결제 라이브러리로 제품을 소비하는 방법을 보여줍니다.

Kotlin

suspend fun handlePurchase(purchase: Purchase) {
    // Purchase retrieved from BillingClient#queryPurchasesAsync or your PurchasesUpdatedListener.
    val purchase : Purchase = ...;

    // Verify the purchase.
    // Ensure entitlement was not already granted for this purchaseToken.
    // Grant entitlement to the user.

    val consumeParams =
        ConsumeParams.newBuilder()
            .setPurchaseToken(purchase.getPurchaseToken())
            .build()
    val consumeResult = withContext(Dispatchers.IO) {
        client.consumePurchase(consumeParams)
    }
}

Java

void handlePurchase(Purchase purchase) {
    // Purchase retrieved from BillingClient#queryPurchasesAsync or your PurchasesUpdatedListener.
    Purchase purchase = ...;

    // Verify the purchase.
    // Ensure entitlement was not already granted for this purchaseToken.
    // Grant entitlement to the user.

    ConsumeParams consumeParams =
        ConsumeParams.newBuilder()
            .setPurchaseToken(purchase.getPurchaseToken())
            .build();

    ConsumeResponseListener listener = new ConsumeResponseListener() {
        @Override
        public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
            if (billingResult.getResponseCode() == BillingResponseCode.OK) {
                // Handle the success of the consume operation.
            }
        }
    };

    billingClient.consumeAsync(consumeParams, listener);
}

비소비성 제품

비소비성 구매를 확인하려면 앱에 보안 백엔드가 있는 경우 Purchases.products:acknowledge를 사용하여 구매를 안정적으로 확인하는 것이 좋습니다. Purchases.products:get 호출 결과에서 acknowledgementState를 확인하여 이전에 구매를 확인하지 않았는지 확인합니다.

앱이 클라이언트 전용인 경우 앱에서 Google Play 결제 라이브러리의 BillingClient.acknowledgePurchase()를 사용합니다. 구매를 확인하기 전에 앱은 Google Play 결제 라이브러리의 isAcknowledged() 메서드를 사용하여 이미 확인되었는지 여부를 점검해야 합니다.

다음 예는 Google Play 결제 라이브러리를 사용하여 구매를 확인하는 방법을 보여줍니다.

Kotlin

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

suspend fun handlePurchase() {
    if (purchase.purchaseState === PurchaseState.PURCHASED) {
        if (!purchase.isAcknowledged) {
            val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                    .setPurchaseToken(purchase.purchaseToken)
            val ackPurchaseResult = withContext(Dispatchers.IO) {
               client.acknowledgePurchase(acknowledgePurchaseParams.build())
            }
        }
     }
}

Java

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

void handlePurchase(Purchase purchase) {
    if (purchase.getPurchaseState() == PurchaseState.PURCHASED) {
        if (!purchase.isAcknowledged()) {
            AcknowledgePurchaseParams acknowledgePurchaseParams =
                AcknowledgePurchaseParams.newBuilder()
                    .setPurchaseToken(purchase.getPurchaseToken())
                    .build();
            client.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
        }
    }
}

구독

정기 결제 구매는 비소비성 구매와 유사하게 처리됩니다. 가능하면 Google Play Developer API의 Purchases.subscriptions.acknowledge를 사용하여 보안 백엔드에서 구매를 안정적으로 확인하세요. Purchases.subscriptions:get의 구매 리소스에서 acknowledgementState를 확인하여 구매가 이전에 확인되지 않았는지 점검합니다. 그 외의 경우 isAcknowledged()를 확인한 후 Google Play 결제 라이브러리에서 BillingClient.acknowledgePurchase()를 사용하여 정기 결제를 확인할 수 있습니다. 최초 정기 결제 구매는 모두 확인해야 합니다. 정기 결제 갱신은 확인하지 않아도 됩니다. 정기 결제를 확인해야 하는 경우에 관한 자세한 내용은 정기 결제 판매 주제를 참고하세요.

구매 가져오기

PurchasesUpdatedListener를 사용하여 구매 업데이트를 수신 대기하는 것만으로는 앱이 모든 구매를 처리할 것이라고 보장할 수 없습니다. 앱에서 사용자가 구매한 모든 항목을 인식하지 못할 수 있습니다. 앱에서 구매 추적을 놓치거나 구매를 인식하지 못할 수 있는 몇 가지 시나리오는 다음과 같습니다.

  • 구매 중 네트워크 문제: 사용자가 구매를 성공적으로 완료하고 Google에서 확인을 받았지만 기기가 PurchasesUpdatedListener를 통해 구매 알림을 받기 전에 네트워크 연결이 끊어졌습니다.
  • 여러 기기: 사용자는 한 기기에서 항목을 구입한 후 기기를 전환할 때 이 항목이 표시되기를 기대합니다.
  • 앱 외부에서 이루어진 구매 처리: 프로모션 사용과 같은 일부 구매는 앱 외부에서 이루어질 수 있습니다.

이러한 상황을 처리하려면 앱이 onResume() 메서드에서 BillingClient.queryPurchasesAsync()를 호출하여 구매 처리에 설명된 대로 모든 구매가 성공적으로 처리되도록 해야 합니다.

다음 예는 사용자의 정기 결제 구매를 가져오는 방법을 보여줍니다. queryPurchasesAsync()는 활성 정기 결제 및 미사용 일회성 구매만 반환합니다.

Kotlin

val params = QueryPurchasesParams.newBuilder()
               .setProductType(ProductType.SUBS)

// uses queryPurchasesAsync Kotlin extension function
val purchasesResult = billingClient.queryPurchasesAsync(params.build())

// check purchasesResult.billingResult
// process returned purchasesResult.purchasesList, e.g. display the plans user owns

Java

billingClient.queryPurchasesAsync(
    QueryPurchasesParams.newBuilder()
      .setProductType(ProductType.SUBS)
      .build(),
    new PurchasesResponseListener() {
      public void onQueryPurchasesResponse(BillingResult billingResult, List purchases) {
        // check billingResult
        // process returned purchase list, e.g. display the plans user owns

      }
    }
);

구매 내역 가져오기

queryPurchaseHistoryAsync()는 구매가 만료되었거나 취소되었거나 소비된 경우에도 각 제품에 대한 사용자의 가장 최근 구매를 반환합니다.

Kotlin 확장 프로그램을 사용하는 경우 queryPurchaseHistory() 확장 함수를 사용할 수 있습니다.

Kotlin

val params = QueryPurchaseHistoryParams.newBuilder()
               .setProductType(ProductType.SUBS)

// uses queryPurchaseHistory Kotlin extension function
val purchaseHistoryResult = billingClient.queryPurchaseHistory(params.build())

// check purchaseHistoryResult.billingResult
// process returned purchaseHistoryResult.purchaseHistoryRecordList, e.g. display purchase

Java

billingClient.queryPurchaseHistoryAsync(
    QueryPurchaseHistoryParams.newBuilder()
        .setProductType(ProductType.SUBS)
        .build(),
    new PurchaseHistoryResponseListener() {
      public void onPurchaseHistoryResponse(
        BillingResult billingResult, List purchasesHistoryList) {
          // check billingResult
          // process returned purchase history list, e.g. display purchase history
        }
    }
);

앱 외부에서 이루어진 구매 처리

프로모션 사용과 같은 일부 구매는 앱 외부에서 발생하기도 합니다. 앱 외부에서 구매하는 사용자는 구매가 올바르게 처리되었음을 확인할 수 있도록 앱에서 인앱 메시지를 표시하거나 일종의 알림 메커니즘이 사용되기를 기대합니다. 허용되는 일부 메커니즘은 다음과 같습니다.

  • 인앱 팝업을 표시합니다.
  • 인앱 메시지 상자에 메시지를 전송하고 인앱 메시지 상자에 새 메시지가 있음을 명확히 알립니다.
  • OS 알림 메시지를 사용합니다.

앱에서 구매를 인식할 때 앱의 상태는 다양할 수 있다는 점에 유의하시기 바랍니다. 구매가 이루어졌을 때 앱이 설치되어 있지 않았을 수도 있습니다. 사용자는 앱이 어떤 상태에 있든지 관계없이 앱을 다시 시작할 때 구매를 수신할 것으로 기대합니다.

구매가 이루어졌을 때 앱이 어떤 상태에 있든지 관계없이 구매를 감지해야 합니다. 하지만 구매 항목이 수신되었음을 사용자에게 즉시 알리지 않아도 되는 몇 가지 예외 상황이 있습니다. 예:

  • 게임의 액션 플레이 중에 메시지를 표시하면 사용자의 주의가 산만해질 수 있습니다. 이 경우 액션 플레이가 끝난 후에 사용자에게 알려야 합니다.
  • 컷신 중에 메시지를 표시하면 사용자의 주의가 산만해질 수 있습니다. 이 경우 컷신이 끝난 후에 사용자에게 알려야 합니다.
  • 게임의 초기 튜토리얼 및 사용자 설정 중에도 사용자에게 즉시 알리지 않아도 됩니다. 신규 사용자가 게임을 연 직후 또는 처음 사용자 설정 중에 리워드를 알리는 것이 좋습니다. 그러나 사용자가 메인 게임 시퀀스를 사용할 수 있을 때까지 기다렸다가 알리는 것도 괜찮습니다.

앱 외부에서 이루어진 구매에 관해 사용자에게 알리는 시기 및 방법을 결정할 때는 항상 사용자를 염두에 두어야 합니다. 알림을 즉시 받지 못하면 사용자는 혼란스러워하거나 앱 사용을 중지하거나 사용자 지원팀에 문의하거나 소셜 미디어에 불만이 포함된 글을 게시할 수 있습니다. 참고: PurchasesUpdatedListener는 애플리케이션 컨텍스트에 등록되어 앱 외부에서 시작된 구매와 같은 구매 업데이트를 처리합니다. 즉, 애플리케이션 프로세스가 존재하지 않는 경우 PurchasesUpdatedListener는 알림을 받지 않습니다. 따라서, 앱은 구매 가져오기에서 언급한 대로 onResume() 메서드에서 BillingClient.queryPurchasesAsync()를 호출해야 합니다.

대기 중인 거래 처리

Google Play는 대기 중인 거래 또는 사용자가 구매를 시작한 시점과 구매 결제 수단이 처리되는 시점 사이에 하나 이상의 추가 단계가 필요한 거래를 지원합니다. Google에서 사용자의 결제 수단으로 요금이 청구되었다는 알림을 받을 때까지 앱에서 이러한 유형의 구매에 자격을 부여해서는 안 됩니다.

예를 들어 사용자는 결제 방법으로 현금을 선택하여 인앱 상품의 PENDING 구매를 생성할 수 있습니다. 그런 다음, 사용자는 거래를 완료할 오프라인 상점을 선택하고 알림과 이메일을 통해 코드를 수신할 수 있습니다. 사용자는 오프라인 상점에 도착하면 계산원에게 코드를 사용하여 현금으로 결제할 수 있습니다. 그러면 Google은 개발자와 사용자 모두에게 현금이 수령되었음을 알립니다. 다음으로, 앱에서 사용자에게 자격을 부여할 수 있습니다.

앱은 앱을 초기화하는 일환으로 enablePendingPurchases()를 호출하여 대기 중인 거래를 지원해야 합니다.

앱이 PurchasesUpdatedListener를 통해 또는 queryPurchasesAsync()를 호출한 결과로 새 구매를 수신한 경우 getPurchaseState() 메서드를 사용하여 구매 상태가 PURCHASED인지 또는 PENDING인지 확인합니다.

사용자가 구매를 완료할 때 앱이 실행 중이면 PurchasesUpdatedListener가 다시 호출되며 PurchaseState는 이제 PURCHASED가 됩니다. 이 시점에서 앱은 일회성 구매 처리를 위한 표준 메서드를 사용하여 구매를 처리할 수 있습니다. 또한 앱이 실행되지 않는 동안 PURCHASED 상태로 전환된 구매를 처리하려면 앱의 onResume() 메서드에서 queryPurchasesAsync()를 호출해야 합니다.

또한 앱에서 OneTimeProductNotifications를 수신 대기하여 대기 중인 구매에 실시간 개발자 알림을 사용할 수 있습니다. 구매가 PENDING에서 PURCHASED로 전환되면 앱에서 ONE_TIME_PRODUCT_PURCHASED 알림을 수신합니다. 구매가 취소되면 앱에서 ONE_TIME_PRODUCT_CANCELED 알림을 수신합니다. 이 이벤트는 고객이 필수 기간 내에 결제를 완료하지 않은 경우에 발생할 수 있습니다. 이러한 알림 수신 시 Purchases.productsPENDING 상태가 포함된 Google Play Developer API를 사용할 수 있습니다.

이 시나리오를 테스트하는 방법에 관한 자세한 단계는 구매 대기 중 테스트를 참고하세요.

다중 수량 구매 처리

Google Play 결제 라이브러리 버전 4.0 이상에서 지원되는 Google Play에서는 고객이 장바구니에서 수량을 지정하여 한 번의 거래로 같은 인앱 상품을 두 개 이상 구매할 수 있습니다. 앱은 다중 수량 구매를 처리하고 지정된 구매 수량에 따라 자격을 부여해야 합니다.

다중 수량 구매를 적용하려면 앱의 프로비저닝 로직이 항목 수량을 확인해야 합니다. 다음 API 중 하나에서 quantity 필드에 액세스할 수 있습니다.

다중 수량 구매를 처리하는 로직을 추가한 후 Google Play Console의 인앱 상품 관리 페이지에서 해당 제품에 다중 수량 기능을 사용 설정해야 합니다.

사용자의 결제 구성 쿼리

getBillingConfigAsync()는 사용자가 Google Play에 사용하는 국가를 제공합니다.

BillingClient를 만든 후에 사용자의 결제 구성을 쿼리할 수 있습니다. 다음 코드 스니펫은 getBillingConfigAsync()를 호출하는 방법을 설명합니다. BillingConfigResponseListener를 구현하여 응답을 처리합니다. 이 리스너는 앱에서 시작된 모든 결제 구성 쿼리의 업데이트를 수신합니다.

반환된 BillingResult에 오류가 없으면 BillingConfig 객체의 countryCode 필드를 확인하여 사용자의 Play 국가를 가져올 수 있습니다.

Kotlin

// Use the default GetBillingConfigParams.
val getBillingConfigParams = GetBillingConfigParams.newBuilder().build()
billingClient.getBillingConfigAsync(getBillingConfigParams,
    object : BillingConfigResponseListener {
        override fun onBillingConfigResponse(
            billingResult: BillingResult,
            billingConfig: BillingConfig?
        ) {
            if (billingResult.responseCode == BillingResponseCode.OK
                && billingConfig != null) {
                val countryCode = billingConfig.countryCode
                ...
            } else {
                // TODO: Handle errors
            }
        }
    })

Java

// Use the default GetBillingConfigParams.
GetBillingConfigParams getBillingConfigParams = GetBillingConfigParams.newBuilder().build();
billingClient.getBillingConfigAsync(getBillingConfigParams,
    new BillingConfigResponseListener() {
      public void onBillingConfigResponse(
          BillingResult billingResult, BillingConfig billingConfig) {
        if (billingResult.getResponseCode() == BillingResponseCode.OK
            && billingConfig != null) {
            String countryCode = billingConfig.getCountryCode();
            ...
         } else {
            // TODO: Handle errors
        }
      }
    });