안드로이드 구글 인앱 결제 V4

IM·2022년 5월 11일
1
post-thumbnail
post-custom-banner

구글의 인앱결제 정책에 따라 현재 서비스 하고 있는 어플에 인앱 결제 기능을 추가해야한다.
결제 정책은 아래 링크에서 확인 할 수 있다.


하.. 발등에 또ㅇ.. 아니 불이 떨어졌다. 😇


얼른 인앱 결제를 구현해보자
(아래 내용은 Google Play 결제 시스템 문서를 참고함)

준비하기

결제 프로필 만들기

  • 구글 플레이콘솔 -> 설정 -> 결제 프로필에서 결제 프로필 만들기를 클릭하여 일련의 과정에 따라 결제 프로필을 만들어주면 된다.

Google Play Console에서 결제 관련 기능 사용 설정

  • 인앱 상품을 등록하기 위해서는 Googole Play 결제 라이브러리가 포함된 앱 버전을 게시해야 한다.
    앱의 build.gradle 에 아래 종속 항목을 추가하여 앱을 빌드한다.
dependencies {
	implementation 'com.android.billingclient:billing:4.1.0'
}
  • 콘솔에 등록된 앱의 출시 -> 테스트 -> 내부 테스트 -> 새 버전 만들기를 클릭하여 앱을 게시한다.

제품 생성 및 구성 (소모성 or 비소모성 상품 = 일회성 청구 상품)

  • 앱 게시를 완료하였다면 이제 등록된 앱의 수익 창출 -> 인앱 상품 -> 상품 만들기를 클릭하여 상품을 만들 수 있다.
    고유한 제품 ID, 이름, 설명 및 가격 정보를 제공한다.

구현하기

구매 진행 과정

  • 일회성 구매의 일반적인 구매 흐름은 아래와 같다.

BillingClient 초기화

  • 구매 관련 업데이트를 수신하기 위하여 setListener()에 PurchasesUpdatedListener를 추가한다.
    해당 리스너는 앱의 모든 구매 관련 업데이트를 수신한다.
val billingClient = BillingClient.newBuilder(this)
    .setListener(PurchasesUpdatedListener { billingResult, purchases ->
        //모든 구매 관련 업데이트를 수신한다.
    })
    .enablePendingPurchases()
    .build()

Google Play 연결 설정

  • Google Play에 연결하기 위해 startConnection() 을 호출한다. 비동기적이므로 BillingClientStateListener 를 구현하여 콜백을 수신해야 한다.
billingClient.startConnection(object : BillingClientStateListener {
    override fun onBillingServiceDisconnected() {
    	// 연결 실패 시 재시도 로직을 구현.
    }
    override fun onBillingSetupFinished(billingResult: BillingResult) {
        if (billingResult.responseCode ==  BillingClient.BillingResponseCode.OK) {
        	// 준비 완료가 되면 상품 쿼리를 처리 할 수 있다!
        }
    }
})

상품 정보 가져오기

  • 인앱 상품 정보를 쿼리하려면 querySkuDetailsAsync() 를 호출한다.
    해당 메소드는 SkuType (정기 : SkuType.SUBS / 일회성 : SkuType.INAPP)과 상품 ID 문자열 목록 포함한 SkuDetailsParams 를 매개변수로 사용한다.
private fun querySkuDetails() {
    val skuList = ArrayList<String>()
    
    skuList.add("item_id_1")
    skuList.add("item_id_2")
    skuList.add("item_id_3")
    
    val params = SkuDetailsParams.newBuilder().apply {
        setSkusList(skuList)
        setType(BillingClient.SkuType.INAPP)		//정기 구독일 경우 BillingClient.SkuType.SUBS
    }.build()
    
    billingClient.querySkuDetailsAsync(params) { billingResult, skuDetailsList ->
        // 완료되면 SkuDetails(상품 상세 정보)를 List 형태로 반환한다.
    }
}

구매 흐름 시작

  • 구매 요청을 시작하려면 launchBillingFlow() 를 호출한다.
    해당 메소드는 querySkuDetailsAsync() 호출로 반환받은 SkuDetails가 포함된 BillingFlowParams 를 매개변수로 사용한다.
    그리고 launchBillingFlow()는 BillingResponseCode를 반환하며 이 결과를 검토하여 오류를 처리한다.
val flowParams = BillingFlowParams.newBuilder()
    .setSkuDetails(skuDetails)
    .build()
    
val billingResult = billingClient.launchBillingFlow(
    this,
    flowParams
)

//launchBillingFlow()는 BillingResponseCode를 반환한다. 
if( billingResult.responseCode != BillingClient.BillingResponseCode.OK ) {
	//오류가 발생 할 경우 여기서 처리
}
  • 호출에 성공하면 다음과 같은 구매 화면이 표시된다. (테스트 결제는 다음 챕터에서 설명)

  • 구매 결과는 BillingClient 초기화 시 추가했던 PurchasesUpdatedListener 리스너에 전송된다.

val billingClient = BillingClient.newBuilder(this)
    .setListener(PurchasesUpdatedListener { billingResult, purchases ->
        //모든 구매 관련 업데이트를 수신한다.
        purchasesUpdated(billingResult, purchases)
    })
    .enablePendingPurchases()
    .build()
    
private fun purchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
    if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
        for (purchase in purchases) {
            //구매 성공 시 처리
        }
    } else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
        // 사용자가 구매를 취소했을 경우 처리
    } else {
        // 이외의 오류 처리
    }
}

상품 소비 처리

  • 구매(결제)에 성공하면 서비스 중인 서버에 구매(결제) 검증을 요청하고(현재 내용에서는 생략) 검증 성공 시 소비 처리를 해준다.
    소비성 상품이 경우는 소비 처리를 해주어야 상품을 다시 구매 할 수 있는데, consumeAsync() 를 호출하여 소비 처리를 해준다.
    해당 메소드는 구매 토큰(purchaseToken)이 포함된 ConsumeParams를 매개변수로 사용한다.
    (만약 3일 이내(테스트일 경우 3분)에 소비 처리를 하지 않을 경우, 자동 환불 및 구매 취소가 된다.)
private fun handlePurchase(purchase: Purchase) {
    // 소비 처리 이전에 구매 검증 필요
    
    // 소비 처리
    val consumeParams =
        ConsumeParams.newBuilder()
            .setPurchaseToken(purchase.purchaseToken)
            .build()
            
    billingClient.consumeAsync(consumeParams) { billingResult, str ->
    	//소비 처리에 대한 결과 처리 
	}
}
  • 미처 소비 처리 되지 못한 구매 내역이 있을 수 있으므로 onResume() 에서 queryPurchasesAsync() 를 호출하여 내역이 있다면 구매 재검증 및 구매 처리를 해주어야 한다.
billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP) { billingResult, purchaseList ->
    if( billingResult.responseCode == BillingClient.BillingResponseCode.OK ) {
        purchaseList.forEach {
            handlePurchase(it)
        }
}

결제 테스트하기

라이선스 테스트 설정

  • 구글 플레이 콘솔 설정 -> 라이선스 테스트에 테스터의 Google 이메일 계정을 등록한다.

내부 테스트 설정

  • 내부 테스트로 앱을 게시한 경우에는 등록된 앱의 테스트 -> 내부 테스트 -> 테스터 -> 이메일 목록 만들기를 클릭하여 테스터의 Google 이메일 계정을 추가한다.

구매(결제) 검증

  • 구매(결제) 검증은 민감한 데이터 로직이므로 서버 사이드에서 처리해야한다.
    사용자가 구매 완료시 반환 받는 purchaseToken을 서버로 전송하여 구매(결제) 검증을 해야하는데,
    Google Developer API 의 Purchases.products.get(일회성 구매 상품 일 경우) 또는 Purchases.subscriptions:get(정기 구독 상품 일 경우) 를 사용하여 구매(결제) 검증을 진행한다.
    더 자세한 내용은 아래 링크를 참고하자.

환불처리

  • 아래에서 짧게 설명할 실시간 개발자 알림은 일회성 구매 상품에 대한 환불을 처리하기에는 부적절하므로 일회성 구매 상품의 환불은 Google Developer API 의 Voided Purchases 를 사용하여 처리해야한다. 해당 API를 사용해 무효화된 구매에 대해 확인하고, 무효화된 구매와 관련된 제품(또는 콘텐츠)에 엑세스 하지 못하도록 하는 시스템을 구현해야한다.
    더 자세한 내용은 아래 링크를 참고하자.

실시간 개발자 알림(RTDN) 이란?

  • 실시간 개발자 알림(RTDN) 은 앱 내에서 사용자의 사용 권한이 변경될 때마다 Google의 알림을 수신하는 메커니즘으로 Google Cloud의 Pub/Sub을 활용한다.
    다만 해당 알림으로는 정기 구독에 대한 알림과 일회성 구매 중에서도 지연된 결제건에 대한 알림만 받을 수 있어 이번 내용에서는 다루지 않았다. 더 자세한 내용은 아래 링크를 참고하자.

profile
Android Developer
post-custom-banner

0개의 댓글