gRPC

등장배경

1. Server-Client Model

하나의 메인 프레임에서 동작하는 Monolothic 구조로 설계 시절 네트워크 통신 중요 X
고가인 메인 프레임워크 → 비교적 저가의 워크스테이션 서버로 대체하고 싶어함
But, 메인 프레임워크의 초고양 사양 그대로 제공하는데 한계
⇒ 메인 프레임워크 기능 → 워크스테이션 서버로 분산 + 네트워크 연결로 서비스하는 방식 채택
= Server-Client Model
⇒ 서버-서버, 서버-개인PC 간 네트워크 연결 및 통신 중요
→ OSI 7 layer, TCP/IP 등 네트워크 계층 구조 정의, 발전

2. IPC

프로세스는 기본적으로 상호독립적
메모리 공유 X → 각자 자신의 일만 하고 서로 간섭 X
But, 필요에 따라 프로세스 간 정보를 교환해야 하는 경우
→ 별도의 수단 이용하여 프로세스 통신하는 방법론 = IPC(Inter Process Communication)

1) Socket

OSI 7 layer 구조의 Application Layer(L7)에서 Transport Port(L4)의 TCP or UDP를 이용하기 위한 수단
일종의 창구 역할
일련의 통신 과정 직접 구현 → 통신 관련 장애 처리 ⇒ 개발자의 몫
서비스 고도화 → data formatting 하는 것 어려워짐 ⇒ RPC

2) RPC(Remote Procedure Call)

네트워크로 연결된 서버 상의 프로시저(함수, 메서드 등) 원격으로 호출할 수 있는 기능
네트워크 통신을 위한 작업 챙기지 X 통신이나 call 방식 신경 X 원격자의 자원을 내 것처럼 사용 가능
IDL(Interface Definication Language) 기반으로 다양한 언어를 가진 환경에서도 쉽게 확장 가능
인터페이스 협업에도 용이
지원 언어: C++, Java, Python, Ruby, Node.js, C#, Go, PHP, Objective-C…
서버-클라이언트는 서로 다른 주소 공간 사용 → 함수 호출에 사용된 매개 변수를 꼭 변환해줘야 함
→ 메모리 매개 변수에 대한 포인터가 다른 데이터를 가리키게 될 수 있기 때문
이러한 변환 담당: 스텁(Stub)
client stub
함수 호출에 사용된 파라미터의 변환(Marshalling, 마샬링) 및 함수 실행 후 서버에서 전달된 결과 반환
server stub
클라이언트가 전달한 매개 변수의 역변환(Unmarshalling, 언마샬링) 및 함수 실행 결과 변환 담당
RPC 통신 과정
1.
IDL(Interface Definition Language) 사용 → 호출 규약 정의
a.
함수명, 인자, 반환값에 대한 데이터형이 정의된 IDL 파일 → rpcgen으로 컴파일 ⇒ stub code가 자동으로 생성
2.
Stub Code에 명시된 함수: 원시코드의 형태, 상세 기능: server에서 구현됨
a.
만들어진 stub 코드 → 클라이언트/서버와 함께 빌드
3.
client에서 stub에 정의된 함수 사용시
4.
client stub) RPC runtime ~> 함수 호출
5.
server) 수신된 procedure 호출에 대한 처리 후 결과값 반환
6.
최종적으로 client는 server의 결과값 반환받고, 함수를 local에 있는 것처럼 사용 가능
⇒ 원격지의 것을 끌어다 로컬처럼 사용,
구현체 CORBA, RMI 등 여러가지
→ 로컬에서 제공하는 빠른 속도, 가용성 등 분산 프로그래밍에서도 제공
But, 구현의 어려움/지원 한계 등 → 제대로 활용 X
⇒ 데이터 통신을 익숙한 Web을 활용해보려는 시도 → REST

3) REST(REpresentational State Transfer)

HTTP/1.1 기반으로 URI ~> 자원(resource) 명시 + HTTP Method ~> 처리하는 아키텍처
자원 그 자체를 표현 ⇒ 직관적, HTTP 그대로 계승 → 별도 작업 X 쉽게 사용할 수 있음
But, 일종의 스타일이지 표준 X → parameter와 응답값 명시적 X,
HTTP 메소드 형태 제한적 → 세부 기능 구현에는 제약
+XML, JSON
html과 같이 tag 기반이지만 미리 정의된 태그 X(no pre-defined tags) → 높은 확장성 인정
But, 복잡, 비효율적인 데이터 구조 → 속도 느림
⇒ JSON이 간결한 key-value 구조 기반으로 해결하는 듯 했지만
제공되는 자료형의 한계로 파싱 후 추가 형변환이 필요한 경우 많아짐
두 타입 모두 string 기반이라 사람이 읽기 편하지만, 데이터 전송 및 처리 → 별도의 serialization 필요

gRPC(Google Remote Procedure Call)

goolge 사에서 개발한 오픈소스 RPC 프레임워크
이전까지는 RPC 기능 지원 X, 메시지(JSON 등)을 serialize할 수 있는 프레임워크인 PB(Protocol Buffer, 프로토콜 버퍼)만 제공
⇒ PB 기반 serializer + HTTP/2 결합
REST와 비교: 기반 기술 다름, HTTP/2 사용, 프로토콜 버퍼로 데이터 전송
→ Proto File만 배포하면 환경과 프로그램 언어에 구애 X 서로 간의 데이터 통신 가능

1. HTTP

HTTP/1.1
클라이언트의 요청이 올 때만 서버가 응답을 하는 구조 → 매 요청마다 connection 생성해야 함
cookie 등 많은 메타 정보를 저장하는 무거운 header 요청마다 중복 전달 ⇒ 비효율, 느린 속도
HTTP/2
한 connection으로 동시에 여러 개 메시지 주고 받음
header 압축 → 중복 제거 후 전달 ⇒ 효율적
필요 시 클라이언트 요청 X 서버가 리소스를 전달 할 수 있음 ⇒ 클라이언트 요청 최소화

2. ProtoBuf(Protocol Buffer, 프로토콜 버퍼)

google 사에서 개발한 구조화된 데이터를 직렬화(serialization)하는 기법
직렬화: 데이터 표현을 바이트 단위로 변환하는 작업
text 기반인 json: 82 byte 소요
직렬화 된 protocol buffer
필드 번호, 필드 유형 등 → 1 byte 받아서 식별, 주어진 length 만큼만 읽도록 해 33 byte 필요

3. Proto File

1) Message and Field

주고 받는 data → message라는 것으로 정의
메시지는 여러가지 타입의 필드로 구성
이름은 CamelCase 형태, field 이름은 under_bar 형태로 사용 권장
field 이름은 숫자로 시작 X

2) Package

message type 이름을 중첩없이 구분할 때 사용

3) Service

RPC ~> 서버가 클라이언트에게 제공할 함수의 형태를 정의
서비스명과 RPC 메소드명 모두 CamelCase 권장
옵션 X → 단일 요청/응답으로 동작, stream 옵션 → RPC 구현할 수 있음

gRPC와 서버 간 통신

고성능과 효율성 요구할 때 많이 사용 ⇒ 마이크로서비스 아키텍처 or 분산 시스템에서 많이 사용
바이너리 프로토콜
protobuf라는 바이너리 직렬화 방식 사용 → JSON or XML 같은 텍스트 기반 포맷에 비해 데이터 전송 속도 빠름
HTTP/2
다중 요청/응답 스트림, 멀티플렉싱, 헤더 압축 등 기능 제공 → 클라이언트-서버 효율적인 연결 가능하게 함
양방향 스트리밍
서버와 클라이언트 동시에 메시지 주고받을 수 있어 실시간 데이터 통신이 필요한 서비스에 적합
엄격한 타입 검사
proto 파일 ~> 명확한 스키마 정의 ⇒ 서버 간에 주고받는 데이터 타입을 컴파일 시점에 검증할 수 있음 ⇒ 타입 안정성이 보장

사용 사례

마이크로서비스 간의 통신
여러 개의 서비스가 상호 통신해야 하는 경우가 많은데 서버 간 데이터 전송 빈번하고, 속도와 성능 중요한 경우 사용
ex. 추천 시스템에서 사용자 데이터를 수집하는 서비스, 이를 기반으로 분석을 수행하는 서비스 간 통신
실시간 스트리밍
HTTP/2 기반의 스트리밍 지원 → 실시간으로 대규모 데이터를 주고받는 서비스에 적합
ex. 비디오 스트리밍, 채팅 시스템, 실시간 게임 서버 간 통신
다중 요청/응답 처리
HTTP/2 멀티플렉싱 기능 활용 → 다중 요청 동시에 처리할 수 있음
클라우드 서비스 간 통신
서버 간 통신 최적화
ex. Goole Cloud, Kubernetes 등 클라우드 네이티브 환경
사용 이유
적합한 상황
gRPC
- 속도 - 스트리밍 지원 - 엄격한 타입
- 고성능과 효율성 중요 - 실시간 스트리밍 필요한 경우 - 마이크로서비스 통신
HTTP
- 호환성 - 사용 용이성 - 텍스트 기반
- 서로 다른 언어 or 플랫폼에서 접근해야 하는 경우 - 브라우저 기반 통신 (대부분 gRPC 지원 X)

단점

브라우저 지원 제한
gRPC-Web이라는 프로토콜 사용 or 프록시 서버(Nginx, Envoy 등) 사용
복잡성 증가
바이너리 형식의 Protocol Buffers 사용 → 추가적인 설정과 학습 필요
텍스트 디버깅 어려움
바이너리 기반 직렬화 사용 → 데이터를 사람이 쉽게 읽거나 디버깅하기 어려움
protoc 도구 사용 → 메시지 디코딩
Postman과 같은 gRPC 클라이언트 사용
호환성 문제
메시지 크기 제한
기본 설정에서 최대 메시지 크기 제한적 → 대용량 데이터 전송시 값을 변경해야 함
(해결) gRPC 스트리밍 기능 사용 → 데이터 분할
클라이언트-서버 강한 결합
동일한 프로토콜 정의(.proto 파일) 사용해야 함
표준적인 인증 및 보안 모델 부족
(해결) TLS(전송 계층 보안) ~> 보안 설정 or OAuth 같은 기존 인증 방식 추가로 적용

사용하는 기업

실습

파일 구조

# grpc-client - src - main - java - com.example.grpc.client - TestGrpcClient.java - ClientController.java - resources - application.yml - proto - pb - svc - test - test.proto - unit - common - msg.proto - item - item.proto
Markdown
복사
# grpc-server - src - main - java - com.example.grpc.server - TestService.java - resources - application.yml - proto - pb - svc - test - test.proto
Markdown
복사

build.gradle

buildscript { ext { protobufVersion = '3.25.1' protobufPluginVersion = '0.8.14' grpcVersion = '1.58.1' } } plugins { id 'java' id 'org.springframework.boot' version '3.3.4' id 'io.spring.dependency-management' version '1.1.6' id 'com.google.protobuf' version '0.9.4' } group = 'com.example' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' // gRPC 관련 의존성 implementation "com.google.protobuf:protobuf-java-util:3.25.1" implementation 'com.google.protobuf:protobuf-java:3.25.1' // grpc-client 프로젝트에서는 아래 server을 client로 수정 implementation 'net.devh:grpc-server-spring-boot-starter:2.15.0.RELEASE' runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" compileOnly 'org.apache.tomcat:annotations-api:6.0.53' } //tasks.named('test') { // useJUnitPlatform() //} protobuf { protoc { artifact = "com.google.protobuf:protoc:${protobufVersion}" } clean { delete generatedFilesBaseDir } plugins { grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } } generateProtoTasks { all()*.plugins { grpc{} } } }
Java
복사

Proto

.proto 파일은 gRPC 서비스의 계약을 정의하는 파일
클라이언트-서버: 어떤 서비스와 메시지가 사용되는지에 대한 명세 제공

test.proto

syntax = "proto3"; // .proto 파일의 패키지 네임스페이스 정의, 패키지 ~> 메시지와 서비스 구분 package pb.svc.test; // Java 코드 생성 시, .proto 파일에서 생성된 클래스들이 패키지에 속하도록 설정 option java_package="com.example.pb.svc.test"; // message 및 service가 별도의 Java 파일로 생성되도록 함 option java_multiple_files=true; // 외부의 .proto 파일(msg.proto) 가져와 사용할 수 있게 함 import "pb/unit/common/msg.proto"; // 클라이언트로부터 GreetingReq 메시지 받아 GreetingRes 메시지로 응답 service Test { rpc Greeting(GreetingReq) returns (GreetingRes); } // 클라리언트 -> 서버: 요청 메시지 message GreetingReq { string some = 1; } // 서버 -> 클라이언트: 응답 메시지 message GreetingRes { unit.common.ReturnMsg result = 1; }
Protobuf
복사

msg.proto

syntax = "proto3"; // .proto 파일의 패키지 정의, 다른 .proto 파일들에서 ReturnMsg 메시지 가져다 사용할 수 있음 package unit.common; option java_package="com.example.pb.unit.common"; option java_multiple_files=true; message ReturnMsg { string message = 1; // 응답시 사용하는 메시지 int32 code = 2; // 응답의 상태 코드(200: 성공, 400: 실패) }
Protobuf
복사

Client

동기 방식의 gRPC 호출을 통해 서버로부터 데이터를 수신한 후, 결과를 반환하는 방식
1.
@GrpcClient ~> 서버와 연결, 클라이언트 스텁(TestBlockingStub) → 서버로 요청 전송
2.
GreetingReq 메시지를 생성하고 서버로 보내면, GreetingRes 메시지 ~> 서버 응답 받음
// application.yml spring: application: name: grpc-client server: port: 8080 grpc: client: test: address: ${TEST_ADDR:localhost:9090} enable-keep-alive: true keep-alive-without-calls: true negotiation-type: plaintext
Java
복사

TestController

package com.example.grpcclient; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor public class TestController { private final TestService testService; @GetMapping("/greet") public String greet(@RequestParam String name) { return testService.sendGreeting(name); } }
Java
복사

TestService

package com.example.grpcclient; import com.example.pb.svc.test.*; import lombok.RequiredArgsConstructor; import net.devh.boot.grpc.client.inject.GrpcClient; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class TestService { @GrpcClient("test") private TestGrpc.TestBlockingStub testStub; public String sendGreeting(String message) { GreetingReq request = GreetingReq.newBuilder() .setSome(message) .build(); GreetingRes response = testStub.greeting(request); return response.getResult().getMessage(); } }
Java
복사
TestBlockingStub은 =동기식 호출을 지원하는 클라이언트 스텁
서버로부터 응답을 받을 때까지 대기하는 방식

Server

클라이언트로부터 요청 받고, 응답 메시지 생성해 반환하는 역할 수행
@GrpcService 어노테이션 ~> gRPC 서버로서 동작
// application.yml spring: application: name: grpc-server main: web-application-type: none grpc: server: port: 9090
Java
복사

TestService

package com.example.grpc_server; import com.example.pb.svc.test.*; import com.example.pb.unit.common.ReturnMsg; import io.grpc.stub.StreamObserver; import net.devh.boot.grpc.server.service.GrpcService; @GrpcService public class TestService extends TestGrpc.TestImplBase{ @Override public void greeting(GreetingReq request, StreamObserver<GreetingRes> responseObserver) { String received = request.getSome(); System.out.println("Received: " + received); // 응답 메시지 생성 ReturnMsg returnMsg = ReturnMsg.newBuilder() .setMessage("Hello, " + received) .setCode(200) .build(); GreetingRes response = GreetingRes.newBuilder() .setResult(returnMsg) .build(); // 클라이언트로 응답 전송 responseObserver.onNext(response); responseObserver.onCompleted(); } }
Java
복사