Apache Thrift 시작하기

개요

여기에서는 블로킹, 논블로킹 및 비동기와 같은 다양한 모드에서 Thrift 서비스 및 클라이언트를 작성하는 방법을 설명한다.

Thrift 0.17.0을 사용해 보도록 하겠다. 언어는 java 로 사용해 보겠다.

사전 준비

먼저, Thrift가 사전에 설치되어 있어야 한다. 설치 방법은 여기 참고 하길 바란다.

Apache Thrift 활용한 개발

Thrift 스크립트로 서비스 정의

설치가 완료되면, Thrift 스크립트를 작성해 보겠다. 이 스크립트를 한번 작성해 놓으면 다양한 언어에서 사용될 수 있는 코드를 자동으로 생성할 수 있다.

namespace java com.devkuma.thrift.tutorial  // define namespace for java code
 
typedef i64 long
typedef i32 int

service ArithmeticService {  // defines simple arithmetic service
  long add(1:int num1, 2:int num2),
  long multiply(1:int num1, 2:int num2),
}

가장 먼저 네임스페이스를 지정하는 것을 볼 수 있다.
코드는 자바의 경우 동일한 package 구조(com.devkuma.thrift.tutorial)로 생성이 되며, 다른 네임스페이스를 지원하는 언어도 마찬가지로 될 것이다.

Thrift에서는 32비트/64비트 두가지 Integer 형을 사용할 수 있는데, 위와 같이 typedef를 통해 재정의하는 것도 가능하다. 64비트 int는 long으로 지정한다.

Service가 서버-서버 또는 서버-클라이언트 통신을 가능하게 하는 구현체를 작성되어 있다. 간단하게 생각해서 클래스와 메소드라고 생각하면 된다.

그럼 이제 만들어 놓은 thrift 스크립트를 코드를 생성해 보겠다. thrift 컴파일로 코드 생성하는 명령어는 아래와 같다.

thrift --gen <language> <Thrift filename>

Java 코드를 생성하기 위해 다음 명령을 실행한다.

% thrift -r --gen java arithmetic.thrift

위 명령어를 실행해서 에러가 없이 실행이 되었다면, gen-java 디렉터리에 네임스페이스에 맞게 /gen-java/com/devkuma/thrift/tutorial/ArithmeticService.java 파일이 생성된다. 이 생성되 파일을 이용해서 서버 및 클라이언트를 생성해 보겟다.

Java 프로젝트 생성

먼저 Java 프로젝트를 gradle 빌드 환경으로 생성하고, 필요한 라이브러리를 추가한다.

위에 성생된 서비스로 즉, Thrift 통신을 하기 위한 기본적인 구현체인 TSocket, Ttransport 등을 사용한다.
그러기에, thrift 라이브러리(org.apache.thrift:libthrift)가 필요하기 때문에 아래와 같이 build.gradle에 의존성을 추가한다.

dependencies {
    implementation 'org.apache.thrift:libthrift:0.18.1'
    implementation 'javax.annotation:javax.annotation-api:1.3.2'
    implementation 'ch.qos.logback:logback-classic:1.4.7'
    implementation 'org.slf4j:slf4j-api:2.0.7'

    // ... 생략 ...
}

서비스 구현체 생성

먼저 Thrift으로 생성된 서비스 인터페이스인 스켈레톤(Skeleton)을 사용하여 서비스를 구현해야 한다. 구현할 인터페이스는 ArithmeticService.Iface이다.

package com.devkuma.thrift.tutorial;

import org.apache.thrift.TException;

public class ArithmeticServiceImpl implements ArithmeticService.Iface {

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

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

Blocking Mode : 블로킹 모드

블로킹 모드 서버와 그 서비스를 사용할 클라이언트를 생성해 보겠다.

먼저 Thrift으로 생성된 서비스 인터페이스인 스켈레톤(Skeleton)을 사용하여 서비스를 구현해야 한다. 구현할 인터페이스는 ArithmeticService.Iface이다.

package com.devkuma.thrift.tutorial;

import org.apache.thrift.TException;

public class ArithmeticServiceImpl implements ArithmeticService.Iface {

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

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

이것으로 인터페이스가 어떻게 동작할지 구현이 되었고, 이제 이 서비스를 요청하는 Thrift 서버를 생성해 보겠다. 이 서버는 블로킹 서버이므로 I/O를 수행하는 서버 스레드는 대기하게 된다.

package com.devkuma.thrift.tutorial;

import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TThreadPoolServer;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TTransportException;

public class BlockingServer {

    private void start() {
        try {
            TServerSocket serverTransport = new TServerSocket(7911);

            ArithmeticService.Processor processor = new ArithmeticService.Processor(new ArithmeticServiceImpl());

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

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

여기서는 들어오는 요청을 처리하기 위해 스레드 풀을 활용하는 TThreadPoolServer 구현이 사용된다.

이번에는 클라이언트를 만들어 보겠다.

package com.devkuma.thrift.tutorial;

import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportException;

public class BlockingClient {

    private void invoke() {
        TTransport transport = null;
        try {
            transport = new TSocket("localhost", 7911);
            TProtocol protocol = new TBinaryProtocol(transport);

            ArithmeticService.Client client = new ArithmeticService.Client(protocol);
            transport.open();

            long addResult = client.add(100, 200);
            System.out.println("Add result: " + addResult);
            long multiplyResult = client.multiply(20, 40);
            System.out.println("Multiply result: " + multiplyResult);

        } catch (TTransportException e) {
            e.printStackTrace();
        } catch (TException e) {
            e.printStackTrace();
        } finally {
            if (transport != null) {
                transport.close();
            }
        }
    }

    public static void main(String[] args) {
        BlockingClient client = new BlockingClient();
        client.invoke();
    }
}

ArithmeticService.Client를 이용하여 서버와 통시하게 된다.

TBinaryProtocol은 서버와 클라이언트 간에 전송되는 데이터를 인코딩하는데 사용된다.

클라이언트에서 add와 multiply 메소드

이것으로 서버를 시작하고, 클라이언트를 사용하여 서비스를 호출해 본다. 결과가 아래와 같이 나오면 잘 동작된 거다.

Add result: 300
Multiply result: 800

TBinaryProtocol을 이용할 경우 객체를 그대로 시리얼라이즈 하여 서버와 통신하게 됩니다. 이 밖에 사용할 수 있는 프로토콜은 다음과 같다.

PROTOCOL DESCRIPTION
TBinaryProtocol 단순하게 바이너리를 그대로 전송한다. 공간 효율은 떨어지지만 다른 텍스트 프로토콜에 비해 처리가 빠르다. 하지만 디버그가 어렵다.
TCompactProtocol TBinaryProtocol의 공간효율을 좀 더 높인 프로토콜이다. 일반적으로 좀 더 효율적으로 처리한다.
TDebugProtocol 디버깅에 용이한 사람이 읽을 수 있는 텍스트 형태로 전송한다.
TDenseProtocol TCompactProtocol과 비슷하지만 전송과 관련된 메타 데이터를 제거한 프로토콜이다.
TJSONProtocol 데이터를 JSON 형태로 인코딩하여 전송한다.

Non Blocking Mode : 논블로킹 모드

이번에는 논블록킹 모드로 동작하는 서버는 아래와 같다. 서비스 구현체는 앞에서 만들었던 ArithmeticServiceImpl를 동일하게 사용하였다.

package com.devkuma.thrift.tutorial;

import org.apache.thrift.server.TNonblockingServer;
import org.apache.thrift.server.TServer;
import org.apache.thrift.transport.TNonblockingServerSocket;
import org.apache.thrift.transport.TNonblockingServerTransport;
import org.apache.thrift.transport.TTransportException;

public class NonblockingServer {

    private void start() {
        try {
            TNonblockingServerTransport serverTransport = new TNonblockingServerSocket(7911);
            ArithmeticService.Processor processor = new ArithmeticService.Processor(new ArithmeticServiceImpl());

            TServer server = new TNonblockingServer(new TNonblockingServer.Args(serverTransport).
                                                            processor(processor));
            System.out.println("Starting server on port 7911 ...");
            server.serve();
        } catch (TTransportException e) {
            e.printStackTrace();
        }
    }

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

여기서는 ServerSocketChannel 캡슐화한 TNonblockingServerSocket이 사용된다. TNonblockingServer를 사용하게 되면, TSimpleServer에서 하나의 요청을 처리하는 동안 다른 요청이 모두 블록되는 문제를 해결할 수 있다. ServerSocketChannel을 이용하여 먼저 들어온 요청이 처리되는 중에도 다른 요청을 받을 수 있게 고안되었다.

이어서 논블록킹 모드로 동작할 수 있는 클라이언트는 아래와 같다.

package com.devkuma.thrift.tutorial;

import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportException;
import org.apache.thrift.transport.layered.TFramedTransport;

public class NonblockingClient {

    private void invoke() {
        TTransport transport = null;
        try {
            transport = new TFramedTransport(new TSocket("localhost", 7911));
            TProtocol protocol = new TBinaryProtocol(transport);

            ArithmeticService.Client client = new ArithmeticService.Client(protocol);
            transport.open();

            long addResult = client.add(100, 200);
            System.out.println("Add result: " + addResult);
            long multiplyResult = client.multiply(20, 40);
            System.out.println("Multiply result: " + multiplyResult);

            transport.close();
        } catch (TTransportException e) {
            e.printStackTrace();
        } catch (TException e) {
            e.printStackTrace();
        } finally {
            if (transport != null) {
                transport.close();
            }
        }
    }

    public static void main(String[] args) {
        NonblockingClient client = new NonblockingClient();
        client.invoke();
    }
}

클라이언트는 일반 TSocket을 래핑한 TFramedTransport를 사용하게 된다. 논블록킹 서버는 클라이언트가 전송된 데이터를 구조화 하는 TFramedTransport를 사용해야 한다.

서버를 시작하고, 클라이언트를 실행하면 요청을 보낸다. 논블로킹 모드를 사용하여 이전과 동일한 결과를 볼 수 있다.

Asynchronous Mode : 비동기 모드

이번에는 Thrift 서비스를 비동기로 호출하는 클라이언트를 만들어 보겠다. 요청이 성공하게 되면, 호출되는 콜백을 미리 지정해 두는 과정이 필요하다. 비동기 모드의 클라이언트는 블록킹 모드의 서버와 통신이 되지 않는다. (실행이 되어도 빈값이 반환된다) 클라이언트를 비동기로 동작시키기 위해서는 TNonblockingSocket을 사용해야 한다. 또한 Thrift가 생성한 ArithmeticService.AsyncClient를 사용해야 한다.

package com.devkuma.thrift.tutorial;

import java.io.IOException;

import org.apache.thrift.TException;
import org.apache.thrift.async.AsyncMethodCallback;
import org.apache.thrift.async.TAsyncClientManager;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.transport.TNonblockingSocket;
import org.apache.thrift.transport.TTransportException;

public class AsyncClient {

    private void invoke() {
        try {
            ArithmeticService.AsyncClient client = new ArithmeticService.AsyncClient(new TBinaryProtocol.Factory(),
                                                                                     new TAsyncClientManager(),
                                                                                     new TNonblockingSocket("localhost", 7911));
            client.add(200, 400, new AddMethodCallback());

            client = new ArithmeticService.AsyncClient(new TBinaryProtocol.Factory(),
                                                       new TAsyncClientManager(),
                                                       new TNonblockingSocket("localhost", 7911));
            client.multiply(20, 50, new MultiplyMethodCallback());
        } catch (TTransportException e) {
            e.printStackTrace();
        } catch (TException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        AsyncClient client = new AsyncClient();
        client.invoke();
        Thread.sleep(50);
    }

    class AddMethodCallback implements AsyncMethodCallback<Long> {
        public void onComplete(Long response) {
            System.out.println("Add from server: " + response);
        }

        public void onError(Exception e) {
            System.out.println("Error : ");
            e.printStackTrace();
        }
    }

    class MultiplyMethodCallback implements AsyncMethodCallback<Long> {
        public void onComplete(Long response) {
            System.out.println("Multiply from server: " + response);
        }

        public void onError(Exception e) {
            System.out.println("Error : ");
            e.printStackTrace();
        }
    }
}

위의 코드에서 특이한 점은 두개의 비동기 수행을 위해 두개의 콜백이 정의 되어 있다. 그러고 각가 호출되는 별도의 클라이언트 인스턴스가 만들어져 있다. 이렇게 하지 않으면 비동기 모드에서는 하나의 클라이언트는 동시에 한개이상의 작업을 수행할 수 없다. 두번째 new ArithmeticService.AsyncClient(…)를 수행하지 않고 곧바로 client.multiply(…)를 수행하였다면 다음 예외 발행하고 클라이언트는 실패하게 된다.

Exception in thread "main" java.lang.IllegalStateException: Client is currently executing another method:
com.devkuma.ArithmeticService$AsyncClient$add_call

참고




최종 수정 : 2024-03-09