Android

안드로이드 인앱 결제를 시작해보자! Google Play 결제 라이브러리6 사용 방법

구글플레이 결제 라이브러리6을 사용하여 인앱 결제 기능을 구현해보자. 구글 플레이 콘솔에 로그인 후 결제 프로필 작성을 먼저 해야하고 결제 계좌 및 세금 관련 정보도 등록을 먼저 해야한다. 구글 플레이 콘솔 -> 설정 -> 결제 프로필에서 결제 프로필 만들기를 클릭하여 일련의 과정에 따라 결제 프로필을 만들어주면 된다. 그리고 인앱 상품을 등록하기 위해서는 Googole Play 결제 라이브러리가 포함된 앱 버전을 게시해야 한다. 앱의 build.gradle 에 아래 종속 항목을 추가하여 앱을 빌드한다.

dependencies {
    // 결제
    implementation "com.android.billingclient:billing:6.0.1
}

그런 다음에 콘솔에서 출시 -> 테스트 -> 내부 테스트 -> 새 버전 만들기를 클릭하여 앱을 게시한다.

앱을 게시 후 또는 게시 이전에 “계정의 세부정보에 실제 주소”를 추가해야하고, 테스트 계정을 지정해야한다.

내부테스트 앱 게시 완료 후 다음 작업으로 상품을 만들어야한다.

 이제 등록된 앱의 왼쪽메뉴에서 수익 창출 -> 인앱 상품 -> “상품 만들기“를 클릭하여 상품을 만든다.
고유한 제품 ID, 이름, 설명 및 가격 정보를 설정 후 저장 버튼을 클릭하여 저장 후 “활성화” 버튼을 클릭한다.

인앱 상품이 준비되었다. 제품ID로 “cash_5000″를 지정하였다. 이것은 코드 구현시 구입 가능한 제품을 가져올 때 코드상에 하드코딩 되어 사용된다.

이제 앱에 구글 결제 라이브러리를 추가하여 구현을 시작해보자.

구글플레이 결제 관련 아키텍처는 아래 다이어그램을 보면 쉽게 이해할 수 있다.



구매 진행 과정 알아보기

일회성 구매 또는 정기 결제의 일반적인 구매 흐름은 아래와 같다.

  1. 사용자에게 구입할 수 있는 항목을 보여준다.
  2. 사용자가 구매를 수락할 수 있도록 구매 흐름을 시작한다.
  3. 서버에서 구매를 인증한다.
  4. 사용자에게 콘텐츠를 제공하고 콘텐츠 전송을 확인한다. 선택적으로 사용자가 항목을 다시 구입할 수 있도록 항목을 “구매”로 표시한다.

정기 결제는 취소 될 때까지 자동으로 갱신되며, 정기 결제는 다음과 같이 상태가 변하게 된다.

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



구매 토큰 및 주문 ID

Google Play에서는 구매 토큰과 주문 ID를 사용하여 제품 및 거래를 추적한다.

  • 구매 토큰은 Google Play에서 제품에 대한 구매자의 자격을 나태내는 문자열이다.
  • 주문 ID는  Google Play에서 금융 거래를 나타내는 문자열이다.

주문 ID는 금융 거래가 발생할 때마다 생성되며 구매 토큰은 사용자가 구매 흐름을 완료할 때만 생성된다.

  • 1회성 제품의 경우, 매 구매 시 항상 새 구매 토큰이 생성된다. 또한 사용자에게 비용이 청구되는 제품인 경우, 새로운 주문 ID도 같이 생성된다.
  • 정기 결제의 경우 최초 구매 시에는 구매 토큰 및 주문 ID가 생성된다. 이후 계속되는 각 결제 기간에 구매 토큰은 동일하게 유지되며 새로운 주문 ID가 발급된다.
  • 업그레이드, 다운그레이드, 대체 및 재가입은 모두 새로운 구매 토큰 및 주문 ID를 생성한다.

정기 결제의 경우 다음을 유의해야 한다.

  • 정기 결제 업그레이드, 다운그레이드 및 기타 정기 걸제 구매 흐름은 이전 구매 토큰을 대체해야 하는 구매 토큰을 생성한다.
  • 정기 결제 갱신 주문 번호에는 특정 갱신 인스턴스를 나타내는 정수가 추가로 포함된다.

* 참고사항: 사용자가 인앱상품을 구매할 때 돈을 지불할 필요가 없는 경우(예: 정기 결제의 무료 체험판 기간에 구매한 경우) $0의 주문 ID가 발급된다.  예를 들어 사용자가 정기 결제를 취소하면 결제 기간이 종료될 때까지 정기 결제가 유효한 상태로 유지된다. 사용자가 다시 가입하기로 하면 일부 잔액이 계정에 남아 있다. 이 경우 새 구매 토큰이 생성되고, $0의 주문 ID가 생성되며, 잔액이 소진된 후 정기 결제가 갱신된다.


오류 처리

Google Play 결제 라이브러리는 BillingResult 형식으로 오류를 반환된다. BillingResult에는 BillingResponseCode가 포함되어 있어 앱에서 발생할 수 있는 결제 관련 오류를 분석한다. 예를 들어 SERVICE_DISCONNECTED 오류 코드가 수신되면 앱에서 Google Play와의 연결을 다시 초기화해야한다. 또한 BillingResult에는 개발 중에 오류를 진단하는 데 유용한 디버그 메시지가 포함되어 있다.



인앱 결제 구현 방법

Google play 결제 시스템을 앱에 통합을 시작해보자

1. 구글 결제 라이브러리를 build.gradle(:app)파일에 추가한다. 가장 최신 버전인 6.0.1 버전을 사용해본다.

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    ......

    // 결제
    implementation "com.android.billingclient:billing:6.0.1
}

2. BillingClient 초기화

Google Play 결제 라이브러리를 추가한 후 BillingClient 인스턴스를 초기화해야 한다. BillingClient는 Google Play 결제 라이브러리와 나머지 앱 간의 통신을 위한 기본 인터페이스이다.

BillingClient를 생성하려면 newBuilder()를 사용해야 한다. 구매 관련 업데이트를 수신하려면 setListener()를 호출하여 PurchasesUpdateListener에 대한 참조를 전달해야 한다. 이 리스너는 앱의 모든 구매 관련 업데이트를 수신한다.

아래 코드를 결제를 구현하는 액티비티 클래스에 추가한다.

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(MainActivity.this)
    .setListener(purchasesUpdatedListener)
    .enablePendingPurchases()
    .build();

3. Google Play에 연결한다.

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

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

모든 메서드를 실행할 때는 BillingClient 연결을 유지해야 한다.

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

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.
    }
});

4. 구입 가능한 제품 표시

Google Play에 연결했다면 구매 가능한 제품을 사용자에게 표시할 준비가 끝났다. 인앱 상품 세부정보를 쿼리하려면 queryProductDetailsAsync()를 호출한다.

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

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

List<String> ids = Arrays.asList("
cash_5000"); // your product IDs
List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
for(String productId : ids) {
    productList.add(
            QueryProductDetailsParams.Product.newBuilder()
                    .setProductId(productId)
                    .setProductType(BillingClient.ProductType.INAPP)
                    .build()
    );
}

QueryProductDetailsParams queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
        .setProductList(productList)
        .build();

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

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

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

정기 결제를 제공하기 전에 사용자가 이미 정기 결제하지 않았는지 확인해야한다.

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


5. 구매 흐름 시작

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

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

ImmutableList productDetailsParamsList =
    ImmutableList.of(
        ProductDetailsParams.newBuilder()
             // retrieve a value for "productDetails" by calling queryProductDetailsAsync()
            .setProductDetails(productDetails)
            // to get an offer token, call ProductDetails.getSubscriptionOfferDetails()
            // 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에 나열된 몇 가지 응답 코드 중 하나를 반환한다. 이 결과를 검토하여 구매 흐름을 시작하는 동안 발생한 오류가 없는지 확인한다. OK의 BillingResponseCode는 성공적으로 시작되었음을 나타낸다.

launchBillingFlow() 호출에 성공하면 시스템은 Google Play 구매 화면을 표시한다.


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

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

private PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
    @Override
    public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
        // To be implemented in a later section.
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
                && purchases != null) {
            for (Purchase purchase : purchases) {
                handlePurchase(purchase);
            }
        } else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
            // Handle an error caused by a user cancelling the purchase flow.
        } else {
            // Handle any other error codes.//오류가 발생 할 경우 여기서 처리
        }
    }
};

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

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

6. 맞춤설정된 가격 표시

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

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

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

자세한 내용은 소비자 고객센터를 참고!!

ImmutableList productDetailsParamsList =
                ImmutableList.of(
                        BillingFlowParams.ProductDetailsParams.newBuilder()
                                // retrieve a value for "productDetails" by calling queryProductDetailsAsync()
                                .setProductDetails(productDetails)
                                // to get an offer token, call ProductDetails.getSubscriptionOfferDetails() .getOfferToken()
                                // for a list of offers that are available to the user
                                // 구독일 때 구독인 경우 getOfferToken()다음과 같은 방법을 사용해 볼 수 있을 것 같다.
                                // https://developer.android.com/reference/com/android/billingclient/api/ProductDetails.SubscriptionOfferDetails#getOfferToken()

                                //.setOfferToken(selectedOfferToken)  //구독용
                                .build()
                );

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

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

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

7. 구매처리

3일 이내에 구매를 확인하지 않으면 사용자에게 자동으로 환불되고 Google Play에서 구매를 취소한다.

사용자가 구매를 완료하면 앱에서 구매를 처리해야 한다. 대부분의 경우 앱은 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에 알려준다.

7-1. 소비성 제품일때 처리

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

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

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);
}
주의사항 : 소비 요청이 때로 실패할 수 있으므로 보안 백엔드 서버를 확인하여 각 구매 토큰이 사용되지 않았는지 확인해야 합니다. 그래야 앱이 동일한 구매에 대해 여러 번 자격을 부여하지 않습니다. 또는 자격을 부여하기 전에 앱이 Google Play에서 성공적인 소비 응답을 받을 때까지 기다릴 수 있습니다. Google Play에서 성공적인 소비 응답을 보낼 때까지 사용자의 구매를 보류하기로 선택하는 경우 소비 요청을 보낸 구매에 관한 추적을 놓치지 않도록 매우 주의해야 합니다.


7-2. 비소비성 제품일때 처리

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

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

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

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

7-3. 정기 결제

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

판매하는 아이템이 ‘광고 제거’와 같이 한번 구매하면 영구적으로 갖고 있을 경우 별다른 처리를 하지 않아도 된다. 그러나재 구매가 가능한 별도 재화나 아이템을 판매했다면 별도 소모 처리를 진행해야 한다.  그리고 소모 처리가 완료 됐을때 재화나 아이템을 제공해야 한다. onConsumeResponse에서 response코드를 체크 후 재화를 제공하면 됩니다.


8. 구매 가져오기

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

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

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

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

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

      }
    }
);

9. 구매 내역 가져오기

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

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

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

10. 결제모듈 종료 처리

Activity나 Fragment가 Destroyed 될때 ‘EndConnection’을 호출해야 메모리 누수를 피할 수 있다.

 @Override
    protected void onDestroy() {
        if (billingClient != null) {
            billingClient.endConnection();
        }

        super.onDestroy();
    }

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

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

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

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

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

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

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

대기 중인 거래 처리

참고: 대기 중인 거래는 Google Play 결제 라이브러리 버전 2.0 이상에서 필요합니다. 대기 중인 거래를 명시적으로 처리해야 합니다.

참고: 정기 결제 구매에는 추가 결제 수단을 사용할 수 없습니다.

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

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

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

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

참고: 상태가 오직PURCHASED인 경우에만 자격을 부여해야 합니다. getOriginaljson() 대신 getPurchaseState()를 사용하고 PENDING 트랜잭션을 제대로 처리하도록 합니다.

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

참고: 상태가 PURCHASED인 경우에만 구매를 확인해야 합니다. 구매가 PENDING 상태일 때는 확인하면 안 됩니다. 3일의 확인 기간은 구매 상태가 ‘PENDING’에서 ‘PURCHASED’로 전환되는 경우에만 시작됩니다.

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

참고: 대기 중인 거래는 라이선스 테스터를 사용해 테스트할 수 있습니다. 라이선스 테스터는 2개의 테스트 신용카드 외에도 결제가 몇 분 후에 자동으로 완료되거나 취소되는 지연된 결제 수단을 위한 2개의 테스트 계측에 액세스할 수 있습니다. 애플리케이션을 테스트하는 동안 이러한 두 도구 중 하나를 사용하여 애플리케이션이 구매 직후에 자격을 부여하거나 구매를 확인하지 않는지 검증해야 합니다. 자동으로 완료되는 테스트 도구를 사용해 구매할 때는 애플리케이션이 구매 완료 후 자격을 부여하고 구매를 확인하는지 검증해야 합니다.

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

다중 수량 구매 처리

Google Play 결제 라이브러리 버전 4.0 이상에서 지원되는 Google Play에서는 고객이 장바구니에서 수량을 지정하여 한 번의 거래로 같은 인앱 상품을 두 개 이상 구매할 수 있습니다. 앱은 다중 수량 구매를 처리하고 지정된 구매 수량에 따라 자격을 부여해야 합니다.참고: 다중 수량은 구매하고 소비한 후 다시 구매할 수 있는 상품인 소비성 인앱 상품을 위한 것입니다. 반복적으로 구매할 수 없는 상품에는 이 기능을 사용 설정하지 마세요.

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

다중 수량 구매를 처리하는 로직을 추가한 후 Google Play Console의 인앱 상품 관리 페이지에서 해당 제품에 다중 수량 기능을 사용 설정해야 합니다.참고: Console에서 이 기능을 사용 설정하기 전에 앱에서 다중 수량 구매를 적용하는지 확인합니다. 제품에서 이 기능을 사용 설정하려면 먼저 지원을 제공하는 앱 버전으로 강제 업데이트해야 할 수도 있습니다.

마지막으로 결제 테스트를 해보자!!

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

앱 선택 후 왼쪽메뉴에서 테스트 -> 내부 테스트 -> 테스터 탭 클릭-> 이메일 목록 만들기를 클릭하여 테스터의 Google 이메일 계정을 추가한다.

구매(결제) 검증

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

모든 구매에서 orderId가 생성되는 것은 아니므로 데이터베이스에서 중복 구매를 확인하거나 기본 키로 orderId를 사용하지 마세요. 특히, 프로모션 코드로 구매하면 orderId가 생성되지 않습니다.

서버 사이드에서 구매를 검증하는 이유

서버측 검증은 필수는 아니다. 인앱구매는 검증 없이도 이루어진다.하지만 다음과 같은 몇 가지 중요한 이점이 있다.

  1. 고급 결제 분석은 구독에 특히 중요한데, 활성화 후에 발생하는 모든 일을 장치에서 처리하지는 않기 때문이다. 서버 측 구매 처리가 없으면, 현재 구독 상태를 검색할 수 없고, 사용자가 구독을 갱신 또는 취소했는지, 지불 문제가 있는지 등의 여부를 알 수 없다.
  2. 구매의 진위 여부를 확인할 수 있음. 거래가 사기가 아니고 사용자가 실제로 제품에 대한 비용을 지불했음을 확인할 수 있다.
  3. 플랫폼 간 구독. 사용자의 구독 상태를 실시간으로 확인할 수 있는 경우, 다른 플랫폼과 동기화할 수 있다. 예를 들어 iOS 기기에서 구독을 구매한 사용자는 Android, 웹 및 기타 플랫폼에서 구독을 사용할 수 있다.
  4. 서버 측에서 콘텐츠 접근을 제어할 수 있게 되어, 단순히 서버에 대한 요청을 실행하는 것만으로 구독 없이 데이터에 접근하려는 사용자로부터 앱을 보호한다.

환불처리

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

Voided Purchases API

Google Play Voided Purchases API는 사용자가 무효화한 구매와 관련된 주문 목록을 제공한다. 이 목록의 정보를 사용하여 사용자가 무효화한 주문의 제품에 액세스하지 못하도록 방지하는 취소 시스템을 구현할 수 있다.

Voided Purchases API는 일회성 인앱 주문과 앱 정기 결제에 적용된다.

다음 방법으로 구매를 무효화할 수 있다.

  • 사용자가 주문의 환불을 요청한다.
  • 사용자가 주문을 취소한다.
  • 주문의 지불을 거절한다.
  • 개발자가 주문을 취소하거나 환불한다. 취소된 주문만 Voided Purchases API에 표시된다. 개발자가 취소 옵션을 설정하지 않고 구매를 환불하면 API에서 주문을 반환하지 않는다.
  • Google에서 주문을 취소하거나 환불합니다.

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

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

[참고]

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

Google Play 결제 라이브러리 출시 노트

[AOS] 인앱 결제 라이브러리 4 적용 및 AIDL 마이그레이션 ( + 광고 제거 기능 구현 )

Android 인앱 구매, 5부: 서버 측 구매 검증

[코틀린으로 구현된 인앱 결제 예시 참고]

https://github.com/zinc0214/InAppBillingExample/tree/master

Leave a Reply

error: Content is protected !!