Spring gRPC 1.0.0 ๋ฒ์ ์ด ์ ์ ๋ฆด๋ฆฌ์ฆ ๋๋ค. ์ด๋ฅผ ํตํด ์ด์ Spring ๊ณต์ ์ง์์ ํตํด ์์ ์ ์ธ ๊ณ ์ฑ๋ฅ RPC(Remote Procedure Call) ํ๊ฒฝ์ ์ฝ๊ฒ ๊ตฌ์ถํ ์ ์๋ค. ์ด๋ฒ ํฌ์คํ ์์๋ gRCP 1.0.0 ์ ์ ๋ฆด๋ฆฌ์ฆ๊ฐ ๋๋ฉด์ ๊ธฐ์กด์ gRPC 0.X ๋ฒ์ ์ ์ฌ์ฉํ๋ ๊ธฐ์กด ํ๋ก์ ํธ์์ gRPC 1.0.0 ๋ฒ์ ์ ์ ์ฉํ๊ธฐ ์ํด์ ๊ณ ๋ คํด์ผ ํ ์ฌํญ๊ณผ ์ ์ฉ ๋ฐฉ๋ฒ, ๊ทธ๋ฆฌ๊ณ gRPC ์ฌ์ฉ๋ฒ์ ๋ํด์ ๊ฐ๋จํ ์ ๋ฆฌํด ๋ณด๊ณ ์ ํ๋ค.
gRPC ํต์ ์ ์ด์ ์ ๋ํ์ ์ผ๋ก ๋ค์๊ณผ ๊ฐ๋ค.
- ์ ์ง์ฐ ํต์ : REST/JSON ๋๋น ํจ์ฌ ์์ Protobuf ํ์ด๋ก๋๋ฅผ ์ฌ์ฉํ์ฌ ๋คํธ์ํฌ ๋์ญํญ์ ์ ์ฝํ๊ณ ์ง์ฐ ์๊ฐ์ ์ค์ผ ์ ์๋ค.
- ํ์ ์์ ์ฑ: Protobuf ์คํค๋ง๋ฅผ ํตํด ์ปดํ์ผ ์์ ์ ํต์ ๊ณ์ฝ์ ๊ฒ์ฆํ ์ ์์ด ๋ฐํ์ ์ค๋ฅ ๊ฐ๋ฅ์ฑ์ด ํฌ๊ฒ ์ค์ด๋ ๋ค. ์ด๋ฅผ ํตํด ๊ฐ๋ฐ ์์ฐ์ฑ ๋ฐ ์์คํ ์์ ์ฑ ํฅ์์ ๊ธฐ๋ํ ์ ์๋ค.
ํ์ง๋ง ์์ฝ๊ฒ๋ Spring Framework 7 (Spring Boot 4) ๋ฒ์ ์ ํฌํจ์ํฌ ๊ฒ์ ์ผ๋์ ๋๊ณ ํ๋ก์ ํธ๊ฐ ์์๋์ด Spring Boot 3๋ฒ์ ์์๋ ์ฌ์ฉํ ์ ์์ง๋ง ๋ง์ฝ ์ถํ์ Spring Boot 3์์ gRPC 0.12(์คํ๋ฒ์ )์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ Spring Boot 4๋ก ๋ฒ์ ์ ์ ๊ทธ๋ ์ด๋ ํ๋๋ผ๋ ๋ง์ด๊ทธ๋ ์ด์ ํด์ผ ํ ์ฌํญ์ ํฌ๊ฒ ์์ ๊ฒ ๊ฐ๋ค.
์ด์ ๋ ๋ค์๊ณผ ๊ฐ์ ํธํ์ฑ์ ๊ณ ๋ คํ๊ธฐ ๋๋ฌธ์ด๋ค.
- ์ด๋ ธํ ์ด์ : @GrpcService, @GrpcClient ์ ์ง
- ํจํค์ง ๊ฒฝ๋ก: org.springframework.grpc.* ์ ์ง
- ์ค์ ํ์ผ: application.yml์ spring.grpc.* ์์ฑ ๊ตฌ์กฐ ์ ์ง
- ๋น์ฆ๋์ค ๋ก์ง: StreamObserver๋ฅผ ์ฌ์ฉํ๋ ๋ก์ง์ gRPC ์์ฒด ํ์ค์ด๋ฏ๋ก ๋ณ๊ฒฝ ์์
Spring gRPC ์ฌ์ฉ์ ์ํ ๊ถ์ฅ ๋ฉ์ปค๋์ฆ์ธ BOM(Bill of Materials), ์๋ ๊ตฌ์ฑ ๋ฐ starter๋ ํ์ฌ ์์น(groupId๊ฐ org.springframework.grpc์ธ ์ํ)๋ฅผ ์ ์งํ๋ค. ๋ฐ๋ผ์ Spring gRPC 0.12.0์ ์ฌ์ฉ ์ค์ด์๋ค๋ฉด ์์กด์ฑ ๊ด๋ฆฌ์์ ๋ฒ์ ๋ง ๋ณ๊ฒฝํ๋ฉด ๋๋ค. ์๋ ๊ตฌ์ฑ ํด๋์ค ์์ฒด๋ ํจํค์ง ์ด๋ฆ์ด ๋ณ๊ฒฝ๋๋ฏ๋ก ํด๋น ํด๋์ค๋ฅผ ๋ช
์์ ์ผ๋ก ์ฌ์ฉ ์ค์ธ ๊ฒฝ์ฐ(์๋ง๋ ์์ ์ฌ๋ก์ผ ๊ฒ) import๋ ๋ณ๊ฒฝํด์ผ ํ ์ ์๋ค.
Spring Boot 4.1์ ํฌํจ๋ ์์ ์ธ ์๋ ๊ตฌ์ฑ ๊ธฐ๋ฅ์ด ๋ณํฉ๋๋ฉด ๋ณ๋์ BOM์ด ํ์ํ์ง ์์ผ๋ฉฐ starter์ ๋ํ ์ข
์์ฑ ๊ด๋ฆฌ์์ ์ฝ๋๋ค์ดํธ๋ง ๋ณ๊ฒฝํ๋ฉด ๋๋ค.
https://spring.io/blog/2025/11/05/spring-grpc-next-steps ์ฐธ์กฐ
๊ฐ๋จํ ์๋ก ๊ธฐ์กด gRPC 0.12.0๋ฒ์ ์ ์ฌ์ฉํ๊ณ ์๋ ๊ฒฝ์ฐ maven ์ค์ ์ ์๋์ ๊ฐ์ ๊ฒ์ด๋ค.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.grpc</groupId>
<artifactId>spring-grpc-dependencies</artifactId>
<version>0.12.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.grpc</groupId>
<artifactId>spring-grpc-spring-boot-starter</artifactId>
</dependency>
...XMLgRPC 1.0.0์ผ๋ก ์
๊ทธ๋ ์ด๋ ํ๊ธฐ ์ํด์๋ <version> ํ๋์ ์ ๋ณด๋ง ๋ณ๊ฒฝํ๋ฉด ๋๋ค.
gradle์ ๊ฒฝ์ฐ๋ ๋ง์ฐฌ๊ฐ์ง๋ค.
dependencyManagement {
imports {
mavenBom 'org.springframework.grpc:spring-grpc-dependencies:0.12.0'
}
}
dependencies {
implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter'
...GroovydependencyManagement ์น์ ์ 0.12.0์ 1.0.0์ผ๋ก ๋ณ๊ฒฝ๋ง ํ๋ฉด ๋๋ค.
Spring gRPC ์ฌ์ฉ๋ฒ
๋ฐฑ๋ฌธ์ด ๋ถ์ฌ์ผ๊ฒฌ์ด๋ผ๊ณ gRPC ์ฌ์ฉ๋ฒ์ ๋ํด์ ์ํ ํ๋ก์ ํธ๋ฅผ ํตํด์ ์์๋ณด์. ๋น๋ ๋๊ตฌ๋ gradle์ ์ฌ์ฉํ๋ค.
์ํ ํ๋ก์ ํธ๋ ์ด 3๊ฐ์ submodule๋ก ๊ตฌ์ฑ๋์ด ์๋ค.
- grpc-client
- grpc-interface
- grpc-server
grpc-client ๋ชจ๋์ ํด๋ผ์ด์ธํธ ์ญํ ์ ํ ๋ชจ๋์ด๋ค.
grpc-interface ๋ชจ๋์ ํด๋ผ์ด์ธํธ์ ์๋ฒ๊ฐ์ gRPC ํต์ ์ ์ํ protobuf ๋ช
์ธ๋ฅผ ์ ์ํ๊ธฐ ์ํ ๋ชจ๋์ด๋ค.
grpc-server ๋ชจ๋์ ์๋ฒ ์ญํ ์ ํ ๋ชจ๋์ด๋ค.
๋น๋ ๋ฐ ๋ํ๋์ ์ ์
๊ธฐ๋ณธ ๋น๋ ์ค์ ์ grpc-client, grpc-server๋ root์ build.gradle ์ค์ ์ ์ด์ด๋ฐ์ ์ฌ์ฉํ๋๋ก ํ๋ค.
root settings.gradle
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
// โจ ์ฌ๊ธฐ์ ๋ฒ์ ์ ์ ์ํ๋ฉด, ํ๋ก์ ํธ ๋ด ๋ชจ๋ build.gradle์์ ๋ฒ์ ์๋ต ๊ฐ๋ฅ!
plugins {
id 'com.google.protobuf' version '0.9.5'
id 'io.spring.dependency-management' version '1.1.7'
id 'org.springframework.boot' version '4.0.0'
id 'java-library' // ์ฝ์ด ํ๋ฌ๊ทธ์ธ์ ๋ฒ์ ๋ถํ์
id 'java'
}
}
rootProject.name = 'spring-boot-grpc'
include 'grpc-server'
include 'grpc-client'
include 'grpc-interface'Groovysettings.gradle์ pluginManagement๋ฅผ ์ ์ํ์ฌ ๋ชจ๋ submodule์์ ๊ณตํต์ผ๋ก ์ฌ์ฉํ๋๋ก ์ ์ํ๋ค.
org.springframework.boot ๋ฒ์ ์ 4.0.0 ์ด์ ๋ฒ์ ์ ์ง์ ํด์ผ ํ๋ค.
root build.gradle
plugins {
id 'java'
id 'java-library'
id 'org.springframework.boot' apply false
id 'io.spring.dependency-management' apply false
// Protobuf ์ปดํ์ผ ํ๋ฌ๊ทธ์ธ
id 'com.google.protobuf' apply false
}
// ๋ชจ๋ submodule์์ ์ฌ์ฉํ๊ธฐ ์ํ ์ค์
subprojects {
apply plugin: 'java'
group = 'org.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
}
description = 'spring-boot-grpc-1.0.0'
// [์กฐ๊ฑด๋ถ ์ ์ฉ] grpc-server๋ grpc-client ๋ชจ๋์๋ง ์ ์ฉ (interface ์ ์ธ!)
configure(subprojects.findAll { it.name.contains('server') || it.name.contains('client') }) {
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
ext {
set('springGrpcVersion', "1.0.0")
}
dependencyManagement {
imports {
mavenBom "org.springframework.grpc:spring-grpc-dependencies:${springGrpcVersion}"
mavenBom "org.springframework.boot:spring-boot-dependencies:4.0.0"
}
}
dependencies {
// grpc-server์ grpc-client๋ชจ๋ grpc-interface๋ฅผ ๊ณตํต์ผ๋ก ์ฌ์ฉํด์ผ ํ๋ฏ๋ก ๋ฐ๋์ ํฌํจํด์ผ ํ๋ค.
implementation project(':grpc-interface')
developmentOnly 'org.springframework.boot:spring-boot-devtools'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter'
testImplementation 'org.springframework.grpc:spring-grpc-test'
}
tasks.named('test') {
useJUnitPlatform()
}
}
Groovygrpc-server, grpc-client ์์กด์ฑ์ ๊ฑฐ์ ๋์ผํ๋ฏ๋ก root ํ๋ก์ ํธ build.gradle์ ์ ์ํ์๋ค.
grpc-server, grpc-client ๋ชจ๋์ build.gradle์ root์ build.gradle์์ ์ ์ํ์ผ๋ฏ๋ก description ์ ๋ณด๋ง ์ถ๊ฐํ๋ค.
# grpc-server์ build.gradle
description = 'grpc-server'
# grpc-client์ build.gradle
description = 'grpc-client'Groovygrpc-interface
์ด๋ฒ ํฌ์คํ ์์ ๊ฐ์ฅ ํต์ฌ ์ญํ ์ ํ ๋ชจ๋์ด๋ค. build.gradle๊ณผ proto ํ์ผ ์ ์๋ฅผ ์ดํด๋ณด์.
build.gradle
grpc client์ grpc server๊ฐ ๊ณตํต์ผ๋ก ์ฌ์ฉํ๋ ๋ฉ์์ง๋ฅผ ์ ์ํด์ผ ํ๋ฏ๋ก protobuf ๋ช ์ธ๋ ๋ณ๋์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ์์ ํ์ฌ client์ server์ ๋ชจ๋ ๋ฐฐํฌํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ผ ๊ฒ ๊ฐ๋ค.
plugins {
id 'java-library'
id 'io.spring.dependency-management'
// Protobuf ์ปดํ์ผ ํ๋ฌ๊ทธ์ธ
id 'com.google.protobuf'
}
description = 'grpc-interface'
// ์์ ์ธ๊ธํ๋ฏ์ด Spring gRPC 0.12.0์ ์ฌ์ฉํ๊ณ ์์๋ค๋ฉด 1.0.0์ผ๋ก ๋ฒ์ ๋ง ๋ณ๊ฒฝํด ์ฃผ๋ฉด ๋๋ค.
ext {
set('springGrpcVersion', "1.0.0")
}
dependencyManagement {
imports {
mavenBom "org.springframework.grpc:spring-grpc-dependencies:${springGrpcVersion}"
}
}
dependencies {
// Protobuf ๋ฉ์์ง (Message) ์ง์
// (.proto์ message๊ฐ ๋ณํ๋ ์๋ฐ ํด๋์ค๊ฐ ์์๋ฐ๋ ๊ธฐ๋ณธ ํด๋์ค๋ค)
api 'com.google.protobuf:protobuf-java'
// gRPC ์คํ
(Stub) ์ง์
// (Service๊ฐ ๋ณํ๋ Stub ์ฝ๋๊ฐ ์์กดํ๋ gRPC ํต์ฌ ๊ธฐ๋ฅ)
api 'io.grpc:grpc-stub'
// Protobuf์ gRPC ์ฐ๊ฒฐ
// (Protobuf ๊ฐ์ฒด๋ฅผ gRPC ํต์ ์ผ๋ก ์ง๋ ฌํ/์ญ์ง๋ ฌํํ๋ ๊ธฐ๋ฅ)
api 'io.grpc:grpc-protobuf'
// ์ด๋
ธํ
์ด์
์ง์ (@Generated ๋ฑ ์์ฑ๋ ์ฝ๋์ ๋ถ์)
implementation 'javax.annotation:javax.annotation-api:1.3.2'
}
// .proto ํ์ผ์ ์๋ฐ ์ฝ๋๋ก ๋ณํํด ์ฃผ๊ธฐ ์ํ ์ค์
protobuf {
// protobuf ์ปดํ์ผ๋ฌ(protoc) ๋ค์ด๋ก๋
protoc {
artifact = "com.google.protobuf:protoc"
}
// service/stub ํด๋์ค ํ์ผ ์์ฑ์ ์ํ ํ๋ฌ๊ทธ์ธ ์ถ๊ฐ
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java"
}
}
generateProtoTasks {
all()*.plugins {
grpc {
option '@generated=omit'
}
}
}
}
Groovyproto ํ์ผ ์ ์
protobuf ๋ช
์ธ์๋ฅผ ์ ์ํด์ผ ํ๋๋ฐ ์ด ์ญํ ์ ํ๋ ๊ฒ์ด proto ํ์ผ์ด๋ค.
src/main/proto ๋๋ ํ ๋ฆฌ์ ๋ค์ ๋ ๊ฐ์ proto ํ์ผ์ ์ ์ํ ๊ฒ์ด๋ค.
src/main/proto/simple.proto (helloworld์ ๊ฐ์ ๋จ์ํ ์์ฒญ ์๋ต ์ ์)
syntax = "proto3";
// Protobuf ์คํ์ ํจํค์ง ๊ฒฝ๋ก
package org.example.grpcinterface;
// [Java ์ต์
์ค์ ]
// 1. ํ์ผ์ ๊ฐ๊ฐ ๋ถ๋ฆฌํด์ ์์ฑ (ํ์ ๊ถ์ฅ)
option java_multiple_files = true;
// 2. ์์ฑ๋ Java ํ์ผ์ ํจํค์ง ๊ฒฝ๋ก (์์ package ์ ์ ๋ณด๋ค ์ด ์ค์ ์ด ์ฐ์ ํ๋ค!!)
option java_package = "org.example.grpcinterface";
// 3. (์ ํ) Outer Class ์ด๋ฆ ์ง์ (java_multiple_files=false์ผ ๋ ์ฃผ๋ก ์)
option java_outer_classname = "SimpleProto";
// ์๋น์ค ์ ์ (Interface)
service SimpleService {
// ์์ฒญ(HelloRequest)์ ๋ฐ์์ ์๋ต(HelloReply)์ ๋ฐํํ๋ ๋ฉ์๋
rpc SayHello (HelloRequest) returns (HelloReply);
}
// ์์ฒญ ๋ฉ์์ง (DTO)
message HelloRequest {
string name = 1; // 1๋ฒ ํ๋
}
// ์๋ต ๋ฉ์์ง (DTO)
message HelloReply {
string message = 1; // 1๋ฒ ํ๋
}Groovy์๋ฒ์์ ์ฌ์ฉํ SimpleService์ ์ฃผ๊ณ ๋ฐ์ ๋ฉ์์ง DTO๋ HelloRequest, HelloReply๋ค.
proto3 ๋ฒ์ ์ ์ฌ์ฉํ์๋๋ฐ ํด๋น proto ์์ฑ ๊ด๋ จ ๊ฐ์ด๋๋ https://protobuf.dev/programming-guides/proto3/ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ๋ฉด ๋์์ด ๋ ๊ฒ์ด๋ค.
src/main/proto/fileupload.proto
syntax = "proto3";
package org.example.grpcinterface;
option java_multiple_files = true;
option java_package = "org.example.grpcinterface";
option java_outer_classname = "FileUploadProto";
service FileUploadService {
// stream์ด Request ์ชฝ์ ๋ถ์ด ์๋ค. (ํด๋ผ์ด์ธํธ๊ฐ ๊ณ์ ๋ณด๋ธ๋ค๋ ์๋ฏธ)
rpc UploadFile( stream FileUploadRequest ) returns ( FileUploadResponse );
}
message FileUploadRequest {
//oneof: ๋ ์ค ํ๋๋ง ๋ด์์ ๋ณด๋ธ๋ค.
oneof request {
FileInfo info = 1; // ์ฒซ ๋ฒ์งธ ๋ฉ์์ง ์ฉ (ํ์ผ ๋ฉํ ์ ๋ณด)
bytes chunk_data = 2; // chunked file data
}
}
message FileInfo {
string file_name = 1;
string file_type = 2;
}
message FileUploadResponse {
string file_name = 1;
string status = 2;
int64 total_size = 3;
}GroovygRPC๋ฅผ ํตํด ํ์ผ ์ ๋ก๋๋ฅผ ์ํด์ ์ ์ํ์๋ค. FileUploadRequest ์ ์์์ oneof๋ FileInfo์ chunk_data ๋ ์ค์ ํ๋๋ง ๋ณด๋ผ ์ ์๋ค. ์ฒ์ ํต์ ์์๋ FileInfo๋ฅผ ํตํด ํ์ผ์ ๋ํ ๋ฉํ ์ ๋ณด๋ฅผ ์ ์ก ํ๊ณ ์ดํ ํ์ผ ๋ฐ์ดํฐ๋ฅผ chunked ๋ฐฉ์์ผ๋ก ์ ์กํ ๊ฒ์ด๋ค.
์ด์ grpc-interface ๋ชจ๋์ build ํ๋ฉด grpc-interface ๋ชจ๋ ํ์์
build/generated/proto/main/grpc, build/generated/proto/main/java ๋๋ ํ ๋ฆฌ๊ฐ ์์ฑ๋ ๊ฒ์ด๋ค.
- build/generated/sources/proto/main/grpc์ ํต์ ์ ์ํ ๋๊ตฌ ์ญํ ์ ํ๋ ํด๋์ค ํ์ผ์ด ์์ฑ๋๋๋ฐ ์ด๋ proc-gen-grpc-java ํ๋ฌ๊ทธ์ธ์์ ์์ฑํ๋ค. (๋ณดํต์ ํ์ผ๋ช ๋ค์ Grpc๊ฐ ๋ถ๋๋ค.)
- build/generated/sources/proto/main/java๋ Message/DTO ์ญํ ์ ํ๋ ํด๋์ค๊ฐ ์์ฑ๋๋ค. proto ํ์ผ์์ message ํค์๋๋ก ์ ์ํ ๋ด์ฉ๋ค์ด ์๋ฐ ํด๋์ค๋ก ๋ณํ๋์ด ์์ฑ๋๋๋ฐ ์์ฑ ์ฃผ์ฒด๋ protoc ์ปดํ์ผ๋ฌ๊ฐ ์์ฑํ๋ค.

grpc-client / grpc-server ๊ตฌํ
grpc-interface๊ฐ ์ฑ๊ณต์ ์ผ๋ก ๋น๋ ๋์๋ค๋ฉด ์ด์ gRPC๋ฅผ ์ฌ์ฉํ ์ค๋น๋ ๋๋ฌ๋ค.
simple service
์ฐ์ simple proto์ ๋ํ client์ server ๊ตฌํ ์ํ ์ฝ๋๋ค.
grpc-client
GrpcClientConfig
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import org.example.grpcinterface.FileUploadServiceGrpc;
import org.example.grpcinterface.SimpleServiceGrpc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GrpcClientConfig {
// ์ฑ๋(Connection) ๋น ์์ฑ
@Bean
public ManagedChannel simpleChannel() {
// application.yml์ ์ค์ ์ ๊ฐ์ ธ์์ ์ธ ์๋ ์์ง๋ง,
// 1.0.0 ์ด๊ธฐ ์ค์ ์์๋ ๋ช
์์ ์ผ๋ก ๋น๋๋ฅผ ์ฐ๋ ๊ฒ์ด ํ์คํ๋ค.
return ManagedChannelBuilder.forAddress("localhost", 9090)
.usePlaintext() // ๊ฐ๋ฐ์ฉ: ํ๋ฌธ ํต์
.build();
}
// Simple ์คํ
(Stub) ๋น ์์ฑ
@Bean
public SimpleServiceGrpc.SimpleServiceStub simpleStub( ManagedChannel channel) {
return SimpleServiceGrpc.newStub(channel);
}
}Javagrpc-client์์ simple service์ ํต์ ์ ์ํ Stub ํด๋์ค๋ฅผ ๋น์ผ๋ก ์ ์ํด์ ์ฌ์ฉํ ์ ์๋ค. Stub์ ๋ง์น ๋ก์ปฌ์ ์๋ ๋ฉ์๋๋ฅผ ํธ์ถํ๋ฏ์ด ํธ์ถํ ์ ์๋๋ก ํ๋ค. ์ด ์๋ฏธ๋ Stub ๋ด๋ถ์ ์ผ๋ก ์๋ฒ์ ์๋น์ค์ ํต์ ํ๋ ์ญํ ์ ๋ค ์ํํ๋ค๋ ์๋ฏธ๋ค.
SimpleRunner
import io.grpc.stub.StreamObserver;
import org.example.grpcinterface.HelloReply;
import org.example.grpcinterface.HelloRequest;
import org.example.grpcinterface.SimpleServiceGrpc;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;
@Component
public class SimpleRunner implements CommandLineRunner {
private final SimpleServiceGrpc.SimpleServiceStub simpleStub;
public SimpleRunner( SimpleServiceGrpc.SimpleServiceStub simpleStub ) {
this.simpleStub = simpleStub;
}
@Override
public void run( String... args ) {
System.out.println( "start simple runner" );
CompletableFuture<String> future = new CompletableFuture<>();
// 1. ์์ฒญ ๊ฐ์ฒด ์์ฑ (Builder ํจํด)
HelloRequest request = HelloRequest.newBuilder()
.setName("Grpc Developer")
.build();
// 2. gRPC ํธ์ถ (๋ง์น ๋ก์ปฌ ๋ฉ์๋ ๋ถ๋ฅด๋ฏ์ด!)
// ์ฌ๊ธฐ์ ๋คํธ์ํฌ ํต์ ์ด ์ผ์ด๋๋ค.
simpleStub.sayHello( request, new StreamObserver<>() {
@Override
public void onNext( HelloReply value ) {
future.complete( value.getMessage() );
}
@Override
public void onError( Throwable t ) {
future.completeExceptionally( t );
}
@Override
public void onCompleted() {
}
} );
future.whenComplete( ( resp, err ) -> {
if ( err != null ) {
System.out.println("simple service error: " + err.getMessage());
}
else {
System.out.println("simple service response: " + resp);
}
});
future.join();
}
}JavagRPC๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋น๋๊ธฐ๋ก ๋์์ ํ๊ธฐ ๋๋ฌธ์ StreamObserver๋ฅผ ํตํด์ ์ด๋ฒคํธ๋ฅผ ํธ๋ค๋ง ํด์ค์ผ ํ๋ค. ๋ฌผ๋ก ๋๊ธฐ ๋ฐฉ์์ผ๋ก๋ ์ฌ์ฉํ ์ ์๋ค. ์ํ์์๋ ๋น๋๊ธฐ ๋ฐฉ์์ผ๋ก ๊ตฌํ์ ํ๊ณ main ์ค๋ ๋ ์ข ๋ฃ๋ฅผ ๋ง๊ธฐ ์ํด์ CompletableFuture๋ฅผ ์ฌ์ฉํ์ฌ ํต์ ์ด ์๋ฃ๋ ๋๊น์ง ๋๊ธฐํ๋๋ก ํ๋ค.
grpc-server
client์์ sayHello๋ฅผ ํธ์ถํ์ ๋ ์๋ฒ์์๋ ์ด๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํ ์๋น์ค๋ฅผ ๋ง๋ค์ด์ผ ํ๋ค.
import io.grpc.stub.StreamObserver;
import org.example.grpcinterface.HelloReply;
import org.example.grpcinterface.HelloRequest;
import org.example.grpcinterface.SimpleServiceGrpc;
import org.springframework.stereotype.Service;
@Service
public class GrpcSimpleService extends SimpleServiceGrpc.SimpleServiceImplBase {
@Override
public void sayHello( HelloRequest request, StreamObserver<HelloReply> responseObserver) {
// 1. ์์ฒญ ๊ฐ(request)์์ ์ด๋ฆ ๊บผ๋ด๊ธฐ
String name = request.getName();
String message = "Hello, " + name + "! Welcome to Spring gRPC 1.0 world.";
// 2. ์๋ต ๊ฐ์ฒด(Reply) ๋น๋ ํจํด์ผ๋ก ์์ฑ
HelloReply reply = HelloReply.newBuilder()
.setMessage(message)
.build();
// 3. ํด๋ผ์ด์ธํธ์๊ฒ ์๋ต ๋ณด๋ด๊ธฐ
responseObserver.onNext(reply);
// 4. ์๋ต ๋๋ฌ์์ ์๋ฆผ (ํ์!)
responseObserver.onCompleted();
}
}JavaFileUpload service
gRPC๋ฅผ ํตํด์ ํด๋ผ์ด์ธํธ์์ ์๋ฒ๋ก ํ์ผ์ ์ ๋ก๋ ํ๋ ์ํ์ ๊ตฌํํด ๋ดค๋ค.
grpc-client
GrpcClientConfig
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import org.example.grpcinterface.FileUploadServiceGrpc;
import org.example.grpcinterface.SimpleServiceGrpc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GrpcClientConfig {
// ์ฑ๋(Connection) ๋น ์์ฑ
@Bean
public ManagedChannel simpleChannel() {
// application.yml์ ์ค์ ์ ๊ฐ์ ธ์์ ์ธ ์๋ ์์ง๋ง,
// 1.0.0 ์ด๊ธฐ ์ค์ ์์๋ ๋ช
์์ ์ผ๋ก ๋น๋๋ฅผ ์ฐ๋ ๊ฒ์ด ํ์คํ๋ค.
return ManagedChannelBuilder.forAddress("localhost", 9090)
.usePlaintext() // ๊ฐ๋ฐ์ฉ: ํ๋ฌธ ํต์
.build();
}
// FileUpload ์คํ
(Stub) ๋น ์์ฑ
@Bean
public FileUploadServiceGrpc.FileUploadServiceStub fileUploadStub( ManagedChannel channel) {
return FileUploadServiceGrpc.newStub(channel);
}
}JavaFileUploadServiceStub๋ฅผ ๋น์ผ๋ก ์ ์ํ์๋ค. ์๋ฒ์ ํต์ ํ๋ ํด๋ผ์ด์ธํธ ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ๋ค.
import com.google.protobuf.ByteString;
import io.grpc.stub.StreamObserver;
import org.example.grpcinterface.FileInfo;
import org.example.grpcinterface.FileUploadRequest;
import org.example.grpcinterface.FileUploadResponse;
import org.example.grpcinterface.FileUploadServiceGrpc;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.CompletableFuture;
@Component
public class FileUploadRunner implements CommandLineRunner {
private final FileUploadServiceGrpc.FileUploadServiceStub fileUploadServiceStub;
public FileUploadRunner( FileUploadServiceGrpc.FileUploadServiceStub fileUploadServiceStub ) {
this.fileUploadServiceStub = fileUploadServiceStub;
}
@Override
public void run( String... args ) throws Exception {
System.out.println( "start file upload runner");
CompletableFuture<String> future = new CompletableFuture<>();
// ์๋ต์ ๋ฐ์ Observer ์ ์
StreamObserver<FileUploadResponse> responseObserver = new StreamObserver<>() {
// ์๋ฒ ์๋ต ์ฒ๋ฆฌ
@Override
public void onNext( FileUploadResponse fileUploadResponse ) {
System.out.println( "---- ์๋ฒ ์๋ต ์์ ----" );
System.out.println( "ํ์ผ๋ช
: " + fileUploadResponse.getFileName() );
System.out.println( "์ํ: " + fileUploadResponse.getStatus() );
System.out.println( "ํ์ผํฌ๊ธฐ: " + fileUploadResponse.getTotalSize() + " bytes" );
System.out.println( "---- ์๋ฒ ์๋ต ๋ ----" );
}
//์๋ฒ์์ ์๋ฌ ๋ฐ์์ ์ฒ๋ฆฌ
@Override
public void onError( Throwable t ) {
future.completeExceptionally( t );
}
//ํต์ ์๋ฃ ์ฒ๋ฆฌ
@Override
public void onCompleted() {
future.complete( "ํ์ผ ์
๋ก๋ ์๋ฃ!!" );
}
};
// ์๋ฒ์ ๋ฆฌ๋ชจ์ปจ์ ์ ๋ฌ ๋ฐ๊ณ ์๋ฒ์๊ฒ ๋์ ๋ฆฌ๋ชจ์ปจ์ ์ ๋ฌํ๊ธฐ
StreamObserver<FileUploadRequest> requestObserver = fileUploadServiceStub.uploadFile( responseObserver );
// ์
๋ก๋ ํ์ผ์ ๋ํ ๋ฉํ๋ฐ์ดํฐ ์ ์ก
ClassPathResource resource = new ClassPathResource( "test-image.jpg" );
Path path = Paths.get( "src/main/resources/test-image.jpg");
FileInfo fileInfo = FileInfo.newBuilder()
.setFileName( resource.getFilePath().getFileName().toString() )
.setFileType( "image/jpg" )
.build();
FileUploadRequest fileUploadRequest = FileUploadRequest.newBuilder()
.setInfo( fileInfo )
.build();
requestObserver.onNext( fileUploadRequest );
// file chunked data ์ ์ก
try ( InputStream fis = resource.getInputStream() ) {
//64kb์ฉ ์ ์ก
byte[] buffer = new byte[ 1024 * 64 ];
int byteRead;
while ( ( byteRead = fis.read( buffer ) ) != -1 ) {
// ByteString์ผ๋ก ๋ณํํ์ฌ ์ ์ก
FileUploadRequest chunkRequest = FileUploadRequest.newBuilder()
.setChunkData( ByteString.copyFrom( buffer, 0, byteRead) )
.build();
requestObserver.onNext( chunkRequest );
}
//์ ์ก ๋ ์๋ฆผ
requestObserver.onCompleted();
}
catch ( Exception e ) {
//์์ธ ๋ฐ์์ ์๋ฒ๋ก ์๋ฌ ์ ๋ฌ
requestObserver.onError( e );
future.completeExceptionally( e );
}
future.whenComplete( (result, throwable) -> {
if ( throwable != null ) {
System.out.println("์๋ฌ ๋ฐ์: " + throwable.getMessage());
}
else {
System.out.println(result);
}
} );
future.join();
}
}
Java์ ์ฝ๋๋ src/main/resources/test-image.jpg ํ์ผ์ gRPC๋ฅผ ์ด์ฉํ์ฌ ์๋ฒ๋ก ์ ์กํ๋ ์ฝ๋๋ค.
๋ณดํต ์ฐ๋ฆฌ๊ฐ ์๊ณ ์๋ ํธ์ถ ๋ฐฉ์์
StreamObserver responseObserver = fileUploadServiceStub.uploadFile( requestObserver );
๊ณผ ๊ฐ์ ํ์์ผ ๊ฒ์ด๋ค. ํ์ง๋ง ์๋์ ๊ฐ์ด ์ฐ๋ฆฌ๊ฐ ์๊ฐํ๋ ์์์๋ ๋ญ๊ฐ ์ข ๋ค๋ฅด๋ค.
// ์๋ฒ์ ๋ฆฌ๋ชจ์ปจ์ ์ ๋ฌ ๋ฐ๊ณ ์๋ฒ์๊ฒ ๋์ ๋ฆฌ๋ชจ์ปจ์ ์ ๋ฌํ๊ธฐ
StreamObserver requestObserver = fileUploadServiceStub.uploadFile( responseObserver );
์ ์ฝ๋๋ ์๋ฒ์๊ฒ ๋(client)์ ๋ฆฌ๋ชจ์ปจ(responseObserver)์ ์ ๋ฌํ๊ณ ์๋ฒ๋ ๋(client)์๊ฒ ์๋ฒ ์์ ์ ๋ฆฌ๋ชจ์ปจ(requestObserver) ์ ์ ๋ฌํ๋ค.
์ฆ client๋ requestObserver๋ฅผ ํตํด์ ์๋ฒ์๊ฒ ์์ฒญ์ ์ ๋ฌํ๊ณ ์๋ฒ๋ responseObserver๋ฅผ ํตํด์ client์๊ฒ ์๋ต์ ์ ๋ฌํ๋ค๋ ์๋ฏธ๋ค.
์ด ์ญ์ ๋น๋๊ธฐ๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ ๋ฉ์ธ ์ค๋ ๋์ ์ฐ์ ์ข
๋ฃ๋ฅผ ๋ง๊ธฐ ์ํด CompletableFuture๋ฅผ ์ฌ์ฉํ๋ค.
grpc-server
ํ์ผ ์ ๋ก๋ ์ฒ๋ฆฌ๋ฅผ ์ํ ์๋ฒ ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ๋ค.
import io.grpc.stub.StreamObserver;
import org.example.grpcinterface.FileInfo;
import org.example.grpcinterface.FileUploadRequest;
import org.example.grpcinterface.FileUploadResponse;
import org.example.grpcinterface.FileUploadServiceGrpc;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Service
public class GrpcFileUploadService extends FileUploadServiceGrpc.FileUploadServiceImplBase {
private static final String UPLOAD_DIR = "uploads/";
/**
* ํ์ผ ์
๋ก๋ ์ฒ๋ฆฌ๋ฅผ ์ํ ์๋น์ค
*
* @param responseObserver ์๋ฒ๊ฐ ํด๋ผ์ด์ธํธ์๊ฒ ์๋ต์ ์ฃผ๋ ๋ฆฌ๋ชจ์ปจ.
* @return StreamObserver<FileUploadRequest> ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์๊ฒ ์์ฒญ์ ํ๋ ๋ฆฌ๋ชจ์ปจ.
*/
@Override
public StreamObserver<FileUploadRequest> uploadFile( StreamObserver<FileUploadResponse> responseObserver ) {
// ์์ฒญ์ ์ฒ๋ฆฌํ ์ต๋ช
ํด๋์ค ๋ฐํ
return new StreamObserver<FileUploadRequest>() {
// ์ํ๊ฐ ์ ์ง (ํ์ผ ์คํธ๋ฆผ, ํ์ผ๋ช
, ์์ ํ์ผ ์ฌ์ด์ฆ)
private OutputStream writer;
private String fileName;
private long totalSize = 0L;
@Override
public void onNext( FileUploadRequest fileUploadRequest ) {
try {
// ์ฒ์ ์
๋ก๋ ๋์ ํ์ผ ๋ฉํ ์ ๋ณด ์์
if ( fileUploadRequest.hasInfo() ) {
FileInfo fileInfo = fileUploadRequest.getInfo();
this.fileName = fileInfo.getFileName();
//์ ์ฅ ๊ฒฝ๋ก ์์ฑ
Path uploadPath = Paths.get( UPLOAD_DIR );
if ( !Files.exists( uploadPath ) ) {
Files.createDirectories( uploadPath );
}
uploadPath = uploadPath.resolve( fileName );
this.writer = Files.newOutputStream( uploadPath );
System.out.println( "[Server] ํ์ผ ์์ฑ ์์: " + uploadPath );
}
//๋ฉํ ์ ๋ณด ์์ ์ดํ ํ์ผ ๋ฐ์ดํฐ๊ฐ ์ ์ก๋๋ค.
else if ( fileUploadRequest.hasChunkData() ) {
if ( writer == null ) {
throw new IOException( "file writer is not created." );
}
byte[] bytes = fileUploadRequest.getChunkData().toByteArray();
writer.write( bytes );
writer.flush();
totalSize += bytes.length;
}
}
catch ( IOException e ) {
this.onError( e );
}
}
@Override
public void onError( Throwable throwable ) {
System.err.println( "์๋ฌ ๋ฐ์: " + throwable.getMessage() );
closeFile();
}
@Override
//ํด๋ผ์ด์ธํธ๊ฐ ๋ค ๋ณด๋ด๋ฉด ํธ์ถ๋จ
public void onCompleted() {
closeFile();
System.out.println( "[Server] ์
๋ก๋ ์๋ฃ. ํ์ผ ํฌ๊ธฐ: " + this.totalSize );
//์๋ต ๋ณด๋ด๊ธฐ
FileUploadResponse respons = FileUploadResponse.newBuilder()
.setFileName( this.fileName )
.setTotalSize( this.totalSize )
.setStatus( "SUCCESS" )
.build();
responseObserver.onNext( respons );
responseObserver.onCompleted();
}
private void closeFile() {
try {
if ( writer != null ) writer.close();
}
catch ( IOException e ) {
e.printStackTrace();
}
}
};
}
}
Java์๋ฒ์ ํด๋ผ์ด์ธํธ๊ฐ์ ํ์ผ ์ ๋ก๋ ํต์ ํ๋ฆ์ ๋ค์๊ณผ ๊ฐ๋ค.
ํด๋ผ์ด์ธํธ ์๋ฒ
------------------>
connection
<----------------->
์๋ก๊ฐ์ ๋ฆฌ๋ชจ์ปจ ๊ตํ //ํด๋ผ์ด์ธํธ๋ ์๋ฒ ๋ฆฌ๋ชจ์ปจ์ผ๋ก ์๋ฒ์๊ฒ ์์ฒญ ์ ํธ ์ ๋ฌ.
//์๋ฒ๋ ํด๋ผ์ด์ธํธ ๋ฆฌ๋ชจ์ปจ์ผ๋ก ํด๋ผ์ด์ธํธ์๊ฒ ์๋ต ์ ํธ ์ ๋ฌ
//์ญํ ์ ํ๊ธฐ ์ํด ์๋ก์ ๋ฆฌ๋ชจ์ปจ์ ๊ตํํ๋ค.
------------------>
FileInfo ์ ์ก (์๋ฒ๊ฐ ์ค ๋ฆฌ๋ชจ์ปจ์ผ๋ก onNext)
------------------>
ํ์ผ chunked data ์ ์ก (์๋ฒ๊ฐ ์ค ๋ฆฌ๋ชจ์ปจ์ผ๋ก onNext) (ํ์ผ ๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ ์ฝ์ ๋๊น์ง)
------------------>
ํ์ผ ์ ์ก ๋ ์ด๋ฒคํธ ์ ์ก (์๋ฒ๊ฐ ์ค ๋ฆฌ๋ชจ์ปจ์ผ๋ก onCompleted)
<-----------------
์๋ต ์ ์ก (ํด๋ผ์ด์ธํธ๊ฐ ์ค ๋ฆฌ๋ชจ์ปจ์ผ๋ก onNext)
<-----------------
์๋ฃ ์ ์ก (ํด๋ผ์ด์ธํธ๊ฐ ์ค ๋ฆฌ๋ชจ์ปจ์ผ๋ก onCompleted)
Plaintext์ ์ฒด ์ฝ๋๋
https://gitlab.com/blog4031530/spring-boot-grpc ์์ ํ์ธ ํ ์ ์๋ค.
