프로그래밍/Kotlin

Rest API와 Grpc API 서버를 하나로 서비스 할 수 있다고?!

seungdols 2024. 8. 28. 23:25

Rest API와 Grpc API 서버를 하나로 서비스 할 수 있다고?! - 부제 (armeria + kotlin + spring boot integration)

결론적으로 내가 하는 게 아니라, armeria가 다 해줍니다. (킹왕짱 armeria의 아버지 이희승님 찬양 합니다.) 

요즘은 armeria를 붙여서 써 보고 있는 중인데, armeria의 장점은 크게 보면, 아래와 같다.

  1. 비동기 웹 프레임워크
  2. grpc, graphql, thrift 동시 지원 가능 (한 서버로 프로토콜 다르게 서빙이 가능한 최고의 장점)
  3. spring 과도 통합이 가능하다.

spring은 크게 보면, 2가지가 있다.

  1. spring framework
  2. spring webflux

armeria는 spring webflux와도 연동이 가능하고, spring boot와도 통합이 가능하다.

https://armeria.dev/docs/advanced-spring-webflux-integration#using-armeria-with-spring-webflux

 

Using Armeria with Spring WebFlux — Armeria documentation

Using Armeria with Spring WebFlux Visit armeria-examples to find a fully working example. Spring framework provides powerful features which are necessary for building a web application, such as dependency injection, data binding, AOP, transaction, etc.…

armeria.dev

https://armeria.dev/docs/advanced-spring-boot-integration#spring-boot-integration

 

Spring Boot integration — Armeria documentation

Spring Boot integration The Spring framework provides powerful features necessary for building web applications such as dependency injection, data binding, AOP and transaction management. By integrating your Spring application with Armeria, you can serv…

armeria.dev

https://armeria.dev/docs/advanced-kotlin#kotlin-integration

 

Kotlin integration — Armeria documentation

Kotlin integration Coroutines support for annotated services Visit annotated-http-service-kotlin to check out a fully working example. You can implement annotated services with Kotlin coroutines by adding the following dependency: Then, define the sus…

armeria.dev

그렇다면, 익숙한 spring 환경에 armeria 를 통합 해서 사용 해보면 어떨까? 

지원 프로토콜 다르게 여러 서버를 두지 않을 수 있는 장점은 너무나 크다. 

그렇게, 시작 해본 샘플 프로젝트를 github에 올려 두었다. 대다수 코드는 armeria docs를 참조했습니다.

크게 보면, api 모듈, protobufs 모듈 두 모듈만 있는 상태이고, buildSrc는 디펜던시들의 버전을 한군데로 모아둔 역할이라고 생각 하면 편하다. 

셋팅 하는 방법은 너무나 다양한데, 나는 일단 이 방식이 손에 제일 익어서 이렇게 쓰곤 한다. 

아, 참고적으로 protobuf-kotlin 모듈은 실제 protobuf 모듈보다 버전이 좀 느리거나? 버전 잘못 되면 컴파일이 이상하다.

결론, protobuf는 java로 돌리는게 제일 좋은 것 같기도 하다. kotlin은 어차피 kotlin의 언어적인 특성을 모두 잃어버리게 하는게, protobuf-kotlin이라, 약간 계륵 같기도 하다.

아니면 차라리 java21 쓴다고 가정 하면, 굳이 kotlin으로 셋팅 할 이유가 없긴 하다 생각 됩니다. 

이게 결국, kotlin 언어는 좋아도, spring은 지원 잘 되는데, spring 너머의 모듈들은 지원이 제대로 안되거나, 여전히 오류가 있거나 그것도 아니면, syntax sugar 뿌려진 plugin으로 메워야 한다.

(그것은 바로 jpa...hibernate는 kotlin이랑 안맞으므로 kotlin orm 중의 exposed, ktorm 쓰는게 좋아 보인다. 특히나, 둘 중 하나 모듈이 비동기 jdbc 드라이버 사용함. 진짜 개꿀 )

야야 잡담 그만해!

샘플로 작성 해본 protobuf를 봐보자.

syntax = "proto3";

package com.seungdols.company.sample;

option java_package = "com.seungdols.company.sample";

service HelloService {
    rpc Hello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}

사실 여전히 grpc는 잘 모른다. protobuf도 사실 쓸일이 실무에선 없는데, 자꾸 드리븐 해야 쓸일이 생길 것 같긴 하다.

그리고 사실 늘 고민은 protobuf 디렉토리를 한 모듈 내에서까지는 공유하는 건 좋다 생각 하는데, 대규모 프로젝트에서 프로토버프 모듈을 한 군데서 관리 하면 편하긴 한데, 내가 안 쓸 파일까지 컴파일에서 모듈로 들고 온다는게 꺼려지긴 한다. 

이 부분은 아직도 고민 중인 상태라, 부서 전체에 이걸 쓰자 말을 못하겠다. (누군가는 분명 깔끔하지 못하다 생각 할 것이기에)

실무 영역은 그렇다. 내가 진짜 고수가 되어야만 뭔가 새롭게 도입하는게 가능해진다. or 다수의 니즈가 있다면...?

아무튼 각설 하고, API 코드를 보자.

package com.seungdols.company

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class ApiApplication {
}

fun main(args: Array<String>) {
    runApplication<ApiApplication>(*args)
}

application 코드는 별게 없다. 

validationExceptionHandler는 armeria docs에서 가져 온 코드이다.

package com.seungdols.company.sample

import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import com.linecorp.armeria.common.HttpRequest
import com.linecorp.armeria.common.HttpResponse
import com.linecorp.armeria.common.HttpStatus
import com.linecorp.armeria.server.ServiceRequestContext
import com.linecorp.armeria.server.annotation.ExceptionHandlerFunction
import io.micrometer.core.instrument.config.validate.ValidationException
import java.time.Instant

class ValidationExceptionHandler : ExceptionHandlerFunction {
    override fun handleException(
        ctx: ServiceRequestContext,
        req: HttpRequest,
        cause: Throwable,
    ): HttpResponse {
        return if (cause is ValidationException) {
            val status = HttpStatus.BAD_REQUEST
            HttpResponse.ofJson(
                status,
                ErrorResponse(
                    status.reasonPhrase(),
                    cause.message ?: "Unknown error",
                    req.path(),
                    status.code(),
                    Instant.now().toString(),
                ),
            )
        } else {
            ExceptionHandlerFunction.fallthrough()
        }
    }

    data class ErrorResponse
        @JsonCreator
        constructor(
            @JsonProperty("error") val error: String,
            @JsonProperty("message") val message: String,
            @JsonProperty("path") val path: String,
            @JsonProperty("status") val status: Int,
            @JsonProperty("timestamp") val timestamp: String,
        )
}

이건 뭐,,,grpc 서비스 코드 

package com.seungdols.company.sample

import io.grpc.stub.StreamObserver

open class HelloGrpcService : HelloServiceGrpc.HelloServiceImplBase() {
    override fun hello(
        request: Hello.HelloRequest,
        responseObserver: StreamObserver<Hello.HelloReply>,
    ) {
        val reply = Hello.HelloReply.newBuilder().setMessage("Hello, ${request.name}!").build()
        responseObserver.onNext(reply)
        responseObserver.onCompleted()
    }
}

늘, 8-9년간 해왔던 일이 mvc 패턴으로 짜는 거였는데, 생각 해보니 controller가 없어도 되잖아?에서 은근 계몽 했던 순간..

그러면, 진짜 service layer는 도메인 기반의 구조화가 필요한 거구나..싶기도 ? (아니 뭐 평소에 내가 개발 못했다는 소리기도 하다)

막 개발이 짱이지.. 장애 안나고, 기능 구현 일정 내에 맞춰 주고, 읽기 쉽게 작성 하고..? 이게 서비스 개발의 삶이다.

package com.seungdols.company.sample

import com.linecorp.armeria.server.annotation.ExceptionHandler
import com.linecorp.armeria.server.annotation.Get
import com.linecorp.armeria.server.annotation.Param
import org.springframework.stereotype.Component
import org.springframework.validation.annotation.Validated

@Component
@Validated
@ExceptionHandler(ValidationExceptionHandler::class)
class HelloAnnotatedService {
    @Get("/hello")
    fun hello(
        @Param("name") name: String,
    ): String {
        return "Hello, $name!"
    }
}

이것은 rest api의 한 대목이다. 간단하다. 

package com.seungdols.company.configuration

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.grpc.GrpcService
import com.linecorp.armeria.server.logging.AccessLogWriter
import com.linecorp.armeria.server.logging.LoggingService
import com.linecorp.armeria.spring.ArmeriaServerConfigurator
import com.seungdols.company.sample.HelloAnnotatedService
import com.seungdols.company.sample.HelloGrpcService
import com.seungdols.company.sample.HelloServiceGrpc
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class ArmeriaConfiguration {
    @Bean
    fun armeriaServerConfigurator(
        helloAnnotatedService: HelloAnnotatedService,
    ): ArmeriaServerConfigurator {
        return ArmeriaServerConfigurator { builder: ServerBuilder ->
            builder.serviceUnder("/docs", DocService())
            builder.decorator(LoggingService.newDecorator())
            builder.accessLogWriter(AccessLogWriter.combined(), false)

            val services =
                listOf(
                    helloAnnotatedService,
                )

            services.forEach { service ->
                builder.annotatedService(service)
            }

            builder.service(
                GrpcService.builder()
                    .addService(HelloServiceGrpc.bindService(HelloGrpcService()))
                    .supportedSerializationFormats(GrpcSerializationFormats.values())
                    .enableUnframedRequests(true)
                    .build(),
            )
        }
    }
}

이...설정이 은근 복잡하다고 해야 하나? spring framework 쓸 때는 알아서...component scan 착착 하면서 bean 등록을 해주는데, armeria - spring boot 관계에서는 일단, armeria가 갑이다. 

쉽게 이해하게 설명 하자면 갑에게 을의 정보를 전달 해줘야 한다. (개발자가 수동으로..)

이걸 좀 더 고급지게 하는 방법을 아직 찾지 못했다. 끽해야 list에 넣고 loop 돌리는 수준까지 밖에..

아무튼 이렇게 하면 잘 뜬다. 또 하나의 장점은 armeria의 docs는 정말 강력하다. (이것 때문에 반하기도 했다.)

http://localhost:8080/docs/#/ 주소로 접속 하면 문서 페이지가 열린다. client 기능도 있어서 바로 테스트 가능하다.

정상적으로 rest api가 호출 된 것을 확인 할 수 있다. 

물론, grpc도 기똥차게 잘 적용 된 것을 확인 할 수 있다. 

https://github.com/seungdols/armeria_springboot_integration

여기서 시작이다. 

고민 해야 할 부분은 armeria는 http1.1에서도 grpc가 동작하게 만들었으나, 기본적으로 http/2 스펙으로 가야 정상적인 퍼포먼스를 보여줄 것이라 생각 한다. 

그렇기때문에, 인프라 작업을 해줘야 한다. http/2를 적용하려면, 이것 저것 인프라 환경에 손을 대야 하기 때문이다. 

실무에서 적용하기 까다로운 부분이 바로 그 지점이다.

하지만, 용자는 늘 승리하는 법. 용자가 귀찮음을 해결하면 된다. 원래 회사에 그런 사람 한 둘쯤은 있는 것 아닐까?

아무튼 요새, 재테크 공부 하랴, 러닝 하랴, 여행 준비 하랴, 일 하랴 정신 없다. 

결론, 공부 하지 말고 나가 놀자! 그게 삶에 더 유익 합니다.

반응형