Spring 7.X (Spring Boot 4.X) gRPC 1.0.0 ์ •์‹ ๋ฆด๋ฆฌ์ฆˆ ๊ทธ๋ฆฌ๊ณ  gRPC ์‚ฌ์šฉ๋ฒ•

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>
...
XML

gRPC 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'
...
Groovy

dependencyManagement ์„น์…˜์˜ 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'
Groovy

settings.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()
    }
}
Groovy

grpc-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'
Groovy

grpc-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'
            }
        }
    }
}
Groovy
proto ํŒŒ์ผ ์ •์˜

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;
}
Groovy

gRPC๋ฅผ ํ†ตํ•ด ํŒŒ์ผ ์—…๋กœ๋“œ๋ฅผ ์œ„ํ•ด์„œ ์ •์˜ํ•˜์˜€๋‹ค. 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 proto ์ •์˜ ๋นŒ๋“œ ๊ฒฐ๊ณผ
grpc-interface ๋นŒ๋“œ ๊ฒฐ๊ณผ

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);
    }
}
Java

grpc-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();
    }
}
Java

gRPC๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋น„๋™๊ธฐ๋กœ ๋™์ž‘์„ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— 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();
    }
}
Java

FileUpload 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);
    }
}
Java

FileUploadServiceStub๋ฅผ ๋นˆ์œผ๋กœ ์ •์˜ํ•˜์˜€๋‹ค. ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

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 ์—์„œ ํ™•์ธ ํ•  ์ˆ˜ ์žˆ๋‹ค.