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

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