Spring 공식 프로젝트 라이브러리를 활용한 gRPC 구현

Spring 공식 프로젝트(Spring Team이 개발) 라이브러리를 활용한 구현 방법에 대해서 알아 본다.

개요

Spring 공식 프로젝트(Spring Team이 개발) gRPC 라이브러리를 활용한 gRPC 구현을 해보겠다.

목표

  • Spring Boot 프로젝트에 Spring 공식 프로젝트 gRPC 라이브러리를 활용한 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 dependencies=spring-grpc \
-d baseDir=spring-grpc-2 \
-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

import com.google.protobuf.gradle.id

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()
}

extra["springGrpcVersion"] = "0.12.0"

dependencies {
    implementation("io.grpc:grpc-services")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.springframework.grpc:spring-grpc-spring-boot-starter") // Spring gRPC Starter
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
    testImplementation("org.springframework.grpc:spring-grpc-test")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")

    // Kotlin coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")

    // 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")
}

dependencyManagement {
    imports {
        mavenBom("org.springframework.grpc:spring-grpc-dependencies:${property("springGrpcVersion")}")
    }
}

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

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc"
    }
    plugins {
        id("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java"
        }
        id("grpckt") {
            artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.3:jdk8@jar"
        }
    }
    generateProtoTasks {
        all().forEach {
            it.plugins {
                id("grpc") {
                    option("@generated=omit")
                }
                id("grpckt") {
                    option("@generated=omit")
                }
            }
        }
    }
}

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.HelloServiceGrpcKt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.springframework.stereotype.Service

@Service
class HelloServiceImpl : HelloServiceGrpcKt.HelloServiceCoroutineImplBase() {

    override suspend fun sayHello(request: HelloRequest): HelloResponse =
        withContext(Dispatchers.Default) {
            val message = "Hello, ${request.name}! (from Spring gRPC Kotlin)"

            HelloResponse.newBuilder()
                .setMessage(message)
                .build()
        }
}

서버 설정

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

src/main/kotli/resources/application.yml

spring:
  grpc:
    server:
      port: 9090

gRPC 클라이언트 구현

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

클라이언 설정

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

package com.devkuma.grpc.client

import com.devkuma.grpc.HelloServiceGrpcKt
import io.grpc.ManagedChannel
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.grpc.client.GrpcChannelFactory

@Configuration
class GrpcClientConfig {

    @Bean
    fun helloStub(channelFactory: GrpcChannelFactory): HelloServiceGrpcKt.HelloServiceCoroutineStub {
        val channel: ManagedChannel = channelFactory.createChannel("local")
        return HelloServiceGrpcKt.HelloServiceCoroutineStub(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.HelloServiceGrpcKt
import org.springframework.stereotype.Service

@Service
class MyGrpcClientService(
    private val helloStub: HelloServiceGrpcKt.HelloServiceCoroutineStub
) {

    suspend 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.MyGrpcClientService
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class GrpcIntegrationTest {

    @Autowired
    lateinit var client: MyGrpcClientService

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

        println(result)
    }
}

output:

Hello, devkuma!

참고

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