-->

Armeria 는 라인에서 만든 오픈소스 웹 프레임워크로 개발 생산성을 높여준다.

만든 사람의 직강을 한번 보자. Netty를 리드한 오픈소스 컨트리뷰터 이희승님이다.

https://www.youtube.com/watch?v=xMHIMZ8fNuo

이희승님 인터뷰

 

비동기를 사랑하는 오픈소스 개발자, 이희승

세 번째 인터뷰의 주인공은 오픈소스 개발자 이희승 님입니다. 비동기를 사랑하는 오픈소스 개발자, 희승 님의 이야기를 만나 보시죠. Q. 희승 님 안녕하세요. 먼저 간단하게 자기소개 부탁드

engineering.linecorp.com

 

결론적으로 말하면 gRPC, 그리고 Rest API, Thrift 등 다양한 프로토콜을 사용할 때와 분산 환경에서의 Reactive ㅅProgramming이 필요할때 Armeria가 도움이 된다.

프레임워크 레벨에서 서버별 헬스체크를 통한 써킷브레이커 작동도 가능하고 RetryingClient 등으로 재시도 전략을 세울 수도 있다. 간단히 메트릭을 수집해 모니터링을 할 수도 있다. 별다른 라이브러리를 붙이지 않고도 작동하는 것이 매우 편하다.

무엇보다 gRPC 환경에서 웹 브라우저에 Swagger 같은 것을 띄우기가 힘든데 스펙을 살펴보고 테스트 해볼 수 있도록 Docs Service를 제공하는게 제일 좋다.

간단하게 아래 프로젝트 예제를 살펴보자.

 

Intellij에서 Protocol Buffers 플러그인을 설치해준다.

 

Proto 파일 생성

gRPC에서 사용하는 Protobuf는 가이드에 따라 작성하면 된다. 

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.minggu.common";
option java_outer_classname = "ServiceProto";

package common.proto;

/**
 이벤트 응모 Apply Service 정의
 */
service ApplyService {
    rpc applyCoupon (ApplyCouponRequest) returns (ApplyCouponResponse);
}

message ApplyCouponRequest {
    string couponNo = 1;
    string userId = 2;
    string custNo = 3;
}

message ApplyCouponResponse {
    bool isSuccess = 1;
    string applyId = 2;
    string message = 3;
}

/**
 이벤트 조회용 Retrieve Service 정의
 */
service RetrieveService {
    rpc GetRetrieveDetail (RetrieveRequest) returns (RetrieveResponse);
}

message RetrieveRequest {
    string retrieve_id = 1;
}

message RetrieveResponse {
    string retrieve_name = 1;
    string retrieve_detail = 2;
}

 

gRPC 패키지 설정

common 모듈에 gRPC 설정을 하고 apply 모듈에 armeria 설정을 한다.

import com.google.protobuf.gradle.ProtobufPlugin
import com.google.protobuf.gradle.id
import java.util.*

object Version {
    const val GRPC = "3.24.0"
    const val GRPC_KOTLIN = "1.4.1"
    const val GRPC_PROTO = "1.66.0"
}

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

    // gRPC
    api("com.google.protobuf:protobuf-java-util:${Version.GRPC}")
    api("io.grpc:grpc-protobuf:${Version.GRPC_PROTO}")
    api("io.grpc:grpc-kotlin-stub:${Version.GRPC_KOTLIN}")

    // gRPC Netty
    api("io.grpc:grpc-netty-shaded:${Version.GRPC_PROTO}")
//    api("io.grpc:grpc-netty:${Version.GRPC_PROTO}")

    // Test
    testImplementation("io.grpc:grpc-testing:${Version.GRPC_PROTO}")
}

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

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:${Version.GRPC}"
    }
    plugins {
        id("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:${Version.GRPC_PROTO}"
        }
        id("grpckt") {
            artifact = "io.grpc:protoc-gen-grpc-kotlin:${Version.GRPC_KOTLIN}:jdk8@jar"
        }
    }
    generateProtoTasks {
        all().forEach {
            it.plugins {
                id("grpc")
                id("grpckt")
            }
//            it.generateDescriptorSet = true
//            it.descriptorSetOptions.includeSourceInfo = true
//            it.descriptorSetOptions.includeImports = true
                // buildDir 대신 layout.buildDirectory쓰면 에러난다.
//            it.descriptorSetOptions.path = "${buildDir}/resources/META-INF/armeria/grpc/service-name.dsc"
        }
    }
}

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

//    proto { // protoc가 컴파일할 proto message들의 위치
//         default는 "src/main/proto" 이다.
//        srcDirs '{git remote 저장소 이름}/'
//    }
}

tasks.getByName("bootJar") {
    enabled = false
}

tasks.getByName("jar") {
    enabled = true
}

gRPC로 프로젝트를 시작할 때 가장 오랜 시간이 걸리는 작업이 이 build.gradle 설정이다.. 버전별로 sensitive해서 아주 많은 빌드를 해봐야한다.. proto 파일을 가지고 kotlin과 java 파일로 generate 해주는 작업을 gradle에서 하기 때문에 protobuf 블럭을 설정해 빌드시에 파일이 생성되도록 해야한다. 또 코틀린을 사용하기 때문에 코틀린 파일로 만들어주는 설정들도 해야하고..

 

Web 서버 모듈 설정

루트의 build.gradle.kts 안에는 웹 모듈에서 사용할 Armeria 패키지를 주입시켜준다. 

object Version {
    const val ARMERIA = "1.30.0"
}

project(":apply") {
    dependencies {
        implementation(project(":common"))
        implementation(platform("com.linecorp.armeria:armeria-bom:${Version.ARMERIA}"))
        implementation("com.linecorp.armeria:armeria:${Version.ARMERIA}")
        implementation("com.linecorp.armeria:armeria-grpc:${Version.ARMERIA}")
        implementation("com.linecorp.armeria:armeria-spring-boot3-starter:${Version.ARMERIA}")
        implementation("com.linecorp.armeria:armeria-tomcat9")
        implementation("com.linecorp.armeria:armeria-grpc-kotlin")
        compileOnly("org.apache.tomcat:annotations-api:6.0.53")
    }
}

 

Armeria 서버 코드 생성

예제 소스를 참고하면 좋다.

package com.minggu.apply

import com.minggu.common.ApplyCouponRequest
import com.minggu.common.ApplyServiceGrpc
import com.minggu.common.MoaLogger
import com.minggu.santa.core.apply.service.grpc.ApplyCouponServiceImpl
import com.linecorp.armeria.server.Server
import com.linecorp.armeria.server.grpc.GrpcService
import com.linecorp.armeria.common.grpc.GrpcSerializationFormats
import com.linecorp.armeria.server.ServerBuilder
import com.linecorp.armeria.server.docs.DocService
import com.linecorp.armeria.server.docs.DocServiceFilter
import io.grpc.protobuf.services.ProtoReflectionService
import io.grpc.reflection.v1alpha.ServerReflectionGrpc
import org.slf4j.LoggerFactory


object ArmeriaServer {

    private val logger = LoggerFactory.getLogger(ArmeriaServer::class.java)

    @JvmStatic
    fun main(args: Array<String>) {
        val server = newServer(8080, 8443)

        server.closeOnJvmShutdown()

        server.start().join()
        server.activePort()?.let {
            val localAddress = it.localAddress()
            val isLocalAddress =
                localAddress.address.isAnyLocalAddress ||
                    localAddress.address.isLoopbackAddress
            logger.info(
                "Server has been started. Serving DocService at http://{}:{}/docs",
                if (isLocalAddress) "127.0.0.1" else localAddress.hostString,
                localAddress.port,
            )
        }
    }

    private fun newServer(
        httpPort: Int,
        httpsPort: Int,
        useBlockingTaskExecutor: Boolean = false,
    ): Server {
        val exampleRequest: ApplyCouponRequest = ApplyCouponRequest.newBuilder().setCouponNo("couponNo123124").build()
        val grpcService =
            GrpcService.builder()
                .addService(ApplyCouponServiceImpl())
                // See https://github.com/grpc/grpc-java/blob/master/documentation/server-reflection-tutorial.md
                .addService(ProtoReflectionService.newInstance())
                .supportedSerializationFormats(GrpcSerializationFormats.values())
                .enableUnframedRequests(true)
                // You can set useBlockingTaskExecutor(true) in order to execute all gRPC
                // methods in the blockingTaskExecutor thread pool.
                .useBlockingTaskExecutor(useBlockingTaskExecutor)
                .build()
        return Server.builder()
            .http(httpPort)
            .https(httpsPort)
            .tlsSelfSigned()
            .service(grpcService) // You can access the documentation service at http://127.0.0.1:8080/docs.
            // See https://armeria.dev/docs/server-docservice for more information.
            .serviceUnder(
                "/docs",
                DocService.builder()
                    .exampleRequests(
                        ApplyServiceGrpc.SERVICE_NAME,
                        "Hello",
                        exampleRequest,
                    )
                    .exampleRequests(
                        ApplyServiceGrpc.SERVICE_NAME,
                        "LazyHello",
                        exampleRequest,
                    )
                    .exampleRequests(
                        ApplyServiceGrpc.SERVICE_NAME,
                        "BlockingHello",
                        exampleRequest,
                    )
                    .exclude(
                        DocServiceFilter.ofServiceName(
                            ServerReflectionGrpc.SERVICE_NAME,
                        ),
                    )
                    .build(),
            )
            .build()
    }
}

 

실행

'Back-end' 카테고리의 다른 글

[Redis] 나야 조회수  (3) 2024.10.15
[Redis] Redis 데이터 저장 근데 protobuf를 곁들인  (3) 2024.10.14
[gRPC] gRPC란  (3) 2024.10.02
[Spring/Thymeleaf] option 태그에 enum 동적으로 넣기  (0) 2024.08.10
[Redis] Cache 전략  (0) 2024.07.29

+ Recent posts