Notice
Recent Posts
Recent Comments
Link
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Archives
Today
Total
관리 메뉴

디지안의 개발일지

[실습] Kotlin + Armeria + gRPC 사용기 - 기본편 본문

Kotlin

[실습] Kotlin + Armeria + gRPC 사용기 - 기본편

안덕기 2021. 8. 12. 20:59

들어가기 전에

 Java를 주로 사용하다가 주언어를 Kotlin으로 변경한지 이제 2년이 다 되어 간다. 그 동안 공부했던 것에 대한 정리를 하고 싶다는 생각이 문득 들어 하나씩 글을 작성해보려고 한다. 오늘은 그 첫번째 글로 Kotlin + Armeria + gRPC 사용했던 것에 대해서 정리한다.

 

3가지는 무슨 조합인가?

 별거 없다. 프로그래밍 언어로 Kotlin으로 쓰고 웹 프레임워크로 Armeria를 사용하고 프로토콜로 gRPC를 사용했다.

 

Kotlin

 구글이 안드로이드 공식 언어로 선정한 이후 나름 핫한 언어라고 생각한다. 실제로 써본 느낌으로는 Java 보다 문법이 간단하고 확장 함수, 연산자 오버라이딩, dsl등 기존 자바에서 사용할 수 없는 문법들을 사용할 수 있다는 매력적인 포인트들이 있다고 생각이 든다. 이제는 Java를 쓰기보단 Kotlin을 쓰고 싶은 입장..

 다만, Hibernate를 사용할 때 Kotlin이 가지고 있는 기능들과 Hibernate가 가지고 있는 기능들에 대한 충돌 때문에 처음 적용 시키는 것이나 적응하기가 생각보다 까다롭다. 아직도 정확하게 어떻게 써야 좋을까에 대해서는 의문인 상황. 하지만, 그만큼 무언가 더 개척할 수 있다는 것이 아닐까?

 

Armeria

 Line에서 개발한 MSA 프레임워크다. 요즘 대세인 MSA을 하기 위한 여러가지 기능들을 제공하고 더 많이 제공해주려고 노력하는 중이다.

사용해본 것 중에 편리했던 것은 하나의 포트에서 여러가지 프로토콜(http, gRPC, Thrift)을 사용할 수 있고 gRPC를 쓰는 입장에서 문서화를 자동으로 해주는 것이 매우 편리하다. 앞으로 더 좋은 기능들이 추가될 것으로 기대하는 프레임워크다.

 

gRPC

 구글에서 만든 Remote Procedure Call. ProtocallBuffer의 특징 덕분에 HTTP API 보다 성능적으로 우수(gRPC vs REST)하고 인터페이스 자체를 코드로 관리할 수 있다라는 것이 장점이라고 생각한다. 그냥 API가 아니라 RPC라고 생각하면.. 아니 더 가서 그냥 객체에서 함수를 호출한다고 생각하고 사용하면 코드 작성하는 것이 더더욱 편해지는 느낌이다.

 

프로젝트 만들기

인텔리제이에서 새 프로젝트를 누르고 다음과 같이 선택한다.

프로젝트 이름을 설정한다.

Protocol buffer을 컴파일하기 위한 설정

 gRPC를 명세하기 위해서는 Protocol buffer(이하 protobuf라고 표현)라는 언어를 사용한다. protobuf로 작성된 인터페이스는 자신이 사용할 언어로 컴파일을 해야한다. 이 때 사용하는 컴파일러는 protoc라고 한다. kotlin에서는 이에 대한 설정을 gradle에 작성하면 된다. 

(최근 문서를 보면 Bazel로 컴파일하도록 되어 있는데 공부를 안해서 모른다.)

 

Protocol buffer로 선언해놓은 코드들을 별도로 컴파일하는 경우가 생각보다 많기 때문에 이를 쉽게 관리하기 위해서는 프로젝트를 멀티 프로젝트로 관리하는 방법이 편리했다. 그래서 프로젝트를 멀티 모듈로 구성한다.

 

프로젝트 바로 아래의 build.gradle.kts를 다음과 같이 설정한다.

plugins {
    kotlin("jvm") version "1.5.10"
}

allprojects {
    group = "me.dgahn"
    version = "0.0.1"

    repositories {
        mavenCentral()
    }
}

그리고 다음과 같이 모듈을 추가한다.

플러그인은 다음과 같이 별도로 추가하지 않는다.

모듈의 이름은 protobuf로 하고 만든다.

모듈 생성이 완료되면 프로젝트 바로 아래의 build.gradle.kts의 내용을 다음과 같이 바꾼다.

plugins {
    kotlin("jvm") version "1.5.10"
    id("com.google.protobuf") version "0.8.15" // 추가
}

allprojects {
    group = "me.dgahn"
    version = "0.0.1"

    apply(plugin = "kotlin") // 추가

    repositories {
        mavenCentral()
    }
}

protobuf 모듈 아래의 build.gradle.kts에 다음과 같이 작성한다.

import com.google.protobuf.gradle.*

apply(plugin = "com.google.protobuf")

configurations.forEach {
    if (it.name.toLowerCase().contains("proto")) {
        it.attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage::class.java, "java-runtime"))
    }
}

dependencies {
    compileOnly("javax.annotation:javax.annotation-api:1.3.2")

    api("com.google.protobuf:protobuf-java-util:3.14.0")
    api("io.grpc:grpc-kotlin-stub:1.0.0")
    api("io.grpc:grpc-protobuf:1.34.0")
    api("io.grpc:grpc-netty-shaded:1.34.0")
}

protobuf {
    generatedFilesBaseDir = "$projectDir/build/generated/source"
    protoc {
        artifact = "com.google.protobuf:protoc:3.14.0"
    }
    plugins {
        id("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:1.34.0"
        }
        id("grpckt") {
            artifact = "io.grpc:protoc-gen-grpc-kotlin:1.0.0:jdk7@jar"
        }
    }
    generateProtoTasks {
        ofSourceSet("main").forEach {
            it.plugins {
                id("grpc")
                id("grpckt")
            }
            it.generateDescriptorSet = true
            it.descriptorSetOptions.includeSourceInfo = true
            it.descriptorSetOptions.includeImports = true
            it.descriptorSetOptions.path = "$buildDir/resources/META-INF/armeria/grpc/service-name.dsc"
        }

    }
}

sourceSets {
    main {
        java.srcDir("build/generated/source/main/grpckt")
        java.srcDir("build/generated/source/main/grpc")
        java.srcDir("build/generated/source/main/java")
    }
}

그리고 인텔리제이에서 제공하는 protobuf 플러그인을 설치한다.

이제 실제로 인터페이스를 정의하면 된다. gRPC 서비스를 다음과 같이 정의한다.

syntax = "proto3";

package me.dgahn.account.v1;

import "me/dgahn/account/v1/SignUpV1.proto";

service AccountRouter {
  rpc signUpV1(SignUpRequestV1) returns (SignUpResponseV1) { }
}

그리고 메시지를 아래와 같이 정의한다.

syntax = "proto3";

package me.dgahn.account.v1;

option java_multiple_files = true;
option java_outer_classname = "SignUpV1Proto";

message SignUpRequestV1 {
  string id = 1;
  string name = 2;
  string password = 3;
}

message SignUpResponseV1 {
  string id = 1;
}

우측에서 protobuf 모듈의 빌드를 진행하면 좌측과 같이 컴파일된 파일을 확인할 수 있다.

Armeria 서버 구동하기

모듈을 새로 추가한다. 새로 추가한 모듈의 build.gradle.kts에 의존성을 추가한다.

dependencies {
    implementation(project(":protobuf"))
    implementation("com.linecorp.armeria:armeria:1.9.2")
    implementation("com.linecorp.armeria:armeria-grpc:1.9.2")
    implementation("io.github.microutils:kotlin-logging:1.12.5")
    implementation("ch.qos.logback:logback-classic:1.2.3")
}

armeria 코드를 추가한다.

package me.dgahn

import com.linecorp.armeria.common.grpc.GrpcSerializationFormats
import com.linecorp.armeria.server.Server
import com.linecorp.armeria.server.ServerBuilder
import com.linecorp.armeria.server.docs.DocService
import com.linecorp.armeria.server.grpc.GrpcService
import mu.KotlinLogging

private val logger = KotlinLogging.logger { }

fun main() {
    val server = newServer()
    server.startServer()
}

private fun Server.startServer() {
    Runtime.getRuntime().addShutdownHook(Thread {
        stop().join()
        logger.info("Server has been stopped.")
    })
    start().join()
    logger.info(
        "Server has been started. Serving DocService at http://127.0.0.1:{}/docs",
        activeLocalPort()
    )
}

private fun newServer(httpPort: Int = 8080, httpsPort: Int = 8433): Server = Server.builder()
    .setHttp(httpPort, httpsPort)
    .setGrpcService()
    .build()

private fun ServerBuilder.setHttp(httpPort: Int, httpsPort: Int) = this.apply {
    http(httpPort)
    https(httpsPort)
    tlsSelfSigned()
}

private fun ServerBuilder.setGrpcService() = this.apply {
    val grpcService = createGrpcService()
    service(grpcService)
    service("/", grpcService)
    serviceUnder("/docs", DocService.builder().build())
}

private fun createGrpcService(): GrpcService = GrpcService.builder()
//        .addService(HelloServiceImpl())
    .supportedSerializationFormats(GrpcSerializationFormats.values())
    .enableUnframedRequests(true)
    .build()

위 코드로 실행하면 다음과 같은 로그와 웹페이지를 확인할 수 있다.

Armeria에 gRPC 서비스 등록하기

Armeria에 gRPC 서비스를 등록하기 위해서는 위에서 protobuf 파일을 컴파일한 파일을 기반으로 서버 코드를 구성해야한다. 

컴파일 한 파일을 살펴보면 AccountRouterGrpcKt Object안에 inner 클래스로 AccountRouterCoroutineImplBase 클래스가 선언 되어 있다. 인터페이스가 아닌 클래스로 선언된 정확한 이유는 모르겠지만 사용자가 구현하지 않는 rpc에 대해서 구현되지 않았다는 것을gRPC Status로 제공해주기 위해서가 아닐까 싶다.

 

각설하고 아래와 같이 컴파일한 객체를 상속 받아서 구현한다.

class AccountRouteService: AccountRouterGrpcKt.AccountRouterCoroutineImplBase() {

    override suspend fun signUpV1(request: SignUpRequestV1): SignUpResponseV1 = try {
        AccountService.signUp(request.toEntity()).toProto()
    } catch (e: Exception) {
        logger.error { e.stackTraceToString() }
        throw StatusException(Status.UNKNOWN.withDescription(e.stackTraceToString()))
    }

}
package me.dgahn.account

object AccountService {
    fun signUp(account: Account) = account
}
package me.dgahn.account

data class Account(
    val id: String,
    val password: String,
    val name: String
)

protobuf의 객체에 우리가 함수를 직접 추가할 수는 없기 때문에 아래와 같이 Kotlin 확장함수를 통해 내가 원하는 함수를 추가할 수도 있다.

package me.dgahn.account

import me.dgahn.account.v1.SignUpRequestV1
import me.dgahn.account.v1.SignUpResponseV1

fun SignUpRequestV1.toEntity() = Account(
    id = id,
    password = password,
    name = name
)

fun Account.toProto(): SignUpResponseV1 = SignUpResponseV1.newBuilder()
    .setId(id)
    .build()

서버를 구동하는 곳에서 다음과 같은 코드를 추가한다.

private fun createGrpcService(): GrpcService = GrpcService.builder()
    .addService(AccountRouteService()) // 추가
    .supportedSerializationFormats(GrpcSerializationFormats.values())
    .enableUnframedRequests(true)
    .build()

서버를 껏다가 키고 다시 http://localhost:8080/docs로 들어가면 다음과 같은 화면이 나온다.

Armeria에서 제공하는 강력한 docs 기능을 통해서 API를 호출해보자.

 

마무리

Kotlin + Armeria + gRPC를 통해서 간단한 웹을 만들어 보았다. 

git repo: https://github.com/dgahn/kotlin-armeria