본문 바로가기

Develop/Web

Thrift 뽀개기

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

 

1. Thrift 정의하는 방법

https://thrift.apache.org/

 

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