Play 결제 라이브러리 호출이 작업을 트리거하면 라이브러리는 BillingResult
응답을 반환하여 개발자에게 결과를 알립니다. 예를 들어 queryProductDetailsAsync
를 사용하여 사용자에게 제공되는 혜택을 가져오는 경우 응답 코드는 OK 코드를 포함하고 올바른 ProductDetails
객체를 제공하거나, ProductDetails
객체를 제공할 수 없는 이유를 나타내는 다른 응답을 포함합니다.
모든 응답 코드가 오류인 것은 아닙니다. BillingResponseCode
참고 페이지에서는 이 가이드에서 설명하는 각 응답에 관한 자세한 설명을 제공합니다.
오류를 나타내지 않는 응답 코드의 예는 다음과 같습니다.
BillingClient.BillingResponseCode.OK
: 호출로 트리거된 작업이 성공적으로 완료되었습니다.BillingClient.BillingResponseCode.USER_CANCELED
: 사용자에게 Play 스토어 UI 흐름을 표시하는 작업의 경우. 이 응답은 사용자가 프로세스를 완료하지 않고 UI 흐름에서 벗어났음을 나타냅니다.
응답 코드가 실제로 오류를 나타내는 경우, 일시적인 조건으로 인해 오류가 발생했을 때는 복구가 가능하기도 합니다. Play 결제 라이브러리 메서드를 호출했을 때 복구 가능한 조건을 나타내는 BillingResponseCode
값이 반환된다면 호출을 재시도해야 합니다. 다른 경우에는 조건이 일시적인 것으로 간주되지 않으므로 재시도가 권장되지 않습니다.
일시적인 오류의 경우 사용자가 세션을 진행 중일 때 오류가 발생했는지(예: 사용자가 구매 흐름을 진행하는 중에 오류가 발생한 경우) 아니면 백그라운드에서 오류가 발생했는지(예: onResume
중에 사용자의 기존 구매를 쿼리한 경우)에 따라 서로 다른 재시도 전략이 요구됩니다.
아래의 재시도 전략 섹션에서는 이러한 다양한 전략을 안내하며, 재시도 가능한 BillingResult
응답 섹션에서는 각 응답 코드에 가장 효과적인 전략을 추천합니다.
일부 오류 응답에는 응답 코드 외에도 디버깅 및 로깅 목적의 메시지가 포함되어 있습니다.
재시도 전략
간단한 재시도
사용자가 세션을 진행 중인 상황에서는 오류가 사용자 경험을 최대한 방해하지 않도록 간단한 재시도 전략을 구현하는 것이 좋습니다. 이 경우에는 최대 재시도 횟수를 종료 조건으로 사용하는 간단한 재시도 전략을 사용할 것을 권장합니다.
다음 예는 BillingClient
연결을 설정할 때 오류를 처리하는 간단한 재시도 전략을 보여줍니다.
class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {
// Initialize the BillingClient.
private val billingClient = BillingClient.newBuilder(context)
.setListener(this)
.enablePendingPurchases()
.build()
// Establish a connection to Google Play.
fun startBillingConnection() {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
Log.d(TAG, "Billing response OK")
// The BillingClient is ready. You can now query Products Purchases.
} else {
Log.e(TAG, billingResult.debugMessage)
retryBillingServiceConnection()
}
}
override fun onBillingServiceDisconnected() {
Log.e(TAG, "GBPL Service disconnected")
retryBillingServiceConnection()
}
})
}
// Billing connection retry logic. This is a simple max retry pattern
private fun retryBillingServiceConnection() {
val maxTries = 3
var tries = 1
var isConnectionEstablished = false
do {
try {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
isConnectionEstablished = true
Log.d(TAG, "Billing connection retry succeeded.")
} else {
Log.e(
TAG,
"Billing connection retry failed: ${billingResult.debugMessage}"
)
}
}
})
} catch (e: Exception) {
e.message?.let { Log.e(TAG, it) }
tries++
}
} while (tries <= maxTries && !isConnectionEstablished)
}
...
}
지수 백오프 재시도
백그라운드에서 이루어지며 사용자가 세션을 진행하는 동안 사용자 경험에 영향을 주지 않는 Play 결제 라이브러리 작업에는 지수 백오프를 사용하는 것이 좋습니다.
예를 들어 새 구매를 확인하는 작업은 백그라운드에서 이루어질 수 있고 오류가 발생한 경우에도 확인이 실시간으로 이루어질 필요가 없으므로 지수 백오프를 구현하는 것이 적절합니다.
private fun acknowledge(purchaseToken: String): BillingResult {
val params = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchaseToken)
.build()
var ackResult = BillingResult()
billingClient.acknowledgePurchase(params) { billingResult ->
ackResult = billingResult
}
return ackResult
}
suspend fun acknowledgePurchase(purchaseToken: String) {
val retryDelayMs = 2000L
val retryFactor = 2
val maxTries = 3
withContext(Dispatchers.IO) {
acknowledge(purchaseToken)
}
AcknowledgePurchaseResponseListener { acknowledgePurchaseResult ->
val playBillingResponseCode =
PlayBillingResponseCode(acknowledgePurchaseResult.responseCode)
when (playBillingResponseCode) {
BillingClient.BillingResponseCode.OK -> {
Log.i(TAG, "Acknowledgement was successful")
}
BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> {
// This is possibly related to a stale Play cache.
// Querying purchases again.
Log.d(TAG, "Acknowledgement failed with ITEM_NOT_OWNED")
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build()
)
{ billingResult, purchaseList ->
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
purchaseList.forEach { purchase ->
acknowledge(purchase.purchaseToken)
}
}
}
}
}
in setOf(
BillingClient.BillingResponseCode.ERROR,
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED,
BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE,
) -> {
Log.d(
TAG,
"Acknowledgement failed, but can be retried --
Response Code: ${acknowledgePurchaseResult.responseCode} --
Debug Message: ${acknowledgePurchaseResult.debugMessage}"
)
runBlocking {
exponentialRetry(
maxTries = maxTries,
initialDelay = retryDelayMs,
retryFactor = retryFactor
) { acknowledge(purchaseToken) }
}
}
in setOf(
BillingClient.BillingResponseCode.BILLING_UNAVAILABLE,
BillingClient.BillingResponseCode.DEVELOPER_ERROR,
BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED,
) -> {
Log.e(
TAG,
"Acknowledgement failed and cannot be retried --
Response Code: ${acknowledgePurchaseResult.responseCode} --
Debug Message: ${acknowledgePurchaseResult.debugMessage}"
)
throw Exception("Failed to acknowledge the purchase!")
}
}
}
}
private suspend fun <T> exponentialRetry(
maxTries: Int = Int.MAX_VALUE,
initialDelay: Long = Long.MAX_VALUE,
retryFactor: Int = Int.MAX_VALUE,
block: suspend () -> T
): T? {
var currentDelay = initialDelay
var retryAttempt = 1
do {
runCatching {
delay(currentDelay)
block()
}
.onSuccess {
Log.d(TAG, "Retry succeeded")
return@onSuccess;
}
.onFailure { throwable ->
Log.e(
TAG,
"Retry Failed -- Cause: ${throwable.cause} -- Message: ${throwable.message}"
)
}
currentDelay *= retryFactor
retryAttempt++
} while (retryAttempt < maxTries)
return block() // last attempt
}
재시도 가능한 BillingResult 응답
NETWORK_ERROR(오류 코드 12)
문제
이 오류는 기기와 Play 시스템 간의 네트워크 연결에 문제가 있음을 나타냅니다.
가능한 해결 방법
오류를 해결하려면 이 오류를 트리거한 작업이 무엇인지에 따라 간단한 재시도와 지수 백오프 중 하나를 사용하세요.
SERVICE_TIMEOUT(오류 코드 -3)
문제
이 오류는 Google Play에서 응답하기 전에 요청이 최대 제한 시간에 도달했음을 나타냅니다. 이 상황은 예를 들어 Play 결제 라이브러리 호출에서 요청한 작업의 실행이 지연되어 발생할 수 있습니다.
가능한 해결 방법
이 오류는 보통 일시적인 문제입니다. 오류를 반환한 작업이 무엇인지에 따라 간단한 전략과 지수 백오프 전략 중 하나를 사용하여 요청을 재시도하세요.
아래의 SERVICE_DISCONNECTED
와 달리 Google Play 결제 서비스 연결이 끊어지지 않으며 개발자는 원래 시도한 Play 결제 라이브러리 작업만 재시도하면 됩니다.
SERVICE_DISCONNECTED(오류 코드 -1)
문제
이 치명적인 오류는 BillingClient
를 통한 클라이언트 앱과 Google Play 스토어 서비스의 연결이 끊어졌음을 나타냅니다.
가능한 해결 방법
이 오류를 최대한 방지하려면 Play 결제 라이브러리를 호출하기 전에 항상 BillingClient.isReady()
를 호출하여 Google Play 서비스 연결을 확인하세요.
SERVICE_DISCONNECTED
상태에서 복구를 시도하려면 클라이언트 앱이 BillingClient.startConnection
을 사용하여 연결을 다시 설정해야 합니다.
SERVICE_TIMEOUT
과 마찬가지로, 이 오류를 트리거한 작업이 무엇인지에 따라 간단한 재시도와 지수 백오프 중 하나를 사용하세요.
SERVICE_UNAVAILABLE(오류 코드 2)
중요사항:
Google Play 결제 라이브러리 6.0.0부터는 더 이상 네트워크 문제에 대해 SERVICE_UNAVAILABLE
이 반환되지 않습니다. 결제 서비스를 사용할 수 없는 경우와 지원 중단된 SERVICE_TIMEOUT
케이스 시나리오에서만 반환됩니다.
문제
이 일시적인 오류는 현재 Google Play 결제 서비스를 사용할 수 없음을 나타냅니다. 이 오류는 대부분의 경우 클라이언트 기기와 Google Play 결제 서비스 사이에 네트워크 연결 문제가 있음을 의미합니다.
가능한 해결 방법
이 오류는 보통 일시적인 문제입니다. 오류를 반환한 작업이 무엇인지에 따라 간단한 전략과 지수 백오프 전략 중 하나를 사용하여 요청을 재시도하세요.
SERVICE_DISCONNECTED
와 달리 Google Play 결제 서비스 연결이 끊어지지 않으며 개발자는 원래 시도한 작업만 재시도하면 됩니다.
BILLING_UNAVAILABLE(오류 코드 3)
문제
이 오류는 구매 프로세스 중에 사용자 결제 오류가 발생했음을 나타냅니다. 이 상황이 발생할 수 있는 경우는 다음과 같습니다.
- 사용자 기기의 Play 스토어 앱이 최신 버전이 아닙니다.
- 사용자가 지원되지 않는 국가에 있습니다.
- 사용자가 기업 사용자이며, 기업 관리자가 사용자의 구매 활동을 사용 중지했습니다.
- Google Play가 사용자의 결제 수단에 청구할 수 없습니다. 예를 들어 사용자의 신용카드가 만료되었을 수 있습니다.
가능한 해결 방법
이 경우에는 자동 재시도가 도움이 되지 않습니다. 단, 사용자가 문제의 원인이 된 조건을 해결한다면 수동 재시도가 도움이 될 수 있습니다. 예를 들어 사용자가 Play 스토어 버전을 지원되는 버전으로 업데이트한다면 처음에 시도한 작업을 수동으로 재시도하는 것이 해결 방법이 될 수 있습니다.
사용자가 세션을 진행하고 있지 않을 때 이 오류가 발생했다면 재시도가 적절하지 않을 수 있습니다.
구매 흐름의 결과로 BILLING_UNAVAILABLE
오류가 발생했다면 사용자가 구매 프로세스 중에 Google Play로부터 피드백을 받았으며 이에 따라 무엇이 문제인지 알고 있을 가능성이 큽니다. 이 경우 사용자가 문제를 해결한 후 수동 재시도 옵션을 사용할 수 있도록 무언가 잘못되었다고 안내하는 오류 메시지를 표시하고 '다시 시도' 버튼을 제공할 수 있습니다.
ERROR(오류 코드 6)
문제
Google Play 자체의 내부 문제를 나타내는 치명적인 오류입니다.
가능한 해결 방법
ERROR
로 이어지는 Google Play의 내부적인 문제는 일시적일 때도 있으며, 문제 완화를 위해 지수 백오프 방식의 재시도를 구현할 수 있습니다. 사용자가 세션을 진행 중이라면 간단한 재시도를 사용하는 것이 좋습니다.
ITEM_ALREADY_OWNED
문제
이 응답은 Google Play 사용자가 구매를 시도 중인 정기 결제 또는 일회성 구매 제품을 이미 소유하고 있음을 나타냅니다. 이 오류는 오래된 Google Play 캐시로 인해 발생한 경우를 제외하면 대부분의 경우 일시적인 오류가 아닙니다.
가능한 해결 방법
원인이 캐시 문제가 아닌 경우에 이 오류가 발생하지 않도록 하려면 사용자가 이미 소유하고 있는 제품을 표시하지 마세요. 구매 가능한 제품을 표시할 때는 사용자의 사용 권한을 확인한 후 이에 따라 사용자가 구매할 수 있는 항목을 필터링하세요.
캐시 문제로 인해 클라이언트 앱이 이 오류를 수신한 경우에는 Google Play 캐시가 Play 백엔드의 최신 데이터로 업데이트되도록 트리거됩니다.
이 경우에는 오류가 발생한 후에 재시도하면 일시적인 오류가 해결됩니다. ITEM_ALREADY_OWNED
오류가 표시된 후 BillingClient.queryPurchasesAsync()
를 호출하여 사용자가 제품을 획득했는지 확인하고, 사용자가 제품을 획득하지 않았다면 간단한 재시도 로직을 구현하여 구매를 재시도합니다.
ITEM_NOT_OWNED
문제
이 구매 응답은 Google Play 사용자가 교체, 확인 또는 소비하려는 정기 결제 또는 일회성 구매 제품을 사용자가 소유하지 않고 있음을 나타냅니다. 이 오류는 오래된 Google Play 캐시로 인해 발생한 경우를 제외하면 대부분의 경우 일시적인 오류가 아닙니다.
가능한 해결 방법
캐시 문제로 인해 이 오류를 수신한 경우에는 Google Play 캐시가 Play 백엔드의 최신 데이터로 업데이트되도록 트리거됩니다. 이 경우에는 오류가 발생한 후에 간단한 재시도 전략을 사용하면 일시적인 오류가 해결됩니다. ITEM_NOT_OWNED
오류가 표시된 후 BillingClient.queryPurchasesAsync()
를 호출하여 사용자가 제품을 획득했는지 확인하고, 사용자가 제품을 획득하지 않았다면 간단한 재시도 로직을 구현하여 구매를 재시도합니다.
재시도 불가능한 BillingResult 응답
다음과 같은 오류가 발생한 경우 재시도 로직을 사용하여 복구할 수 없습니다.
FEATURE_NOT_SUPPORTED
문제
재시도 불가능한 이 오류는 사용자의 기기에서 Play 스토어 버전이 오래되어 Google Play 결제 기능이 지원되지 않음을 나타냅니다.
예를 들어 일부 사용자의 기기에서 인앱 메시지를 지원하지 않을 수 있습니다.
가능한 완화 조치
Play 결제 라이브러리를 호출하기 전에 BillingClient.isFeatureSupported()
를 사용하여 기능 지원 여부를 확인하세요.
when {
billingClient.isReady -> {
if (billingClient.isFeatureSupported(BillingClient.FeatureType.IN_APP_MESSAGING)) {
// use feature
}
}
}
USER_CANCELED
문제
사용자가 결제 흐름 UI 외부를 클릭했습니다.
가능한 해결 방법
이는 정보 제공 목적으로만 제공되며 단계적 기능 저하를 구현할 수 있습니다.
ITEM_UNAVAILABLE
문제
이 사용자는 Google Play 결제 정기 결제 또는 일회성 구매 제품을 구매할 수 없습니다.
가능한 완화 조치
앱이 권장된 대로 queryProductDetailsAsync
를 통해 제품 세부정보를 새로고침하는지 확인합니다. 필요한 경우 Play Console 구성에서 제품 카탈로그가 변경되는 빈도를 고려하여 추가 새로고침을 구현합니다.
queryProductDetailsAsync
를 통해 올바른 정보를 반환하는 제품만 Google Play 결제에서 판매합니다.
제품 적합성 구성에 불일치가 있는지 확인합니다.
예를 들어 사용자가 구매하려는 지역이 아닌 다른 지역에서만 사용할 수 있는 제품을 쿼리하고 있을 수 있습니다.
사용자에게 판매하려면 상품이 활성 상태이고, 앱이 게시된 상태여야 하며, 사용자의 국가에서 앱을 사용할 수 있어야 합니다.
테스트 중에 제품 구성의 모든 항목이 올바르지만 사용자에게 이 오류가 표시되는 경우가 있을 수 있습니다. 이는 Google 서버에서 제품 세부정보가 전파되는 데 지연이 발생했기 때문일 수 있습니다. 나중에 다시 시도하세요.
DEVELOPER_ERROR
문제
API를 올바르게 사용하고 있지 않음을 나타내는 치명적인 오류입니다.
예를 들어 BillingClient.launchBillingFlow
에 잘못된 매개변수를 제공하면 이 오류가 발생할 수 있습니다.
가능한 해결 방법
여러 Play 결제 라이브러리 호출을 올바르게 사용하고 있는지 확인합니다. 또한 디버그 메시지에서 오류에 관한 자세한 내용을 확인하세요.