인앱결제 구현

인앱결제는 두 가지 방식으로 구현할 수 있습니다.

첫 번째는 원스토어에서 제공하는 인앱결제 서비스의 인터페이스를 직접 구현하는 방식이고, 두 번째는 인앱결제 서비스 인터페이스 구현체를 래핑(wrapping)한 인앱결제 SDK를 사용하는 방식입니다.

두가지 방법 중 하나를 선택하여 구현합니다. 각 개발사에서는 애플리케이션에 적당한 방법을 택하시면 되고, 좀 더 편리하게 구현하려면 서비스를 직접 구현하는 것 보다는 인앱결제 SDK를 이용해 구현하는 것을 추천합니다.

인앱결제 서비스 인터페이스 직접 구현하기

  • 다양한 형태의 개발 플랫폼(Unity, Unreal 3D 등)의 플러그인 형태를 개발할 때 환경에 맞는 최적화된 개발 방식을 따라야 하기 때문에 인앱결제 서비스 인터페이스를 직접 구현해야 하는 경우가 생길 수 있습니다. (Unity 및 Unreal 3D 원스토어 인앱결제 SDK는 제공예정) 원스토어 인앱결제 서비스의 인터페이스는 AIDL(Android Interface Definition Language) 타입의 ‘IInAppPurchseService.aidl’ 파일로 제공됩니다. 인앱결제 SDK 없이 AIDL 파일을 이용해 인터페이스를 직접 구현하는 방법은 다음과 같습니다.

    • AIDL 바인딩하기

    • 인앱결제를 사용하려면 개발사 앱과 원스토어(서비스) 앱이 통신할 수 있게 IInAppPurchseService.aidl를 이용하여 원스토어 인앱결제 service에 바인딩되어야 합니다.이를 위해 ServiceConnection 객체를 생성합니다.

      private IInAppPurchaseService mService; mServiceConn = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mService = IInAppPurchaseService.Stub.asInterface(service); } @Override public void onServiceDisconnected(ComponentName name) { mService = null; }};

  • 이후 앱에서는 애플리케이션의 life cycle에 맞춰 bindService 메서드를 호출하여 생성된 커넥션 객체를 바인딩하면, mService 객체를 이용해 인앱결제를 요청할 수 있습니다.

    Intent serviceIntent = new Intent();serviceIntent.setComponent(new ComponentName("com.skt.skaf.OA00018282", "com.onestore.extern.iap.InAppPurchaseService"));serviceIntent.setAction("com.onestore.extern.iap.InAppBillingService.ACTION"); if (Context.getPackageManager().resolveService(serviceIntent, 0) != null) { Log.d(LOG_TAG, "bindService "); mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);} else { Log.e(LOG_TAG, "no service ");}

인앱결제 SDK를 사용해 구현하기

  • '인앱결제 서비스 인터페이스 직접 구현하기' 방식으로 직접 인앱결제 코드를 작성하면 더 확장성 있게 개발할 수 있으나 구현해야 하는 코드량이 많아질 수 밖에 없습니다. 좀 더 편리하게 구현하려면 인앱결제 서비스를 래핑(wrapping)한 형태의 인앱결제 SDK를 활용합니다. 인앱결제 SDK는 인앱결제 서비스를 사용하는 부분을 감추고 좀 더 편리하고 직관적인 형태로 사용할 수 있도록 PurchaseClient 클래스를 제공합니다. 개발 프로젝트에 SDK를 추가하는 방법은 '인앱결제 라이브러리 추가'를 참고합니다.

    • SDK 에러 코드

      • SDK 에서는 AIDL을 이용 시 발생하는 서버 응답 코드(상세 내용은 인앱결제 레퍼런스 페이지 내 '서버 응답코드' 참고) 외에 SDK에서만 발생될 수 있는 에러 코드를 추가로 정의합니다. 개발사에서는 해당 에러코드 외에 서버 응답 코드를 꼭 확인하여야 하며, 각 에러 상황에 대한 처리를 진행해야 합니다.

        Response CodeValueDescriptionHow to handle

        IAP_ERROR_DATA_PARSING

        1001

        응답 데이터 파싱 오류

        구매 요청시 응답 전문이 잘못된 규격일때 발생합니다. 일시적인 서버오류 일 수 있으며, 계속적으로 발생 시 로그 정보 및 구매 정보를 원스토어 담당자에게 문의합니다.

        IAP_ERROR_SIGNATURE_VERIFICATION

        1002

        구매정보의 서명 검증 에러

        원스토어 개발자 센터에서 내려받은 서명 값으로 verify가 되지 않을 경우 위변조된 응답 전문일 수 있습니다. 만약 개발단계에서 해당 에러가 발생할 시 서명 값과 verify 로직을 개발사에서 확인 하도록 합니다.

        IAP_ERROR_ILLEGAL_ARGUMENT

        1003

        정상적이지 않는 파라미터 입력

        SDK 메서드 호출 시 정상적이지 않는 파라미터를 입력할 경우 발생합니다. 개발단계에서 수정하여야 합니다.

        IAP_ERROR_UNDEFINED_CODE

        1004

        정의되지 않는 에러

        서버 에러코드 와 SDK 에러코드에 해당하지 않는 에러일 경우 발생됩니다. 지속적으로 발생 시 로그 정보 및 구매 정보를 원스토어 담당자에게 문의합니다.

  • SDK 내 주요 클래스 정보

    • IInAppPurchaseService AIDL Interface 원스토어 인앱결제 서비스와 통신하기 위해 정의된 AIDL 인터페이스 파일입니다.

    • 지원여부 조회하기

      • 인앱결제 메서드들을 이용하기 전 각 개발사에서는 해당 메서드를 호출하여 인앱결제 가능상태인지 확인하여야 합니다. AIDL에서 제공하는 API를 이용하는 SDK 메서드들은 콜백 리스너 형태로 성공 및 실패에 대한 응답을 전달하고 있습니다. 실패에 대한 응답은 SDK를 이용하는 개발사에서 꼭 처리하는 3가지의 에러(onErrorRemoteException, onErrorSecurityException, onErrorNeedUpdateException)와 기본에러 응답(onError)을 제공합니다. onError 리스너로 전달되는 IapResult 는 응답코드와 응답코드에 대한 설명으로 구성된 Enum 으로 개발사에서는 해당 에러에 대하여 개발사 시나리오에 맞는 에러 처리를 진행해야합니다.

        /* * PurchaseClient의 isBillingSupportedAsync (지원여부조회) API 콜백 리스너 */PurchaseClient.BillingSupportedListener mBillingSupportedListener = new PurchaseClient.BillingSupportedListener() { @Override public void onSuccess() { Log.d(TAG, "isBillingSupportedAsync onSuccess"); } @Override public void onError(IapResult result) { Log.e(TAG, "isBillingSupportedAsync onError, " + result.toString()); } @Override public void onErrorRemoteException() { Log.e(TAG, "isBillingSupportedAsync onError, 원스토어 서비스와 연결을 할 수 없습니다"); } @Override public void onErrorSecurityException() { Log.e(TAG, "isBillingSupportedAsync onError, 비정상 앱에서 결제가 요청되었습니다"); } @Override public void onErrorNeedUpdateException() { Log.e(TAG, "isBillingSupportedAsync onError, 원스토어 서비스앱의 업데이트가 필요합니다"); }}; // 원스토어 인앱결제 API 버전int IAP_API_VERSION = 5;mPurchaseClient.isBillingSupportedAsync(IAP_API_VERSION, mBillingSupportedListener);

        • 정보를 가져오고자 하는 인앱상품 ID들을 ArrayList 형태로 queryProductAsync 메서드의 파라미터에 넣어서 호출하면 등록된 리스너로 결과를 전달됩니다.

    • 상품정보 조회하기

      • 상품ID는 개발사에서 개발자센터에 상품 등록시 지정한 Custom 상품 ID입니다. 상품정보는 onSuccess 리스너에 SDK의 ProductDetail 모델 형태로 응답을 내려줍니다.

        /* * PurchaseClient의 queryProductsAsync API (상품정보조회) 콜백 리스너 */PurchaseClient.QueryProductsListener mQueryProductsListener = new PurchaseClient.QueryProductsListener() { @Override public void onSuccess(List<ProductDetail> productDetails) { Log.d(TAG, "queryProductsAsync onSuccess, " + productDetails.toString()); } @Override public void onErrorRemoteException() { Log.e(TAG, "queryProductsAsync onError, 원스토어 서비스와 연결을 할 수 없습니다"); } @Override public void onErrorSecurityException() { Log.e(TAG, "queryProductsAsync onError, 비정상 앱에서 결제가 요청되었습니다"); } @Override public void onErrorNeedUpdateException() { Log.e(TAG, "queryProductsAsync onError, 원스토어 서비스앱의 업데이트가 필요합니다"); } @Override public void onError(IapResult result) { Log.e(TAG, "queryProductsAsync onError, " + result.toString()); }}; int IAP_API_VERSION = 5;String productType = IapEnum.ProductType.IN_APP.getType(); // "inapp"ArrayList<String> productCodes = new ArrayList<>();productCodes.add("p5000");productCodes.add("p10000"); mPurchaseClient.queryProductsAsync(IAP_API_VERSION, productCodes, productType, mQueryProductsListener);

    • 구매 요청하기

      • 구매를 수행하기 위해서 launchPurchaseFlowAsync 메서드를 호출합니다. 메서드 호출 시 구매하려는 인앱상품ID, 상품명, 상품타입과 개발사가 임의로 입력한 developerPayload(최대 100byte)를 넣는데, 이 값은 결제 후에 데이터의 정합성과 부가 데이터를 확인하기 위해서 사용합니다. 그리고 파라미터로 전달하는 requestCode값은 후에 onActivityResult로 전달되는 데이터를 확인하기 위한 용도로 활용됩니다. 구매 성공시 onSuccess 리스너로 응답을 받게되며, SDK의 PurchaseData 규격으로 전달됩니다. 개발사에서는 전달받은 응답을 토대로 developerPayload 를 통하여 데이터 정합성 및 부가 데이터 확인을 진행하며, Signature 정보를 가지고 서명 검증을 진행합니다. 최종적으로 관리형 상품의 경우 상품소비를 함으로써 사용자에게 상품을 지급하도록 합니다. 원스토어는 사용자에게 할인 쿠폰, 캐시백 등의 다양한 혜택 프로모션을 진행하고 있습니다. 개발사는 구매 요청 시에 gameUserId, promotionApplicable 파라미터를 이용하여 어플리케이션을 사용하는 유저의 프로모션 참여를 제한하거나 허용 할 수 있습니다. 개발사는 해당 앱의 고유한 유저 식별 번호 및 프로모션 참여 여부를 선택하여 전달하고, 원스토어는 해당 값을 토대로 사용자의 프로모션 혜택을 적용하게 됩니다.

        gameUserId, promotionApplicable 파라미터는 Optional 값으로 원스토어 사업부서 담당자와 프로모션에 대해 사전협의가 된 상태에서만 사용하여야 하며, 일반적인 경우에는 값을 보내지 않아도 됩니다. 또한, 사전협의가 되어 값을 보낼 경우에도 개인정보보호 이슈가 없도록 gameUserId는 hash된 unique한 값으로 전송하여야 합니다.

        /* * PurchaseClient의 launchPurchaseFlowAsync API (구매) 콜백 리스너 */PurchaseClient.PurchaseFlowListener mPurchaseFlowListener = new PurchaseClient.PurchaseFlowListener() { @Override public void onSuccess(PurchaseData purchaseData) { Log.d(TAG, "launchPurchaseFlowAsync onSuccess, " + purchaseData.toString()); // 구매완료 후 developer payload 검증을 수해한다. if (!isValidPayload(purchaseData.getDeveloperPayload())) { Log.d(TAG, "launchPurchaseFlowAsync onSuccess, Payload is not valid."); return; } // 구매완료 후 signature 검증을 수행한다. boolean validPurchase = AppSecurity.isValidPurchase(purchaseData.getPurchaseData(), purchaseData.getSignature()); if (validPurchase) { if (product5000.equals(purchaseData.getProductId())) {{ // 관리형상품(inapp)은 구매 완료 후 소비를 수행한다. consumeItem(purchaseData); } } else { Log.d(TAG, "launchPurchaseFlowAsync onSuccess, Signature is not valid."); return; } } @Override public void onError(IapResult result) { Log.e(TAG, "launchPurchaseFlowAsync onError, " + result.toString()); } @Override public void onErrorRemoteException() { Log.e(TAG, "launchPurchaseFlowAsync onError, 원스토어 서비스와 연결을 할 수 없습니다"); } @Override public void onErrorSecurityException() { Log.e(TAG, "launchPurchaseFlowAsync onError, 비정상 앱에서 결제가 요청되었습니다"); } @Override public void onErrorNeedUpdateException() { Log.e(TAG, "launchPurchaseFlowAsync onError, 원스토어 서비스앱의 업데이트가 필요합니다"); }}; int IAP_API_VERSION = 5;int PURCHASE_REQUEST_CODE = 1000; // onActivityResult 로 전달받을 request codeString product5000 = "p5000"; // 구매요청 상품IDString productName = ""; // "" 일때는 개발자센터에 등록된 상품명 노출String productType = IapEnum.ProductType.IN_APP.getType(); // "inapp"String devPayload = AppSecurity.generatePayload();String gameUserId = ""; // 디폴트 ""boolean promotionApplicable = false; mPurchaseClient.launchPurchaseFlowAsync(IAP_API_VERSION, "호출Activity".this, PURCHASE_REQUEST_CODE, product5000, productName, productType, devPayload, gameUserId, promotionApplicable, mPurchaseFlowListener);

    • 결제 성공 시 리스너로 전달되는 Purchase에 대한 정보는 인앱결제 레퍼런스 페이지 내 'getPurchaseIntent() - 구매 요청' 부분을 참고하시기 바랍니다. 결제 완료에 대한 결과는 launchPurchaseFlowAsync를 호출한 Activity의 onActivityResult로 전달되는데, 이곳에서 구매 결과를 SDK 내에서 처리할 수 있도록 handlePurchaseData 메서드를 반드시 추가해야 합니다. handlePurchaseData 메서드 수행시 구매요청 파라미터로 넣었던 PurchaseFlowListener 가 null일 경우 false를 반환합니다. 성공 및 에러에 대한 처리는 PurchaseFlowListener 리스너를 통하여 응답결과가 전달됩니다.

      @Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) { Log.e(TAG, "onActivityResult resultCode " + resultCode); switch (requestCode) { case PURCHASE_REQUEST_CODE: /* * launchPurchaseFlowAsync API 호출 시 전달받은 intent 데이터를 handlePurchaseData를 통하여 응답값을 파싱합니다. * 파싱 이후 응답 결과를 launchPurchaseFlowAsync 호출 시 넘겨준 PurchaseFlowListener 를 통하여 전달합니다. */ if (resultCode == Activity.RESULT_OK) { if (mPurchaseClient.handlePurchaseData(data) == false) { Log.e(TAG, "onActivityResult handlePurchaseData false "); // listener is null } } else { Log.e(TAG, "onActivityResult user canceled"); // user canceled , do nothing.. } break; default: }}

  • 구매내역 조회하기

    • 상품 소비하기

      • 관리형 상품(managed product)은 구매한 상품을 소비(consume)하기 전까지는 재구매할 수 없습니다.상품을 구매한 즉시 해당 상품에 대한 구매 정보를 원스토어에서 관리하게 되며, 상품을 소비하면 상품 구매에 대한 사용자의 권한은 즉시 회수됩니다. 즉, 관리형 상품을 구매하고 소비하지 않으면 영구성 형태의 상품 타입처럼 활용할 수 있으며, 구매 후 즉시 소비하면 소비성 형태의 상품 타입, 특정 기간 이후에 구매 상품을 소비하면 기간제 형태의 상품 타입처럼 활용할 수 있습니다. 구매 상품을 소비하려면 launchPurchaseFlowAsync나 queryPurchasesAsync로 전달받은 구매 정보를 consumeAsync 메서드의 파라미터로 전달하여 호출합니다.

        /* * PurchaseClient의 consumeAsync API (상품소비) 콜백 리스너 */PurchaseClient.ConsumeListener mConsumeListener = new PurchaseClient.ConsumeListener() { @Override public void onSuccess(PurchaseData purchaseData) { Log.d(TAG, "consumeAsync onSuccess, " + purchaseData.toString()); // 상품소비 성공, 이후 시나리오는 각 개발사의 구매완료 시나리오를 진행합니다. } @Override public void onErrorRemoteException() { Log.e(TAG, "consumeAsync onError, 원스토어 서비스와 연결을 할 수 없습니다"); } @Override public void onErrorSecurityException() { Log.e(TAG, "consumeAsync onError, 비정상 앱에서 결제가 요청되었습니다"); } @Override public void onErrorNeedUpdateException() { Log.e(TAG, "consumeAsync onError, 원스토어 서비스앱의 업데이트가 필요합니다"); } @Override public void onError(IapResult result) { Log.e(TAG, "consumeAsync onError, " + result.toString()); }}; int IAP_API_VERSION = 5;PurchaseData purchaseData; // 구매내역조회 및 구매요청 후 전달받은 PurchaseDatamPurchaseClient.consumeAsync(IAP_API_VERSION, purchaseData, mConsumeListener);

    • 월정액상품 상태변경 요청하기

      • 월정액상품 타입(auto)은 최초 구매후 1개월 뒤에 자동결제가 이루어집니다. 자동결제 전에 월정액상품의 상태를 변경하기 위해서는 해당 메서드를 이용하여 월정액상품의 상태(recurringState)를 변경할 수 있습니다(상태정보는 구매내역조회하기 정보에 포함되어있습니다). 월정액상품 상태변경을 요청하려면 launchPurchaseFlowAsync나 queryPurchasesAsync로 전달받은 구매 정보를 manageRecurringProductAsync 메서드의 파라미터로 전달하여 호출합니다. 또한 월정액 해지예약 시에는 action 필드를 "cancel", 월정액 해지예약취소 시에는 action 필드를 "reactivate"로 전달하여 호출합니다. 다음은 메서드를 호출하는 샘플 코드입니다.

        /* * PurchaseClient의 manageRecurringProductAsync API (월정액상품 상태변경) 콜백 리스너 */PurchaseClient.ManageRecurringProductListener mManageRecurringProductListener = new PurchaseClient.ManageRecurringProductListener() { @Override public void onSuccess(PurchaseData purchaseData, String manageAction) { Log.d(TAG, "manageRecurringProductAsync onSuccess, " + manageAction + " " + purchaseData.toString()); } @Override public void onErrorRemoteException() { Log.e(TAG, "manageRecurringProductAsync onError, 원스토어 서비스와 연결을 할 수 없습니다"); } @Override public void onErrorSecurityException() { Log.e(TAG, "manageRecurringProductAsync onError, 비정상 앱에서 결제가 요청되었습니다"); } @Override public void onErrorNeedUpdateException() { Log.e(TAG, "manageRecurringProductAsync onError, 원스토어 서비스앱의 업데이트가 필요합니다"); } @Override public void onError(IapResult result) { Log.e(TAG, "manageRecurringProductAsync onError, " + result.toString()); }}; int IAP_API_VERSION = 5;PurchaseData purchaseData; // 구매내역조회 및 구매요청 후 전달받은 PurchaseDataString action = IapEnum.RecurringAction.CANCEL.getType(); // "cancel"mPurchaseClient.manageRecurringProductAsync(IAP_API_VERSION, purchaseData, action, mManageRecurringProductListener);

    • 원스토어 로그인 요청하기

      • 원스토어 로그인을 수행하기 위해서 launchLoginFlowAsync 메서드를 호출합니다. 파라미터로 전달하는 requestCode값은 후에 onActivityResult로 전달되는 데이터를 확인하기 위한 용도로 활용됩니다.

        /* * PurchaseClient의 launchLoginFlowAsync API (로그인) 콜백 리스너 */PurchaseClient.LoginFlowListener mLoginFlowListener = new PurchaseClient.LoginFlowListener() { @Override public void onSuccess() { Log.d(TAG, "launchLoginFlowAsync onSuccess"); // 개발사에서는 로그인 성공시에 대한 이후 시나리오를 지정하여야 합니다. } @Override public void onError(IapResult result) { Log.e(TAG, "launchLoginFlowAsync onError, " + result.toString()); } @Override public void onErrorRemoteException() { Log.e(TAG, "launchLoginFlowAsync onError, 원스토어 서비스와 연결을 할 수 없습니다"); } @Override public void onErrorSecurityException() { Log.e(TAG, "launchLoginFlowAsync onError, 비정상 앱에서 결제가 요청되었습니다"); } @Override public void onErrorNeedUpdateException() { Log.e(TAG, "launchLoginFlowAsync onError, 원스토어 서비스앱의 업데이트가 필요합니다"); } }; int IAP_API_VERSION = 5;int LOGIN_REQUEST_CODE = 2000; // onActivityResult 로 전달받을 request code mPurchaseClient.launchLoginFlowAsync(IAP_API_VERSION, "호출Activity".this, LOGIN_REQUEST_CODE, mLoginFlowListener);

        • 로그인 완료에 대한 결과는 launchLoginFlowAsync를 호출한 Activity의 onActivityResult로 전달되는데, 이곳에서 구매 결과를 SDK 내에서 처리할 수 있도록 handleLoginData 메서드를 반드시 추가해야 합니다. handleLoginData 메서드 수행시 구매요청 파라미터로 넣었던 LoginFlowListener 가 null일 경우 false를 반환합니다. 성공 및 에러에 대한 처리는 LoginFlowListener 리스너를 통하여 응답결과가 전달됩니다.

          @Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) { Log.e(TAG, "onActivityResult resultCode " + resultCode); switch (requestCode) { case LOGIN_REQUEST_CODE: /* * launchLoginFlowAsync API 호출 시 전달받은 intent 데이터를 handleLoginData를 통하여 응답값을 파싱합니다. * 파싱 이후 응답 결과를 launchLoginFlowAsync 호출 시 넘겨준 LoginFlowListener 를 통하여 전달합니다. */ if (resultCode == Activity.RESULT_OK) { if (mPurchaseClient.handleLoginData(data) == false) { Log.e(TAG, "onActivityResult handleLoginData false "); // listener is null } } else { Log.e(TAG, "onActivityResult user canceled"); // user canceled , do nothing.. } break; }}

  • 소비성 형태의 상품 구현 시 주의할 점

    • 관리형 상품은 사용자가 한 번 구매한 이후에는 원스토어에 의해서 관리되며, 동일 상품을 재구매할 수 없습니다. 계속해서 구매할 수 있는 소비성 형태의 상품으로 사용하려면 구매가 완료된 직후 바로 구매 상품을 소비시켜야 하며, 반드시 소비 요청이 성공하고 난 후에 사용자에게 아이템을 지급해야 합니다. 구매 프로세스 진행 시 순간적으로 네트워크가 단절되어 사용자의 구매는 완료되었으나 개발사 앱에서는 실패로 결과가 전달되거나, 알 수 없는 오류로 앱에 충돌이 발생하여 아예 구매 결과를 받지 못할 수도 있습니다. 이 경우엔 사용자가 금액은 지불하였으나 개발사 앱의 아이템을 지급받지 못하는 중대한 문제가 발생하게 됩니다. 이런 문제를 최소화하고 사용자가 동일 상품을 구매할 때 문제가 발생하지 않게 하려면, 앱이 실행될 때 또는 구매 복원 버튼 등을 제공하여 소비되지 않은 상품의 목록을 가져와서 상품소비를 진행한 후 개발사 앱의 아이템 지급을 재처리하는 로직을 반드시 넣어 주어야 합니다.

Last updated