Thrift 뽀개기 | Cracking Thrift
보통 서버 통신을 할 때 http 를 떠올리기 쉽다.
좀 더 가볍고, 다른 언어간의 호환이 편했으면 하는 이슈와, 당시 facebook에서 만들어서 인기가 많았다는 이유로
thrift를 서버간의 통신에서 사용하기도한다.

1. Thrift 정의하는 방법
Apache Thrift - Home
The Apache Thrift software framework, for scalable cross-language services development, combines a software stack with a code generation engine to build services that work efficiently and seamlessly between C++, Java, Python, PHP, Ruby, Erlang, Perl, Haske
thrift.apache.org
먼저 thrift사용을 위해 thrift를 설치한다
$ brew update
$ brew install thrift
thrift 파일을 작성한다 (IDL). 해당 파일에 작성된 대로 해당 서버에서 thrift로 통신할 수 있는 요청 응답 포맷이 정의가 되게 된다.
IDL 작성법과 예시는 공식문서에 나와있는 example이 가장 좋은 것 같다.
https://thrift.apache.org/docs/types
Apache Thrift - Thrift Type system
Thrift Types The Thrift type system is intended to allow programmers to use native types as much as possible, no matter what programming language they are working in. This information is based on, and supersedes, the information in the Thrift Whitepaper. T
thrift.apache.org
기본 타입리스트
- 기본타입 : bool, byte, i16, i32, i64, double, string (숫자는 전부 signed 타입이다.)
- 특별 타입 : binary - 인코딩되지 않은 바이트 시퀀스
- struct : oop 언어의 클래스와 동일하나 상속은 없다.
- 컨테이너 : list, set, map<type1, type2>
위에서 말한 타입을 기반으로 https://thrift.apache.org/docs/idl 을 추가로 정의할 수 있다.
서비스
위에서 정의한 type들을 사용하여 정의된다. 인터페이스를 정의하는 것과 유사하고, 이렇게 정의된 service를 thrift 컴파일러로 컴파일을 하게되면, thrift가 정의한 서비스대로 요청 응답을 할 수 있는 클라이언트 코드 및 서버 코드를 작성해준다.
네임스페이스
작성한 thrift IDL 을 컴파일 했을 때 어떤언어로 내보낼지 설정한다. 문서를 보면 내보내기 가능한 언어 리스트가 적혀져있다. 다양한 언어로 thrift가 생성이 가능하기 때문에, 서로 다른 언어로 짜여진 서버간의 통신에 사용할 때 이점이 있다고 보는 것 같다.
사용법 예시를 위해 thrift DML을 아래와 같이 작성을 해보았다.
namespace java com.jyami.java.thrift.service
namespace py com.jyami.python.thrift.service
struct CommonResponse {
1:required i32 status,
2:optional binary bsonBody
}
struct RequestWithFlag {
1:required binary bsonData,
2:required bool flag
}
service PostService {
void ping()
CommonResponse read(1:RequestWithFlag request),
CommonResponse save(1:binary bsonData),
CommonResponse remove(1:binary bsonData)
}
2. thrift 컴파일로 코드 생성
thrift --gen <language> <Thrift filename>

위 명령어를 java로 실행했을 때 ( thrift --gen java app.thrift ) 위와 같은 java 파일이 자동생성되었다.
namespace에 python 도 짜두었으니, python 도 마찬가지로 생성이 가능하다.

3. Client, Server 코드 생성
https://thrift.apache.org/tutorial/java.html
Apache Thrift - Java
Java Tutorial Introduction All Apache Thrift tutorials require that you have: The Apache Thrift Compiler and Libraries, see Download and Building from Source for more details. Generated the tutorial.thrift and shared.thrift files: thrift -r --gen java tuto
thrift.apache.org
사실 공식문서에 너무 잘되어있다. java로 Client, Server 전부 작성해보았다.
결국은 위에서 thrift gen으로 생성한 java 파일안의 클래스와 메서드들을 사용하는게 포인트이며,
Thrift 통신을 하기위한 기본적인 구현체인 TSocket, Ttransport 등등은 아래와 같이 thrift 라이브러리를 사용하면 받을 수 있다.
implementation("org.apache.thrift:libthrift:0.18.0")
3-0. 세팅

편의를 위해 client와 server를 한 디렉토리에 두었다.
하지만 핵심은 thrift 폴더 아래에 있는 app.thrift에서 생성된 java 코드 안에 있는 각종 클래스나 메서드와 같은 구현체를
비즈니스 로직이 있는 실제 클라이언트 혹은 서버의 main 코드에서 직접 사용하여 thrift로 통신을 할 수 있다는 것이다.
intellij gradle project 기준에서, 위와같이 main폴더와 thrift폴더 사이의 코드들이 서로 import 해서 사용하고, 이를 개발환경에서도 intellij 상에서 indexing을 진행한 후 사용하기 위해, 여러가지 세팅을 해주었다.
## build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.7.10"
}
group = "org.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
testImplementation(kotlin("test"))
implementation("org.apache.thrift:libthrift:0.18.0")
implementation("javax.annotation:javax.annotation-api:1.3.2")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.1")
implementation("de.undercouch:bson4jackson:2.13.1")
implementation("org.slf4j:slf4j-api:1.7.25")
}
tasks.test {
useJUnitPlatform()
}
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
sourceSets {
this.getByName("main"){
this.java.srcDir("src/thrift/gen-java")
}
}

intellij 로 실행할 때 thrift 폴더도 sourceDirectory로 인지하고 해당 폴더 안에 있는 자바 코드를 사용하기 위해 build.gradle에 sourceSets 를 설정해주었다. 또한 intellij 상에서 해당 폴더를 실제 sources로 인식할 수 있게 project structure(command + ;)에서 modules 안에 source로 잘 들어갔는지도 확인해준다.
3-1. Client 코드 작성
thrift gen으로 생성된 파일들의 실제 내용을 보면 알겠지만 RequestWithFlag, CommonRespones와 같은 파일은 요청 응답을 위해 정의한 구조체의 클래스만 들어있다. 즉 app.thrift에서 struct로 정의한 객체들의 파일이다.
실제로 서버로 호출을 하기위한 주된 역할을 하는 java파일은 PostService이며, Client와 Server 각각의 구현체 작성을 위한 코드는 전부 여기있다.
가이드에 따라 Client 코드를 짜고, 내 나름대로 리팩토링을 한다.
package com.jyami.sample.client
import com.jyami.java.thrift.service.CommonResponse
import com.jyami.java.thrift.service.PostService
import com.jyami.java.thrift.service.RequestWithFlag
import org.apache.thrift.protocol.TBinaryProtocol
import org.apache.thrift.transport.TSocket
import org.apache.thrift.transport.TTransport
import java.nio.ByteBuffer
fun main() {
val sampleByte = ByteBuffer.wrap("hello".toByteArray())
connectWithServer { transport ->
val client = makeClient(transport)
client.ping()
val readResponse: CommonResponse = client.read(RequestWithFlag(sampleByte, false))
println("read response $readResponse")
val saveResponse: CommonResponse = client.save(sampleByte)
println("save response $saveResponse")
val removeResponse: CommonResponse = client.remove(sampleByte)
println("remove response $removeResponse")
}
}
fun connectWithServer(process: (transport: TTransport) -> Unit) {
val transport = TSocket("localhost", 9090)
transport.open()
process.invoke(transport)
transport.close()
}
fun makeClient(transport: TTransport): PostService.Client = PostService.Client(TBinaryProtocol(transport))
build.gradle에 넣어준 thrift 라이브러리에 들어있는 것들 TBinaryProtocol, TSocket, TTransport 와 같은 객체들은 전부
thrift를 이용한 socket통신을 하기위해 apache에서 제공하는 추상화된 코드들이다. 해당 객체들을 사용하여 서버와 연결지을 커넥션을 생성하고, thrift.gen으로 만들어진 java 파일들 CommonResponse, PostService, RequestWithFlag를 사용하면 쉽게 클라이언트 코드를 작성할 수 있다.
connectWithServer(), makeClient() 메서드들 각각을 각각 thrift에서 실제 socket 커넥션을 맺는 역할, 호출을 위한 client 객체를 만드는 역할로 각각 코드를 작성해주었다.
connectWithServer로 서버와 connection을 맺고, 해당 서버에 요청을 보낼 내용들은 makeClient로 생성된 PostService.Client객체에 정의된 메서드들을 사용한다. 해당 메서드들은 app.thrift에서 정의한 service들의 메서드임을 알 수 있고, 서버에서 해당 service들에 대한 비즈니스 로직을 채워서 client에 응답을 주면 client는 해당하는 응답에 따라 클라이언트 자체 뷰를 그리기도 하고 print를 하기도하는 등 클라이언트의 요구사항을 채워준다.
3-2. Server 코드 작성
package com.jyami.sample.server
import com.jyami.java.thrift.service.PostService
import org.apache.thrift.server.TServer.Args
import org.apache.thrift.server.TSimpleServer
import org.apache.thrift.transport.TServerSocket
fun main(){
val serverTransport = TServerSocket(9090)
val postServiceImpl = PostServiceImpl()
val server = TSimpleServer(Args(serverTransport).processor(PostService.Processor(postServiceImpl)))
println("Starting the simple server")
server.serve()
}
서버 코드도 클라이언트와 마찬가지로 thrift 라이브러리를 사용하여 ServerSocket을 열어준다. TServerSocket, TSimpleServer(http2와 같은 secure 연결이 되어있지 않은 서버) 를 활용하여 thrift 통신을 하는 서버를 구동시킨다.
이때 해당 서버에 요청이 들어오면 어떤 처리를 할 것 인지를 지정하는 것을 .processor() 메서드로 지정하는데.
이때 처리할 Processor 구현체는 아까 app.thrift에서 생성한 PostService안에 생성이 되어있다.
package com.jyami.sample.server
import com.jyami.java.thrift.service.CommonResponse
import com.jyami.java.thrift.service.PostService
import com.jyami.java.thrift.service.RequestWithFlag
import java.nio.ByteBuffer
class PostServiceImpl : PostService.Iface{
override fun ping() {
println("ping")
}
override fun read(request: RequestWithFlag): CommonResponse {
println("server : read request")
return CommonResponse()
}
override fun save(bsonData: ByteBuffer?): CommonResponse {
println("server : save request")
return CommonResponse()
}
override fun remove(bsonData: ByteBuffer?): CommonResponse {
println("server : remove request")
return CommonResponse()
}
}
실제 해당 서버가 어떤 요청응답을 받는지에 대한 인터페이스는 PostService.Iface 를 이용하면 되기 때문에, 실제 서버에 작성한 프로토콜 명세에 대한 요청을 어떻게 처리할지만 정해주면 된다.
PostService.Iface를 구현한 구현체에서 각각의 프로토콜에 대한 비즈니스 로직을 길게 넣어주고 thrift파일에 정의한 프로토콜 명세에 맞게 요청값 응답값을 잘 넣어준다.
PostService.Iface의 구현체 즉, 각 프로토콜의 동작이 정의된 비즈니스 로직을 담은 PostServiceImpl을 processor() 메서드 안에 넣어주면, 해당 thrift 서버의 동작을 정의할 수 있는 것이다.
3-3. 실행
위 코드대로 실제로 서버를 구동시키고 클라이언트가 2개 있다는 가정하에 Client 코드를 2번 가동시키면
서버 로그

클라이언트로부터 ping, read, save, remove, ping, read, save, remove 요청을 받아 처리하였다.
클라이언트 로그

서버에 read, save, remove 요청을 보내고 해당하는 응답을 받아 해당 응답을 직접 출력하였다.
3-4. 추가 잡담 - 코드 의도
thrift.app 파일에 int, long, string과 같은 다양한 타입을 정의할 수 있지만, struct에 굳이 binary를 사용한 이유는 thrift로 생성된 객체를 직접 바로 사용하고 싶지 않아서이다.
만약 thrift로 생성된 요청 응답 객체를 직접 사용한다면, 요구사항에 따라 request에 param이 하나가 추가가 되야할 경우 그때마다 thrift.app 파일을 변경하고 그에따라 thrift gen을 이용해서 java 파일을 생성하고 생성된 java 파일에서 변경된 클래스명이나 파라이터 명에 따라 main 코드를 고쳐야한다.
그러나 내가 원하는 요청응답 객체가 있을 때 (Post(title:String, username: String)) 위 객체를 jackson을 이용하여 byinary로 파싱하여 담으면 Post 객체 자체에 대한 요구사항을 훨씬 유연하게 처리할 수 있으면서, 가장 바깥에서 통신하는 thrift 객체는 body가 binary이기 때문에 거의 변화가 없어 위에서 말한 thrift 파일 변경 > thrift gen으로 파일생성 > 이에따른 main 코드 변경이 잦지 않게 되었기 때문에 위와 같은 코드 예시를 선택하였다.
샘플코드라서 오히려 간단해서 구분이 안갈 수 있지만, 해당 샘플 코드를 기반으로 나의 의도를 담아 비즈니스로직을 구현한 예시 소스코드가 보고싶다면 아래 링크에 넣어놨다. thrift 뽀개기 끝 ~_~
https://github.com/jyami-kim/Jyami-Java-Lab/tree/master/thrift-sample
GitHub - jyami-kim/Jyami-Java-Lab: 💻 Jyami의 Spring boot 및 Java 실험소 💻
💻 Jyami의 Spring boot 및 Java 실험소 💻. Contribute to jyami-kim/Jyami-Java-Lab development by creating an account on GitHub.
github.com
When it comes to server communication, HTTP is usually the first thing that comes to mind.
However, due to the need for something lighter and better cross-language compatibility, and partly because Facebook created it and it gained a lot of popularity,
Thrift is also used for server-to-server communication.

1. How to Define Thrift
Apache Thrift - Home
The Apache Thrift software framework, for scalable cross-language services development, combines a software stack with a code generation engine to build services that work efficiently and seamlessly between C++, Java, Python, PHP, Ruby, Erlang, Perl, Haske
thrift.apache.org
First, install Thrift to use it.
$ brew update
$ brew install thrift
Write a Thrift file (IDL). Based on what's written in this file, the request and response formats for Thrift communication on that server will be defined.
For IDL syntax and examples, the official documentation's examples are probably the best resource.
https://thrift.apache.org/docs/types
Apache Thrift - Thrift Type system
Thrift Types The Thrift type system is intended to allow programmers to use native types as much as possible, no matter what programming language they are working in. This information is based on, and supersedes, the information in the Thrift Whitepaper. T
thrift.apache.org
Basic Type List
- Basic types: bool, byte, i16, i32, i64, double, string (all numeric types are signed.)
- Special type: binary - a sequence of unencoded bytes
- struct: Same as a class in OOP languages, but without inheritance.
- Containers: list, set, map<type1, type2>
Based on the types mentioned above, you can additionally define elements described at https://thrift.apache.org/docs/idl.
Services
Services are defined using the types described above. It's similar to defining an interface. Once a defined service is compiled with the Thrift compiler, Thrift generates both client and server code that can handle requests and responses according to the defined service.
Namespaces
This specifies which language the compiled Thrift IDL should be exported to. The documentation lists all the supported export languages. Since Thrift can generate code for a wide variety of languages, it seems to be particularly advantageous when used for communication between servers written in different languages.
As an example, I wrote a Thrift IDL file like below.
namespace java com.jyami.java.thrift.service
namespace py com.jyami.python.thrift.service
struct CommonResponse {
1:required i32 status,
2:optional binary bsonBody
}
struct RequestWithFlag {
1:required binary bsonData,
2:required bool flag
}
service PostService {
void ping()
CommonResponse read(1:RequestWithFlag request),
CommonResponse save(1:binary bsonData),
CommonResponse remove(1:binary bsonData)
}
2. Generating Code with Thrift Compiler
thrift --gen <language> <Thrift filename>

When running the above command for Java ( thrift --gen java app.thrift ), the Java files shown above were auto-generated.
Since we also set up Python in the namespace, Python files can be generated in the same way.

3. Creating Client and Server Code
https://thrift.apache.org/tutorial/java.html
Apache Thrift - Java
Java Tutorial Introduction All Apache Thrift tutorials require that you have: The Apache Thrift Compiler and Libraries, see Download and Building from Source for more details. Generated the tutorial.thrift and shared.thrift files: thrift -r --gen java tuto
thrift.apache.org
Honestly, the official documentation is really well done. I wrote both Client and Server in Java.
The key point is using the classes and methods inside the Java files generated by thrift gen,
and the basic implementations for Thrift communication like TSocket, TTransport, etc. can be obtained by using the Thrift library as shown below.
implementation("org.apache.thrift:libthrift:0.18.0")
3-0. Setup

For convenience, I placed the client and server in the same directory.
But the key takeaway is that the various classes and methods — the implementations generated from app.thrift inside the thrift folder —
can be directly used in the actual client or server main code where the business logic resides, enabling Thrift communication.
In an IntelliJ Gradle project setup, I configured several settings so that the code between the main folder and the thrift folder can import and use each other, and also so that IntelliJ can index and recognize them properly in the development environment. I checked in Project Structure (Command + ;) under Modules to make sure the thrift folder was properly registered as a source.
## build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.7.10"
}
group = "org.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
testImplementation(kotlin("test"))
implementation("org.apache.thrift:libthrift:0.18.0")
implementation("javax.annotation:javax.annotation-api:1.3.2")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.1")
implementation("de.undercouch:bson4jackson:2.13.1")
implementation("org.slf4j:slf4j-api:1.7.25")
}
tasks.test {
useJUnitPlatform()
}
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
sourceSets {
this.getByName("main"){
this.java.srcDir("src/thrift/gen-java")
}
}

I configured sourceSets in build.gradle so that IntelliJ recognizes the thrift folder as a sourceDirectory and can use the Java code inside it when running. I also verified in Project Structure (Command + ;) under Modules that the folder was properly registered as a source.
3-1. Writing the Client Code
If you look at the actual contents of the files generated by thrift gen, you'll see that files like RequestWithFlag and CommonResponse only contain the struct classes defined for requests and responses. In other words, they are the files for the objects defined as structs in app.thrift.
The main Java file responsible for making actual server calls is PostService, and all the code needed to write both Client and Server implementations is in there.
I wrote the Client code following the guide, and then refactored it in my own way.
package com.jyami.sample.client
import com.jyami.java.thrift.service.CommonResponse
import com.jyami.java.thrift.service.PostService
import com.jyami.java.thrift.service.RequestWithFlag
import org.apache.thrift.protocol.TBinaryProtocol
import org.apache.thrift.transport.TSocket
import org.apache.thrift.transport.TTransport
import java.nio.ByteBuffer
fun main() {
val sampleByte = ByteBuffer.wrap("hello".toByteArray())
connectWithServer { transport ->
val client = makeClient(transport)
client.ping()
val readResponse: CommonResponse = client.read(RequestWithFlag(sampleByte, false))
println("read response $readResponse")
val saveResponse: CommonResponse = client.save(sampleByte)
println("save response $saveResponse")
val removeResponse: CommonResponse = client.remove(sampleByte)
println("remove response $removeResponse")
}
}
fun connectWithServer(process: (transport: TTransport) -> Unit) {
val transport = TSocket("localhost", 9090)
transport.open()
process.invoke(transport)
transport.close()
}
fun makeClient(transport: TTransport): PostService.Client = PostService.Client(TBinaryProtocol(transport))
Objects like TBinaryProtocol, TSocket, and TTransport that come from the Thrift library added in build.gradle are all
abstracted code provided by Apache for socket communication using Thrift. You create a connection to the server using these objects, and then use the Java files generated by thrift gen — CommonResponse, PostService, RequestWithFlag — to easily write client code.
I wrote the connectWithServer() and makeClient() methods to handle establishing the actual socket connection with Thrift and creating the client object for making calls, respectively.
connectWithServer establishes the connection with the server, and the actual requests sent to the server use the methods defined on the PostService.Client object created by makeClient. You can see that these methods correspond to the service methods defined in app.thrift. When the server fills in the business logic for those services and sends responses back to the client, the client can then render its own views, print output, or fulfill whatever client-side requirements it has.
3-2. Writing the Server Code
package com.jyami.sample.server
import com.jyami.java.thrift.service.PostService
import org.apache.thrift.server.TServer.Args
import org.apache.thrift.server.TSimpleServer
import org.apache.thrift.transport.TServerSocket
fun main(){
val serverTransport = TServerSocket(9090)
val postServiceImpl = PostServiceImpl()
val server = TSimpleServer(Args(serverTransport).processor(PostService.Processor(postServiceImpl)))
println("Starting the simple server")
server.serve()
}
Just like the client, the server code also uses the Thrift library to open a ServerSocket. It uses TServerSocket and TSimpleServer (a server without secure connections like HTTP/2) to start a server that communicates via Thrift.
To specify what processing should happen when a request comes into the server, you use the .processor() method.
The Processor implementation used here is already generated inside PostService, which was created from app.thrift.
package com.jyami.sample.server
import com.jyami.java.thrift.service.CommonResponse
import com.jyami.java.thrift.service.PostService
import com.jyami.java.thrift.service.RequestWithFlag
import java.nio.ByteBuffer
class PostServiceImpl : PostService.Iface{
override fun ping() {
println("ping")
}
override fun read(request: RequestWithFlag): CommonResponse {
println("server : read request")
return CommonResponse()
}
override fun save(bsonData: ByteBuffer?): CommonResponse {
println("server : save request")
return CommonResponse()
}
override fun remove(bsonData: ByteBuffer?): CommonResponse {
println("server : remove request")
return CommonResponse()
}
}
Since the interface for what requests and responses the server handles can be obtained through PostService.Iface, all you need to do is define how to process the requests for the protocol specs written on the server.
In the implementation of PostService.Iface, you add the business logic for each protocol, and make sure the request and response values match the protocol specs defined in the Thrift file.
The implementation of PostService.Iface — that is, the business logic where each protocol's behavior is defined — PostServiceImpl, is passed into the processor() method, which defines the behavior of the Thrift server.
3-3. Running It
Following the code above, after actually starting the server and running the Client code twice (assuming there are 2 clients):
Server Log

It received and processed ping, read, save, remove, ping, read, save, remove requests from the clients.
Client Log

It sent read, save, and remove requests to the server, received the corresponding responses, and printed them out.
3-4. Extra Thoughts - Code Intent
While you can define various types like int, long, string in the thrift.app file, the reason I deliberately used binary in the struct is that I didn't want to directly use the objects generated by Thrift.
If you directly use the request/response objects generated by Thrift, every time a new param needs to be added to a request due to changing requirements, you'd have to modify the thrift.app file, regenerate the Java files with thrift gen, and then update the main code to reflect any changed class names or parameter names.
However, when I have my own desired request/response object (e.g., Post(title:String, username: String)), I can parse it into binary using Jackson and pass it along. This way, I can handle changes to the Post object itself much more flexibly, and since the outermost Thrift object's body is binary, it rarely changes. This means the cycle of modifying the Thrift file → regenerating files with thrift gen → updating main code accordingly happens much less frequently. That's why I chose this code approach.
Since it's sample code, it might actually be too simple to see the distinction clearly, but if you'd like to see the source code where I implemented business logic with my intent based on this sample code, I've put it at the link below. And that's a wrap on cracking Thrift! ~_~
https://github.com/jyami-kim/Jyami-Java-Lab/tree/master/thrift-sample
GitHub - jyami-kim/Jyami-Java-Lab: 💻 Jyami의 Spring boot 및 Java 실험소 💻
💻 Jyami의 Spring boot 및 Java 실험소 💻. Contribute to jyami-kim/Jyami-Java-Lab development by creating an account on GitHub.
github.com
'Develop > Web' 카테고리의 다른 글
| Image Styling with Web Components - 웹 컴포넌트를 사용한 이미지 스타일링 | Image Styling with Web Components (3) | 2019.12.22 |
|---|---|
| web & server - DSC Ewha 세션 | web & server - DSC Ewha Session (0) | 2019.10.15 |
댓글
Comments