grpc로 통신하기 및 REST와의 비교

gRPC란

gRPC는 google에서 개발한 RPC(Remote Procedure Call) 프레임워크이다. RPC는 분산 시스템에서 프로시저 호출을 위한 프로토콜을 의미한다. gRPC는 Protocol Buffers라는 바이너리 직렬화 포맷을 사용해 효율적이고 경량화된 네트워크 통신을 지원하며, 대규모 분산 시스템에서 서비스 간의 통신을 구현할 때 사용된다.

이전에 주로 REST를 사용하다 gRPC를 사용하게 되며 알게 된 것들을 정리해보려 한다.

gRPC와 REST와의 비교

둘 다 웹 서비스를 구축하기 위한 프로토콜이지만 다음과 같은 차이가 있다.

gRPC

  • 프로토콜: Protocol Buffers를 사용해 통신하며, 기본적으로 바이너리 프로토콜인 HTTP/2를 사용한다.
  • 데이터 형식: Protocol Buffers를 사용해 데이터를 직렬화한다. Protocol Buffers는 이진 형식으로 데이터를 효율적으로 직렬화하고 구조화하는데 사용되고 다양한 언어에서 코드를 생성할 수 있다.
  • 지원 언어: 다양한 언어에서 사용할 수 있다. Google이 gRPC를 지원하는 공식 언어는 C++, Java, Python, Go, Ruby, C#, Node.js, Android Java, Objective-C, PHP, Dart, Kotlin, JavaScript (Browser와 Node.js) 등이 있다.
  • 요청 방식: 기본적으로 단방향, 양방향 스트리밍, 단일 요청-응답과 같은 다양한 요청 방식을 지원하며, 서버와 클라이언트 간의 실시간 통신에 적합하다.
  • 성능: 높은 성능을 제공하고, HTTP/2의 다중화 기능을 활용함으로써 여러 개의 요청을 동시에 처리할 수 있으며, 헤더 압축, 서버 푸시 등의 기능으로 성능을 최적화할 수 있다.
  • 프로토콜 지원: Protocol Buffers를 기반으로 해 프로토콜 중립적이며 HTTP/2 외에도 다른 전송 프로토콜을 사용할 수 있다.

REST

  • 프로토콜: Representational State Transfer의 약자로, 웹 아키텍처 스타일이다. REST는 주로 HTTP를 기반으로 하는 프로토콜을 사용하며, 주로 JSON 또는XML과 같은 형태로 데이터를 주고받는다.
  • 데이터 형식: 주로 JSON과 XML 등의 텍스트 형식을 사용해 데이터를 전송한다.
  • 지원 언어: 언어에 종속되지 않아 어떤 언어나 HTTP를 통해 RESTful 서비스에 요청을 보낼 수 있다.
  • 요청 방식: 주로 단일 요청-응답 방식을 사용하고, 클라이언트는 HTTP 요청을 통해 서버에 리소스를 요청하고, 서버는 해당 리소스를 반환한다.
  • 성능: HTTP 프로토콜을 기반으로 하며, 주로 요청-응답 방식을 사용한다. HTTP의 간단한 특성을 활용해 상대적으로 더 가벼운 트래픽이나 요청에 적합하다.
  • 프로토콜 지원: HTTP를 기반으로 해 HTTP와 함께 동작하고, 다른 전송 프로토콜을 사용하려면 변환 계층이 필요하다.

react에서 gRPC 호출하기

@bufbuild/connect-web

  • buf.build에서 제공하는 gRPC-Web 클라이언트 라이브러리이다.
  • 이 라이브러리를 사용하면 JS/TS로 개발하는 프론트엔드 애플리케이션에서 gRPC-Web 서비스와 통신할 수 있다.

@bufbuild/connect-web 에서 다음의 함수와 객체를 사용해 통신한다.

createGrpcWebTransport

  • gRPC-Web을 사용해 gRPC 통신을 하기 위한 transport를 생성하는 함수이다.
  • baseUrl 등의 파라미터를 받아 웹 transport 객체를 생성하고, 이 객체를 gRPC 클라이언트와 함께 사용해 서버와의 통신을 처리한다.

createPromiseClient

  • gRPC 클라이언트의 비동기 메서드를 Promise 기반으로 사용하기 위한 함수이다.

CallOptions

  • gRPC에서 호출 옵션을 설정하는 객체로, 이 객체를 이용해 요청에 대한 설정, 메타데이터, 타임아웃 등을 제어할 수 있다.

다음은 실존하지 않는 임의의 데이터를 gRPC로 호출하는 예시이다. https://grpc.foo.com url에서 호출할 수 있는 PaymentService에 속해있는 listPaymentItem를 호출한다. 멤버 토큰을 헤더에 넣어 호출하고, 토큰을 얻는 함수는 utils 폴더에 있다고 가정했다.

다음과 같이 통신을 위한 transport를 생성하고, Promise 기반의 클라이언트를 사용해 통신을 할 수 있다.

import { createGrpcWebTransport, createPromiseClient } from '@bufbuild/connect-web'
import type { CallOptions } from '@bufbuild/connect-web'
import { getMemberToken } from '@src/utils'
// ...

// header에 넣은 멤버 토큰을 가져오는 코드이다.
const memberToken = getMemberToken()

// 사용하는 grpc url를 넣어 gRPC 통신을 위한 transport를 생성한다.
const transport = createGrpcWebTransport({
  baseUrl: "https://grpc.foo.com"
})

// 호출에 필요한 옵션을 공통으로 정의한다.
const options: CallOptions = {
	header: {
    'Content-Type': 'application/json',
		Authorization: memberToken
  }
}

// Promise 기반의 클라이언트를 생성한다.
const client = createPromiseClient(PaymentService, transport)

// PaymentItems에 있는 listPaymentItems에 ListPaymentRequest type의 파라미터를 전달해 호출한다. 
const listPaymentItems = (param: ListPaymentRequest) => 
	client.listPaymentItems(
		{ param },
		options
	)

예시 작성을 위해 하나의 파일로 작성했지만 공통으로 사용되는 transport와 options 등은 common.ts 같은 이름의 파일에 분리해서 사용하곤 했다.

그 외

이전에 REST 사용 시에는 api 호출에 필요한 Request와 Response 타입을 직접 정의해서 사용하곤 했었는데, 타입 변경이 있을 경우 공유가 없었을 경우에는 에러 발생 시 이유를 알지 못해 헤매기도 했었다. 하지만 gRPC를 사용하니 buf 버전 업데이트만 하면 최신화된 proto를 알 수 있어 편했다. 그리고 이전에는 문서화를 직접 해서 문서를 항상 최신 상태로 유지하는 데 어려움이 있었는데, https://buf.build/ 에서 문서를 볼 수 있어 좋았다. 공통으로 사용하는 proto인 경우에는 연관되어 있는 buf를 업데이트하지 않으면 문서와 상이한 경우도 있었지만 아주 드문 경우여서 거의 최신의 상태를 확인할 수 있는 것이 장점이다.


Written by@jaeeun
I explain with words and code. I explain with words and code. I explain with words and code.