Spring에 net.devh 라이브러리를 활용한 gRPC 구현

커뮤니티/오픈소스(3rd-party) 라이브러리를 활용한 구현 방법에 대해서 알아 본다.

개요

Spring에 Kotlin와 커뮤니티/오픈소스(3rd-party) 라이브러리인 net.devh를 활용한 간단한 gRPC 구현을 해보겠다.

목표

  • Spring Boot 프로젝트에 커뮤니티/오픈소스(3rd-party) 라이브러리인 net.devh를 활용하여 gRPC 서버를 연동
  • .proto 파일 작성 및 Stub 자동 생성
  • gRPC 서비스 구현
  • gRPC 클라이언트 작성 및 호출

프로젝트 생성

프로젝트 생성은 Spirng ininitializr으로 생성할 수 있다.

Spirng ininitializr

화면 아래 부근에 “GENERATE” 버튼을 누르면, 설정한 프로젝트 파일을 다운로드를 받을 수 있다.

또는, 다음과 같이 curl 명령어를 사용하여 Spring Boot 초기 프로젝트를 생성할 수도 있다.

curl https://start.spring.io/starter.tgz \
-d bootVersion=3.5.7 \
-d baseDir=spring-grpc-1 \
-d groupId=com.devkuma \
-d artifactId=spring-grpc \
-d packageName=com.devkuma.grpc \
-d applicationName=GrpcApplication \
-d packaging=jar \
-d language=kotlin \
-d javaVersion=21 \
-d description="Demo project for Spring gRPC" \
-d type=gradle-project-kotlin | tar -xzvf -

빌드 스크립트 설정

빌드 스크립트에 gRPC 관련 라이브러리를 추가하고, protobuf 설정을 작성한다.

build.gradle

plugins {
	kotlin("jvm") version "1.9.25"
	kotlin("plugin.spring") version "1.9.25"
	id("org.springframework.boot") version "3.5.7"
	id("io.spring.dependency-management") version "1.1.7"

    // gRPC
    id("com.google.protobuf") version "0.9.5"
}

group = "com.devkuma"
version = "0.0.1-SNAPSHOT"
description = "Demo project for Spring gRPC"

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(21)
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation("org.springframework.boot:spring-boot-starter")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
	testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
	testRuntimeOnly("org.junit.platform:junit-platform-launcher")

    // gRPC + protobuf
    implementation("io.grpc:grpc-kotlin-stub:1.4.3")
    implementation("io.grpc:grpc-protobuf:1.75.0")
    implementation("io.grpc:grpc-stub:1.75.0")
    implementation("io.grpc:grpc-netty-shaded:1.75.0")
    implementation("com.google.protobuf:protobuf-kotlin:3.22.3")

    // Spring gRPC Starter
    implementation("net.devh:grpc-server-spring-boot-starter:3.1.0.RELEASE")
}

kotlin {
	compilerOptions {
		freeCompilerArgs.addAll("-Xjsr305=strict")
	}
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.24.4"
    }

    plugins {
        register("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:1.75.0"
        }
    }

    generateProtoTasks {
        all().forEach { task ->
            task.plugins {
                create("grpc")
            }
        }
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

Protobuf 파일 작성

src/main/proto 디렉토리에 gRPC 서비스와 메시지를 정의한 .proto 파일을 작성한다.

**src/main/proto/hello.proto

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.devkuma.grpc";
option java_outer_classname = "HelloProto";

service HelloService {
  rpc SayHello(HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string message = 1;
}

gRPC 서버 구현

Service 객체 생성

src/main/kotlin/com/devkuma/grpc/service/HelloServiceImpl.kt

package com.devkuma.grpc.service

import com.devkuma.grpc.HelloRequest
import com.devkuma.grpc.HelloResponse
import com.devkuma.grpc.HelloServiceGrpc
import net.devh.boot.grpc.server.service.GrpcService
import io.grpc.stub.StreamObserver

@GrpcService
class HelloServiceImpl : HelloServiceGrpc.HelloServiceImplBase() {

    override fun sayHello(
        request: HelloRequest,
        responseObserver: StreamObserver<HelloResponse>
    ) {
        val reply = "Hello, ${request.name}!"

        val response = HelloResponse.newBuilder()
            .setMessage(reply)
            .build()

        responseObserver.onNext(response)
        responseObserver.onCompleted()
    }
}

서버 설정

기본 gRPC 포트는 9090이다.
원하면 application.yml로 변경 가능하다.

src/main/kotli/resources/application.yml

grpc:
  server:
    port: 9090

gRPC 클라이언트 구현

같은 프로젝트에서 테스트를 하기 위해, 클라이언트 설정 Bean을 추가한다.

클라이언 설정

src/main/kotlin/com/devkuma/grpc/client/GrpcClientConfig.kt

package com.devkuma.grpc.client

import com.devkuma.grpc.HelloServiceGrpc
import io.grpc.ManagedChannel
import io.grpc.ManagedChannelBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class GrpcClientConfig {

    @Bean
    fun helloChannel(): ManagedChannel =
        ManagedChannelBuilder
            .forAddress("localhost", 9090)
            .usePlaintext()
            .build()

    @Bean
    fun helloStub(channel: ManagedChannel): HelloServiceGrpc.HelloServiceBlockingStub =
        HelloServiceGrpc.newBlockingStub(channel)
}

gRPC Stub을 호출하는 Service 생성

단순히 stub을 호출하는 Service를 생성한다.

src/main/kotlin/com/devkuma/grpc/client/HelloGrpcClientService.kt

package com.devkuma.grpc.client

import com.devkuma.grpc.HelloRequest
import com.devkuma.grpc.HelloServiceGrpc
import org.springframework.stereotype.Service

@Service
class HelloGrpcClientService(
    private val helloStub: HelloServiceGrpc.HelloServiceBlockingStub,
) {

    fun sayHello(name: String): String {
        val request = HelloRequest.newBuilder()
            .setName(name)
            .build()

        val response = helloStub.sayHello(request)
        return response.message
    }
}

테스트

서버와 클라이언트를 하나의 프로젝트에 넣었기에, 테스트 파일 1개로 테스트가 가능하다.

package com.devkuma.grpc

import com.devkuma.grpc.client.HelloGrpcClientService
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import kotlin.text.contains

@SpringBootTest
class GrpcIntegrationTest {

    @Autowired
    lateinit var client: HelloGrpcClientService

    @Test
    fun testSayHello() {
        val result = client.sayHello("devkuma")
        assert(result.contains("devkuma"))

        println(result)
    }
}

output:

Hello, devkuma!

참고

위에 예제 코드는 GitHub에서 확인해 볼 수 있다.