Tag Archives: Android

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

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

Phrase를 이용한 Android String Formatting

Phrase는 안드로이드용 텍스트 토큰 교체를 위한 마이크로 라이브러리입니다. Phrase는 미국의 유명한 결제 시스템 회사인 Squre에서 자사의 Squre Register라는 안드로이드 어플리케이션을 프랑스와 일본어로 번역하는 과정에서 겪은 문제점들을 해결하기 위해 만들어졌습니다.

다음과 같은 문자열 선언이 strings.xml에 정의되어있다고 가정해 보겠습니다.

<string name="greeting">
  Hello %1$s, today\'s cook yielded %2$d %3$s.
</string>

greeting에 정의된 포매팅은 간단한 형태를 띄고있습니다. 안드로이드의 Context는 정의되어있는 문자열을 가져오고 한번에 포맷팅할 수 있는 오버로딩된getString(…) 메소드를 제공하고 있습니다.

String name = "Walter";
int yield = 50;
String unit = "pounds";
String greeting = context.getString(R.string.greeting, name, yield, unit);

하지만 위에서 볼 수 있는 %1$s 와 같은 문법은 프로그래머가 아닌 사람들에게는 명확하게 이해하기 어렵습니다. 각각의 포매팅 지정자들이 번역될 때 이러한 이해하기 어려운 형태의 지정자들로 인해 오타등의 실수가 발생할 수 있습니다. 또한 지정자들이 또다른 지정자들 혹은 문자열에 인접해 있을 때 더 많은 문제가 발생했습니다.

또다른 문제는 %2$d와 같은 지정자의 의미를 해석하기 쉽지 않다는 점 입니다. 우리는 주변의 텍스트를 통해 여기에 들어갈 문자의 의미를 알아차릴 수 있을 것이고 Java 코드상의 버그를 피하기 위해 파라미터의 순서가 정확하게 일치해야만 합니다.

// 파라미터의 순서가 잘못되었으므로 버그 발생!
String greeting = context.getString(R.string.greeting, name, unit, yield);

마지막으로 Context.getString(…)은 볼드나 이탤릭같은 스타일이 적용된 텍스트를 처리할 수 없다는 점 입니다. 당신이 strings.xml에 간단한 HTML 태그를 포맷 지정자와 함께 사용하였다면 HTML 태그는 조용히 무시될 것입니다.

Phrase

Phrase를 사용하면 greeting은 다음과 같이 변화됩니다. 보시다 싶이 애매모호했던 지정자들은 읽기 쉽고 이해하기 쉬운 {name} 과 {yield} 로 변경되었습니다.

<string name="greeting">
  Hello {name}, today\'s cook yielded {yield} {unit}.
</string>

번역중 발생할 수 있는 오류를 줄이기 위한 첫번째 목표와 이것을 지키기 위한 룰은 간단합니다.

  • 키를 중괄호 {} 로 또한번 감싸야 할 경우(보여지기 위해) 두개의 {{ 를 사용하여 이스케이프 처리 할 수 있습니다.
  • 키는 소문자 영문자로 시작해야 하고 다음으로는 소문자와 언더스코어 _ 가 사용 가능합니다.

더 많은 유연성을 제공하는것은 복잡성을 증가시키기에 우리는 대문자, 숫자, 또는 밑줄 이외의 특수문자를 허용하지 않습니다.

포맷팅은 유연한 형태로 변경 되었고 순서가 없는 키/밸류 형태의 알기쉬운 키형태를 사용하게 되었기에 프로그래머의 인생이 좀 더 편안해 졌습니다. (과연?ㅎ)

// 순서와 상관없이 put(...)을 호출
CharSequence greeting = Phrase.from(context, R.string.greeting)
    .put("unit", unit)
    .put("name", name)
    .put("yield", yield)
    .format();

Phrase는 문자열에 삽입되어있는 HTML태그와 포맷지정자들을 모두 살리기 위해 String 대신에 CharSequence를 반환합니다.

<string name="did_you_learn">
  <!-- class_type is something like Chemistry. -->
  Did you learn <b>nothing</b> from my {class_type} class?
</string>

Validation

Phrase는 fail-fast 철학을 준수합니다. Phrase는 다음과 같은 실수가 발생할 경우 예외를 발생시킵니다.

  • 패턴의 파싱중에 잘못된 중괄호 패턴이나 키에 잘못된 문자열이 사용되었을 경우
  • put(…)을 호출할 때 키 또는 밸류에 null 이 들어갈 경우
  • 포매팅 패턴에 존재하지 않는 키를 put(…)으로 넣을 경우
  • format() 이 호출되는 시점에 모든 키에 대해 밸류가 채워지지 않은 경우

즉각적인 예외 또는 크래시 발생은 개발 과정에서의 실수를 줄여주고 당황스러운 번역 실수 또는 포맷팅 되지 않은 문자열이 표시되는 확률을 최소화 해줍니다. 왜냐하면 키들은 이해하기 쉽고 각각의 언어별로 strings.xml에 정의된 Phrase 키가 똑같기 때문에 유효성 스크립트를 제작하는것도 간단해집니다.

Example

간단하게 위의 내용을 테스트 해보도록 하겠습니다. 개발은 맥 환경의 Android Studio와 Gradle을 이용하였습니다. 먼저 간단한 프로젝트를 만들어 보도록 하겠습니다. 저는 HelloPhrase라는 이름의 프로젝트를 만들었습니다. 이후에 build.gradle 파일의 내용을 수정합니다.

1

dependencies 에 다음의 한줄을 추가하면 사용준비가 끝납니다. 현재 시점에서는 1.0.3이 최신버전입니다.

dependencies {
  compile 'com.squareup.phrase:phrase:(insert latest version)'
}

strings.xml에는 다음을 추가하였습니다.

<string name="greeting">
Hello <b>{name}</b>, today\'s cook yielded <u>{yield}</u> {unit}.
</string>

적절한 텍스트뷰에 위의 결과물을 출력해 보겠습니다.

TextView tv = (TextView) rootView.findViewById(R.id.text);

CharSequence greeting = Phrase.from(getActivity(), R.string.greeting)
    .put("unit", "pounds")
    .put("name", "Walter")
    .put("yield", 50)
    .format();

tv.setText(greeting);

device-2014-01-29-110001

정상적으로 결과물이 출력되는것을 보았습니다. 실제로 개발중에 느낀건데 조금이라도 값이 안맞거나 하면 바로 크래시가 나는군요. 서버에서 출력할 메시지를 받는 경우 잘못하면 앱이 죽어버리는 문제를 야기할 수 도 있을것 같습니다. 사용에 유의하시기 바랍니다.

참고 : http://corner.squareup.com/2014/01/phrase.html