개요
gRPC는 위에서의 설명과 같이 모든 환경에서 실행할 수 있는 고성능 RPC(원격 프로시저 호출) 프레임워크입니다. 구글에서 개발해서 g를 접두사로 붙였습니다. 원격 프로시저 호출이라는 의미는 다른 서버의 함수(프로시저)를 내 서버에서 실행할 수 있다는 의미이고 gRPC는 이를 지원하는 프레임워크입니다.
장점
- HTTP/2와 Protobuf 등을 사용하여 빠르고 효율적인 통신. 특히 대용량 데이터 전송 시 이점이 있음
- 서버와 클라이언트의 양방향 스트리밍을 지원하여 실시간 데이터 처리에 용이
- 거의 모든 환경에서 사용 가능
단점
- 디버깅이 어려움(Protobuf는 바이너리 방식)
- 기본적인 브라우저 환경에서는 사용이 어려움. 추가적인 라이브러리 필요.
예시(Server)
이 글에서는 자주 사용하는 Web 서버의 상황을 예를 들기 위해 Spring Boot, Kotlin, gradle을 사용하는 환경을 기준으로 잡았습니다.
이후에는 Armeria 프레임워크를 사용하거나 Kotlin만을 사용해서 처리하는 방식도 알아보겠습니다.
참고: https://grpc.io/docs/languages/kotlin/quickstart/
아래 예시는 Interface, Server, Client로 나누어 작성하였으며 전부 하나의 모듈에 작성하였습니다.
interface : proto를 작성하고 관리하는 모듈
Interface
build.gradle.kts
plugins {
id("com.google.protobuf") version "0.9.4"
}
// Version 변수는 알맞게 추가합니다.
dependencies {
implementation("io.grpc:grpc-protobuf:$grpcProtoVersion")
implementation("io.grpc:grpc-kotlin-stub:$grpcKotlinVersion")
implementation("com.google.protobuf:protobuf-kotlin:$grpcProtoKotlinVersion")
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:$grpcProtoKotlinVersion"
}
plugins {
id("grpc") {
artifact = "io.grpc:protoc-gen-grpc-java:$grpcProtoVersion"
}
id("grpckt") {
artifact = "io.grpc:protoc-gen-grpc-kotlin:$grpcKotlinVersion:jdk8@jar"
}
}
generateProtoTasks {
all().forEach { generateProtoTask ->
generateProtoTask.plugins {
id("grpc")
id("grpckt")
}
generateProtoTask.builtins {
id("kotlin")
}
}
}
}
위의 내용을 build.gradle.kts에 추가합니다. gRPC를 통해 사용할 함수 내용을 작성하기 위한 부분만 포함하고 있습니다.
만약 위에서 "id()"를 찾지 못한다는 에러가 발생한다면 protobuf 부분을 주석처리 후 플러그인 부터 로드하시면 해결됩니다.
.proto 파일 작성
syntax = "proto3";
package com.marrrang.grpc.lib;
option java_multiple_files = true;
option java_package = "com.marrrang.grpc.lib";
option java_outer_classname = "MarrrangProto";
service Marrrang {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
간단하게 작성한 .proto 파일입니다. 위치는 src.main.kotlin.proto 하위에 작성하였습니다.
위에서 사용한 option에 대해서 추가적인 정보는 여기에서 찾아보실 수 있습니다.
gradle build
./graldew build
성공적으로 build 된다면 build.generated.source.proto.main 폴더 하위에 java, kotlin 폴더에서 저희가 만든 함수 내용을 확인할 수 있습니다.
Server
Interface 내용과 동일한 모듈에서 이후 내용도 진행합니다.
build.gradle.kts
dependencies {
implementation("net.devh:grpc-spring-boot-starter:$version")
}
sourceSets {
getByName("main") {
java {
srcDirs(
"build/generated/source/proto/main/java",
"build/generated/source/proto/main/kotlin"
)
}
}
}
sourceSets : 소스 작성 시에 build한 내용을 참조하여 작성 가능하도록 명시해주는 부분
application.yaml
grpc:
server:
port: 9090
grpc 서버에 대한 설정을 추가합니다.
grpcService 작성
import com.marrrang.grpc.lib.HelloReply
import com.marrrang.grpc.lib.HelloRequest
import com.marrrang.grpc.lib.MarrrangGrpc
import io.grpc.stub.StreamObserver
import net.devh.boot.grpc.server.service.GrpcService
@GrpcService
class GrpcServerService: MarrrangGrpc.MarrrangImplBase() {
override fun sayHello(req: HelloRequest, responseObserver: StreamObserver<HelloReply?>) {
val reply: HelloReply = HelloReply.newBuilder()
.setMessage("Marrrang Hello : " + req.name)
.build()
responseObserver.onNext(reply)
responseObserver.onCompleted()
}
}
위에서 com.marrrang.grpc.lib에서 import한 내용이 .proto 파일에서 정의한 내용으로 만들어진 것입니다.
// .proto 파일
service Marrrang {
...
}
// build 결과물
MarrrangGrpc {
...
}
build 결과물에는 service로 지정한 이름 뒤에 Grpc가 붙어서 클래스가 만들어졌습니다. 이 클래스 하위에 있는 구현체 Base를 상속받아서 서비스 로직을 작성하게 됩니다.
위 내용까지 작성 후 실행하고 PostMan을 사용하여 요청을 보내보면 아래와 같은 결과를 받을 수 있습니다.
Client
Client 부분은 조금 더 활용하기 위해 Spring Web을 추가하여 gRPC로의 직접 호출 이외에도 호출 가능한 부분을 만들려고 합니다.
build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:3.2.4")
}
ClientService 작성
import com.marrrang.grpc.lib.HelloRequest
import com.marrrang.grpc.lib.MarrrangGrpc
import net.devh.boot.grpc.client.inject.GrpcClient
import org.springframework.stereotype.Service
import java.util.concurrent.CompletableFuture
@Service
class GrpcClientService {
@GrpcClient("marrrangGrpcClient")
private lateinit var asyncStub: MarrrangGrpc.MarrrangFutureStub
fun sayHello(name: String): CompletableFuture<String> {
val request = HelloRequest.newBuilder().setName(name).build()
val asyncResponse = asyncStub.sayHello(request)
val completableFuture = CompletableFuture<String>()
asyncResponse.addListener(
{
try {
val response = asyncResponse.get() // 비동기 결과 가져오기
completableFuture.complete(response.message)
} catch (e: Exception) {
completableFuture.completeExceptionally(e) // 예외 처리
}
},
{ runnable -> runnable.run() }
)
return completableFuture
}
}
비동기 처리를 기반으로 했습니다.
Controller 작성
import com.marrrang.grpcexample.service.GrpcClientService
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.util.concurrent.CompletableFuture
@RestController
class GrpcExampleController(private val grpcClientService: GrpcClientService) {
@GetMapping("/sayHelloAsync")
fun sayHelloAsync(@RequestParam name: String): CompletableFuture<String> {
val responseFuture = grpcClientService.sayHello(name)
return responseFuture
}
}
Application.yaml 내용 추가
grpc:
server:
port: 9090
client:
marrrangGrpcClient:
address: "static://localhost:9090"
negotiation-type: plaintext
여기까지 설정 후 호출해보면 정상 처리되는 것을 볼 수 있습니다.
'Web' 카테고리의 다른 글
[Web] 성능테스트에 대하여(with K6) (1) | 2024.11.29 |
---|---|
[Ktor] Ktor 소소하게 알아보기 - Server 방식 (0) | 2022.01.08 |
TDD와 BDD (0) | 2020.11.30 |