Category Archives: Android

Android In-App Billing 구현하기 (IAB Version 3)

android_logo

Google Play의 In-App Billing(IAB)은 앱내구매 요청을 하거나 관리하는데에 직관적이고 간단한 인터페이스를 제공합니다. 이번 글에서는 당신의 어플리케이션에서 In-App Billing 버전 3를 이용하여 어떻게 요청을 만드는지 기초적인 내용을 정리합니다. 이 내용은 기본적으로 In-App Billing의 프로세스에 대해 어느정도 이해하고 있는 상태라고 가정하고 작성되었습니다.

어플리케이션에서 IAB를 구현하기 위해서는 다음과 같은 구현 작업이 필요합니다.

  1. In-App Billing 라이브러리를 당신의 프로젝트에 추가합니다.
  2. AndroidManifest.xml 파일을 업데이트합니다.
  3. ServiceConnection을 생성하고 IInAppBillingService에 바인딩합니다.
  4. 당신의 어플리케이션에서 IInAppBillingService로 In-App Billing 요청을 날립니다.
  5. Google Play로부터 도작한 In-App Billing 응답을 처리합니다.

AIDL 파일을 프로젝트에 추가하기

IInAppBillingService.aidl 파일은 In-App Billing 버전 3 서비스로의 요청이 정의되어있는 Android Interface Definition Language(AIDL)입니다. 당신은 이 인터페이스를 사용하여 IPC(Inter-Process Communication) 메소드 호출을 함으로써 구매 요청을 할 수 있습니다. 프로젝트에 이 AIDL파일을 추가하기 위해서는 다음을 수행해 주시면 됩니다.

  1. IInAppBillingService.aidl 파일을 당신의 안드로이드 프로젝트로 복사합니다.
    • 프로젝트의 src/com/android/vending/billing 위치안에 복사합니다.
  2. 정상적으로 프로젝트에 추가되었다면 빌드 시(Ant, Gradle) /gen 디렉토리 안에 IInAppBillingService.java 파일이 생성됩니다.

AndroidManifest 파일을 수정하기

In-App Billing 요청은 당신의 어플리케이션과 Google Play 서버간의 모든 통신을 처리하는 Google Play 어플리케이션을 필요로합니다. 이 Google Play 어플리케이션을 이용하기 위해서는 당신의 어플리케이션에 적절한 퍼미션이 추가되어있어야 합니다. com.android.vending.BILLING 퍼미션을 AndroidManifest.xml에 추가함으로써 구매 요청이 가능하게 됩니다. 당신의 어플리케이션이 이 퍼미션을 설정하지 않았다면 모든 구매 요청은 Google Play로부터 거부될것이며 에러를 발생하게 됩니다. In-App Billing 요청을 위한 AndroidManifest.xml에 추가해야 하는 설정은 다음과 같습니다.

<uses-permission android:name="com.android.vending.BILLING" />

ServiceConnection 생성하기

Google Play와의 통신을 위해 당신의 어플리케이션은 반드시 ServiceConnection을 가지고 있어야 합니다. 최소한의 구현을 위해 다음과 같은 구현을 추가할 필요가 있습니다.

  • IInAppBillingService에 바인딩하기
  • Google Play 어플리케이션으로의 IPC 메소드 호출을 통한 구매 요청을 날리기
  • 각각의 구매 요청에 대한 동기(Synchronous) 응답 메시지를 처리하기

IInAppBillingService에 바인딩하기

Google Play의 In-App Billing 서비스와의 커넥션을 맺기 위해 ServiceConnection을 구현하여 당신의 Activity를 IInAppBillingService에 바인딩 해주어야 합니다. onServiceDisconnected와 onServiceConnected 메소드를 오버라이딩 하여 커넥션이 연결된 후 IInAppBillingService 인스턴스를 가져올 수 있습니다.

IInAppBillingService mService;

ServiceConnection mServiceConn = new ServiceConnection() {
   @Override
   public void onServiceDisconnected(ComponentName name) {
       mService = null;
   }

   @Override
   public void onServiceConnected(ComponentName name, 
      IBinder service) {
       mService = IInAppBillingService.Stub.asInterface(service);
   }
};

당신의 Activity의 onCreate 메소드안에서 위에서 구현한 ServiceConnection 인스턴스를 가진 Intent를 파라미터로 하여 bindService 메소드를 호출합니다.

@Override
public void onCreate(Bundle savedInstanceState) {    
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);        
    bindService(new 
        Intent("com.android.vending.billing.InAppBillingService.BIND"),
                mServiceConn, Context.BIND_AUTO_CREATE);

정상적으로 Google Play 서비스와 연결이 이루어졌다면 mService를 통해 통신할 수있게 됩니다.

구매를 진행하는 Activity가 종료된다면 In-App Billing 서비스를 종료(Unbind)해주어야 합니다. 만약 바인딩을 종료해 주지 않을 경우 서비스 커넥션은 계속해서 유지되고 디바이스의 퍼포먼스에 영향을 끼칠 수 있습니다. 다음의 예시는 onDestroy 메소드를 오버라이딩하여 mServiceConn을 명시적으로 Unbind 해주는것을 보여주고 있습니다.

@Override
public void onDestroy() {
    super.onDestroy();
    if (mService != null) {
        unbindService(mServiceConn);
    }   
}

In-App Billing 요청 생성하기

어플리케이션이 Google Play와 한번 연결되면 당신은 앱내 판매되는 상품의 구매 요청을 할 수 있게 됩니다. Google Play는 체크아웃 인터페이스를 유저에게 제공하여 적절한 구매 방법을 제공하게 됩니다. 그러므로 당신의 어플리케이션에서 직접적으로 구매를 처리할 필요는 없습니다. 아이템이 정상적으로 구매가 되면 Goole Play는 아이템에 대한 소유권을 획득했는지 확인하며 그 아이템이 소진(Consume)되기전까지 같은 아이템이 중복 구매되는것을 방지해줍니다. 당신은 당신의 어플리케이션에서 구매한 상품의 소진을 어떻게 할것인지를 컨트롤할 수 있습니다. 또한 Google Play에 요청하여 유저에 의해 생성된 구매 리스트를 빠르게 받아올 수 있습니다. 이것은 매우 유용한데 예를 들어 당신의 어플리케이션이 실행되는 시점에 유저의 이전에 실패한 구매 요청을 복원하여 처리할 수 있습니다.

구매 가능한 아이템 목록 가져오기

In-App Billing 버전 3 API를 사용하여 Google Play로 부터 상품의 상세 정보를 가져올 수 있습니다. In-App Billing 서비스에 요청을 하기 위해서는 우선 상품의 아이디 목록을 가지고 있는 Bundle을 생성하여야 합니다.

ArrayList<String> skuList = new ArrayList<String> ();
skuList.add("premiumUpgrade");
skuList.add("gas");
Bundle querySkus = new Bundle();
querySkus.putStringArrayList(“ITEM_ID_LIST”, skuList);

Google Play로 부터 정보를 받아오기 위해서는 getSkuDetails 메소드를 호출하면 됩니다. 여기에 사용되는 파라미터는 IAB API 버전을 의미하는 3과 당신의 어플리케이션의 패키지명, 그리고 위에서 생성한 Bundle을 사용합니다. getSkuDetails 메소드를 호출할 때에는 메인 쓰레드에서 실행하면 안된다는것을 유의해주시기 바랍니다.

Bundle skuDetails = mService.getSkuDetails(3, 
   getPackageName(), "inapp", querySkus);

요청이 성공한다면 BILLING_RESPONSE_RESULT_OK (0) 응답 코드를 가지고 있는 Bundle이 반환됩니다. 모든 응답 코드를 확인하시려면 이 [링크]를 참조하시기 바랍니다.

요청의 결과는 String ArrayList가 DETAILS_LIST라는 키로 담겨져 오게 됩니다. 각각의 정보는 JSON 포맷으로 이루어져 입니다. 상품 상세정보의 각각의 타입에 대한 정의를 보실려면 [이곳]을 참고하시기 바랍니다. 다음의 예제는 위의 코드를 통해 받은 응답 Bundle로부터 가격을 꺼내보는 예시입니다.

int response = skuDetails.getInt("RESPONSE_CODE");
if (response == 0) {
   ArrayList<String> responseList
      = skuDetails.getStringArrayList("DETAILS_LIST");

   for (String thisResponse : responseList) {
      JSONObject object = new JSONObject(thisResponse);
      String sku = object.getString("productId");
      String price = object.getString("price");
      if (sku.equals("premiumUpgrade")) mPremiumUpgradePrice = price;
      else if (sku.equals("gas")) mGasPrice = price;
   }
}

구매 요청하기

당신의 어플리케이션에서 구매를 시작하기 위해서는 In-App Billing 서비스의 getBuyIntent 메소드를 호출해주면 됩니다. In-App Billing API 버전을 의미하는 3과 패키지명이 마찬가지로 사용되며 구매하려는 상품의 아이디와 구매의 타입(inapp 또는 subs)과 마지막으로 developerPayload 문자열이 포함됩니다. 이 추가적인 developerPayload 파라미터는 Google Play가 구매 정보에 함께 포함하여 그대로 되돌려주게 됩니다.

Bundle buyIntentBundle = mService.getBuyIntent(3, getPackageName(),
   sku, "inapp", "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ");

요청이 성공한다면 BILLING_RESPONSE_RESULT_OK (0) 응답 코드를 가지고 있는 Bundle이 반환됩니다. 이 Bundle에는 BUY_INTENT라는 이름으로 구매 플로우를 시작하는데 사용되는 PendingIntent가 들어있습니다.

PendingIntent pendingIntent = buyIntentBundle.getParcelable("BUY_INTENT");

이 구매 트랜젝션을 완료하기 위해서는 위에서 얻은 PendingIntent를 이용하여  startIntentSenderForResult 메소드를 호출하여야 합니다. 다음의 예제에서 Request Code로 1001을 사용하였습니다.

startIntentSenderForResult(pendingIntent.getIntentSender(),
   1001, new Intent(), Integer.valueOf(0), Integer.valueOf(0),
   Integer.valueOf(0));

Google Play는 당신의 어플리케이션의 onActivityResult 메소드로 PendingIntent 형태의 응답을 보내주게 됩니다. onActivityResult 메소드는 Activity.RESULT_OK (1) 또는 Activity.RESULT_CANCELED (0) 를 Result Code로 받게 됩니다. 여기서 Intent 형태의 응답을 받게 됩니다. 여기에 담겨있는 정보들의 타입을 확인하려면 [이곳]을 참고하시기 바랍니다.

구매 데이터는 응답 Intent안에 INAPP_PURCHASE_DATA라는 이름의 키로 JSON 포맷 형태의 문자열로 저장되어있습니다.

'{ 
   "orderId":"12999763169054705758.1371079406387615", 
   "packageName":"com.example.app",
   "productId":"exampleSku",
   "purchaseTime":1345678900000,
   "purchaseState":0,
   "developerPayload":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
   "purchaseToken":"rojeslcdyyiapnqcynkjyyjh"
 }'

위의 내용을 종합하여 보면 다음과 같은 코드가 됩니다. RESPONSE_CODE, INAPP_PURCHASE_DATA, INAPP_DATA_SIGNATURE값을 받아오는것을 확인할 수 있습니다.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { 
   if (requestCode == 1001) {           
      int responseCode = data.getIntExtra("RESPONSE_CODE", 0);
      String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
      String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE");

      if (resultCode == RESULT_OK) {
         try {
            JSONObject jo = new JSONObject(purchaseData);
            String sku = jo.getString("productId");
            alert("You have bought the " + sku + ". Excellent choice, 
               adventurer!");
          }
          catch (JSONException e) {
             alert("Failed to parse purchase data.");
             e.printStackTrace();
          }
      }
   }
}

보안을 위한 조언 : 구매 요청을 보낼 때 구매 요청을 식별할 수 있는 유니크한 문자열 토큰을 생성하여 developerPayload에 담아 보내시기 바랍니다. 랜덤하게 생성된 토큰 형태의 이 문자열을 사용하면 Google Play로부터의 응답을 받을 때 orderId와 developerPayload문자열 값을 통해 응답 데이터가 진짜인지 확인할 수 있습니다. 추가적인 보안을 위해 구매 확인을 위한 자체적인 보안 서버를 구축하기를 권장합니다. 이 서버를 통해 유니크한 값인 orderId가 이전에 이미 처리된적이 없는지 확인할 수 있고 developerPayload 문자열은 이전에 요청에 내가 담아 보낸 값과 동일한지를 확인할 수 있습니다.

구매한 아이템 정보를 가져오기

IAB 버전 3 서비스의 getPurchases 메소드를 호출함으로써 어플리케이션으로부터 사용자가 생성한 구매 정보를 가져올 수 있습니다. IAB 버전을 의미하는 3과 패키지명과 구매타입(inapp 또는 subs)을 파라미터로 사용합니다.

Bundle ownedItems = mService.getPurchases(3, getPackageName(), "inapp", null);

Google Play 서비스는 현재의 디바이스에 로그인되어있는 유저의 어카운트가 생성한 구매의 정보만을 반환합니다. 요청이 성공한다면 응답 코드가 0인 Bundle을 반환합니다. 이 Bundle은 상품의 아이디들과 각각의 구매에 대한 상세 정보를 담은 리스트를 가지고 있습니다.

퍼포먼스 향상을 위해 In-App Billding 서비스는 getPurchases가 처음 호출될 때 유저가 보유하고 있는 상품의 정보를 최대 700개 까지만 반환합니다. 만약 유저가 굉장히 많은 수의 상품을 구매하였다면 Google Play는 아직 더 많은 구매한 상품이 남아있다는 의미를 가진 INAPP_CONTINUATION_TOKEN 키를 함께 보내주게 됩니다. 이 값이 존재할 경우 이 값을 포함하여 getPurchases를 한번 더 요청하여 남은 구매 상품의 정보를 가져오면 됩니다. getPurchases 메소드의 4번째 인자에 INAPP_CONTINUATION_TOKEN 값을 담아서 요청하면 됩니다.

int response = ownedItems.getInt("RESPONSE_CODE");
if (response == 0) {
   ArrayList<String> ownedSkus =
      ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
   ArrayList<String>  purchaseDataList =
      ownedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
   ArrayList<String>  signatureList =
      ownedItems.getStringArrayList("INAPP_DATA_SIGNATURE");
   String continuationToken = 
      ownedItems.getString("INAPP_CONTINUATION_TOKEN");

   for (int i = 0; i < purchaseDataList.size(); ++i) {
      String purchaseData = purchaseDataList.get(i);
      String signature = signatureList.get(i);
      String sku = ownedSkus.get(i);

      // 구매한 상품의 정보를 이용하여 무언가를 처리하면 됩니다.
      // e.g. 유저가 보유한 상품의 리스트를 업데이트
   } 

   // 만약 continuationToken != null 이라면 getPurchases를 한번더 호출합니다.
   // INAPP_CONTINUATION_TOKEN 토큰값을 사용하여 이후의 데이터를 받아올 수 있습니다.
}

구매를 소진 (Consuming)

당신은 In-App Billing 버전 3 API를 사용하여 Google Play의 구매한 앱내 상품에 대해 소유권을 가지고 있는지를 추적할 수 있습니다. 한번 앱내 상품이 구매되면 그것이 소유되었다(owned)고 간주되며 똑같은 상품을 또 구매할 수 없게 됩니다. 반드시 앱내 구매한 상품에 대해 소진 요청을 Google Play에 보내야 하며 Google Play는 해당 상품을 다시 구매 가능한 상태로 전환합니다.

중요 : 관리되는 제품은 소진이 가능하며 구독 상품은 불가능합니다.

상품의 소진을 기록하기 위해 In-App Billing 서비스에 purchaseToken 문자열을 담아 consumePurchase 메소드를  호출해야 합니다. 이 purchaseToken은 구매 요청이 성공했을 때 받았던 응답의 INAPP_PURCHASE_DATA 안에 담겨있습니다. 예를 들어 소진할 상품의 식별자로 pruchaseToken값을 token이라는 변수에 담아 호출해 보았습니다.

int response = mService.consumePurchase(3, getPackageName(), token);

주의 : consumePurchase 메소드를 메인 쓰레드에서 호출하지 않도록 주의해 주시기 바랍니다.

응답인 response가 0일 경우 정상적으로 소진 처리가 되었다고 판단하시면 됩니다. 이제 구매한 상품에 대해 유저에게 무엇을 제공할 것인지에 대한 처리는 당신의 몫입니다. 예를 들어 유저가 게임 화폐를 구매하였다면 유저의 인벤토리안의 보유 금액을 업데이트 하여야 할것입니다.

보안을 위한 조언 : 유저에 구매한 상품의 정보를 업데이트 하기 이전에 반드시 소진 요청을 먼저 하시기 바랍니다. 즉, 소진 요청이 정상적으로 성공한 뒤에 유저에게 상품 지급 처리를 하시는것을 권장합니다.

구독 상품의 구현

구독의 구현은 상품의 타입을 subs로 해야한다는것을 제외하면 상품의 구매 플로우와 매우 비슷합니다. 구매 결과는 마찬가지로 Activity의 onActivityResult 메소드에서 받으며 앱내 상품 구매의 경우와 동일하게 처리됩니다.

Bundle bundle = mService.getBuyIntent(3, "com.example.myapp",
   MY_SKU, "subs", developerPayload);

PendingIntent pendingIntent = bundle.getParcelable(RESPONSE_BUY_INTENT);
if (bundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) {
   // Google Play UI를 띄워 구매 플로우를 시작합니다.
   // 결과는 onActivityResult() 를 통해 반환됩니다.
   startIntentSenderForResult(pendingIntent, RC_BUY, new Intent(),
       Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0));
}

구매된 구독을 활성화(Active)하기 위해서는 subs 파라미터를 포함한 getPurchases 메소드를 이용하면 됩니다.

Bundle activeSubs = mService.getPurchases(3, "com.example.myapp",
                   "subs", continueToken);

이 요청은 유저가 소유한 모든 활성화된 구독 상품의 정보를 Bundle에 담아 반환합니다. 한번 만료(expire)된, 리뉴얼되지 않은 구독 상품의 경우 더이상 Bundle을 통해 반한되지 않습니다.

참고

Apache Thrift를 Android 통신에 활용하기

thrift_logo

Apache Thrift는 다양한 플랫폼간에 매우 편리하게 사용할 수 있는 통합 RPC 환경을 제공합니다. 제가 실무에서 겪을 수 있는 대부분의 Thrift활용 예는 서버들간의 통신들에 국한되어 있었는데요. 검색을 해봐도 Thrift는 서버에만 사용해야 한다는 말은 없더군요. 단지 이것도 하나의 프로토콜에 불과한것이 아닐까 생각됩니다.

그래서 Thrift로 Server ⟷ Android간 통신에 적용해 보기로 하였습니다. 지금까지는 이러한 통신에 HTTP 통신을 이용한 Json을 주고받는 형태로 많이 구현해 봤었는데요, 통신을 위해 서로 프로토콜을 맞추고 인코딩에 신경쓰고 오류메시지에 신경쓰고 하는 부분이 싹 사라졌습니다.

namespace java kr.pe.theeye.thrift.android

service ArithmeticService {
    i32 add(1:i32 num1, 2:i32 num2),
    i32 multiply(1:i32 num1, 2:i32 num2)
}

위와 같은 thrift 파일을 생성하였습니다. 그리고 다음과 같이 Java 코드를 생성할 수 있습니다.

$ thrift --gen java example.thrift

gen-java 디렉토리 밑에 ArithmeticService.java 파일이 생성되었을 것입니다. 이것을 서버와 클라이언트 프로젝트에 동시에 사용하겠습니다. 먼저 서버 프로젝트부터 만들어 보겠습니다. Gradle 프로젝트를 생성합니다.

apply plugin: 'java'

sourceCompatibility = 1.7
version = '1.0'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.apache.thrift:libthrift:+'
    compile 'org.slf4j:slf4j-api:+'
    compile 'org.slf4j:slf4j-jdk14:+'
}

생성된 ArithmeticService 클래스를 사용하여 적절한 핸들러 클래스를 만들어 보겠습니다.

public class ArithmeticServiceImpl implements ArithmeticService.Iface {

    @Override
    public int add(int num1, int num2) throws TException {
        return num1 + num2;
    }

    @Override
    public int multiply(int num1, int num2) throws TException {
        return num1 * num2;
    }
}

이번에는 서버 클래스를 작성하여 보겠습니다.

public class ThriftThreadPoolServer {

    private void start() {
        try {
            ArithmeticService.Processor processor = new ArithmeticService.Processor(new ArithmeticServiceImpl());

            TThreadPoolServer.Args serverArgs = new TThreadPoolServer.Args(new TServerSocket(7911));
            serverArgs.protocolFactory(new TCompactProtocol.Factory());
            serverArgs.transportFactory(new TFastFramedTransport.Factory());
            serverArgs.minWorkerThreads(20);
            serverArgs.maxWorkerThreads(1500);
            serverArgs.processorFactory(new TProcessorFactory(processor));

            TServer server = new TThreadPoolServer(serverArgs);
            System.out.println("Starting server on port 7911 ...");
            server.serve();
        } catch (TTransportException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ThriftThreadPoolServer srv = new ThriftThreadPoolServer();
        srv.start();
    }
}

ThreadPool 서버로 세팅을 하였습니다. 프로토콜은 좀 더 압축률이 높은 CompactProtocol을 사용하였고 FastFramedTransport를 활용하여 통신하려고 합니다. 쓰레드는 최소 20개에서 최대 1500개를 생성하도록 하였습니다. 간단하게 생각해서 동접 1500개 제한이라고 생각하시면 될듯 합니다. 마지막으로 먼저 만들어 두었던 ArithmeticServiceImpl을 RPC 콜이 왔을때 대응할 프로세서(핸들러)로 설정하였습니다.

이번엔 안드로이드 클라이언트를 만들어 보겠습니다. 기본적으로 Fragment를 사용하지 않은 단일 Activity구조로 프로젝트를 만들었습니다. 먼저 Gradle 설정부터 하겠습니다.

apply plugin: 'android'

android {
    compileSdkVersion 19
    buildToolsVersion "19.0.1"

    defaultConfig {
        minSdkVersion 7
        targetSdkVersion 19
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            runProguard false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
    }
    packagingOptions {
        exclude 'META-INF/LICENSE.txt'
        exclude 'META-INF/NOTICE.txt'
    }
}

dependencies {
    compile 'com.android.support:appcompat-v7:+'
    compile 'org.apache.thrift:libthrift:+'
    compile 'org.slf4j:slf4j-api:+'
    compile 'org.slf4j:slf4j-jdk14:+'
    compile fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
}

통신에는 다양한 방법을 사용할 수 있지만 모바일 환경이기에 커넥션을 맺고 끊는것에 신경을 쓰기 어려우므로 비동기 방식으로 구현을 해보겠습니다. 미리 만들어둔 ArithmeticService 클래스가 프로젝트에 포함되어있다고 가정하고 진행하겠습니다.

구현해둔 RPC 메소드는 덧셈, 곱셈 총 두가지 입니다. 이 두가지에 대한 콜백 클래스를 제작합니다.

class AddMethodCallBack implements AsyncMethodCallback<ArithmeticService.AsyncClient.add_call> {

    @Override
    public void onComplete(ArithmeticService.AsyncClient.add_call add_call) {
        try {
            int result = add_call.getResult();
            Log.e(TAG, "AddMethodCallBack onComplete: " + result);
        } catch (TException e) {
            Log.e(TAG, "AddMethodCallBack TException: " + e.getLocalizedMessage());
        }
    }

    @Override
    public void onError(Exception e) {
        Log.e(TAG, "AddMethodCallBack onError: " + e.getLocalizedMessage());
    }
}

class MultiplyMethodCallBack implements AsyncMethodCallback<ArithmeticService.AsyncClient.multiply_call> {

    @Override
    public void onComplete(ArithmeticService.AsyncClient.multiply_call multiply_call) {
        try {
            int result = multiply_call.getResult();
            Log.e(TAG, "MultiplyMethodCallBack onComplete: " + result);
        } catch (TException e) {
            Log.e(TAG, "MultiplyMethodCallBack TException: " + e.getLocalizedMessage());
        }
    }

    @Override
    public void onError(Exception e) {
        Log.e(TAG, "MultiplyMethodCallBack onError: " + e.getLocalizedMessage());
    }
}

이제 비동기 통신을 담당하는 통신 클라이언트 객체를 생성하는 코드를 만들어 보겠습니다.

public class MainActivity extends ActionBarActivity {

    private static final String TAG = "MainActivity";

    private static final String SERVER_HOST_NAME = "192.168.0.10";
    private static final int SERVER_HOST_PORT = 7911;

    private TCompactProtocol.Factory mProtocolFactory;
    private TAsyncClientManager mAsyncClientManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        try {
            mProtocolFactory = new TCompactProtocol.Factory();
            mAsyncClientManager = new TAsyncClientManager();
        } catch (IOException e) {
            Log.e(TAG, "Thrift Client Initialization Failed.");
            Log.e(TAG, e.getLocalizedMessage());
        }
    }

    private ArithmeticService.AsyncClient getAsyncClient() throws IOException {
        return new ArithmeticService.AsyncClient(
                mProtocolFactory,
                mAsyncClientManager,
                new TNonblockingSocket(SERVER_HOST_NAME, SERVER_HOST_PORT));
    }
}

클라이언트가 비동기로 통신하기 위해서는 반드시 NonBlockingSocket을 사용해야만 합니다. 그런데 한가지 문제가 이 NonBlockingSocket은 실제로 동시에 두개의 처리를 할수가 없습니다. 자동으로 채널 셀렉팅을 해주지 않을까 생각했는데 실제로 그렇게 동작하지 않더군요. 그래서 매 요청시마다 커넥션은 새로 만들어 주어야 합니다. 이러한 과정을 getAsyncClient() 메소드에서 처리하도록 하였습니다.

이제 서버와 통신을 해보겠습니다. RPC의 특성상 클라이언트의 메소드를 호출하면 서버의 그것이 실행되어 결과값이 콜백으로 반환됩니다.

try {
    getAsyncClient().add(200, 400, new AddMethodCallBack());
    getAsyncClient().multiply(20, 50, new MultiplyMethodCallBack());
} catch (Exception e) {
    Log.e(TAG, e.getLocalizedMessage());
}

제작한 샘플 코드를 첨부하였습니다.