gRPC를 배워보자 1일차 - gRPC란

gRPC

목록 보기
1/6

gRPC란

마이크로서비스가 보급됨에 따라 서버 간의 통신이 활발히 이루어져야 하는데, 기존의 통신 방식인 RESTful design의 경우는 서버 간의 통신이 활발한 마이크로서비스 상에서 부하가 크고 느린 단점이 있다. 이러한 문제를 해결하고 분산 application을 구축하기 위한 최신 process 간의 통신 스타일이 바로 gRPC이다. gRPC는 주로 통신에 동기식 요청 응답 스타일을 사용하지만 초기 통신이 설정되면 완전 비동기 또는 스트리밍 모드에서 작동할 수 있다.

gRPC는 분산된 이기종 application의 함수를 로컬 함수처럼 쉽게 연결, 호출, 작동 및 디버그 할 수 있는 process 간 통신 기술이다. gRPC application을 개발할 때 가장 먼저 해야할 일은 서비스 인터페이스를 정의하는 것이다. 서비스 인터페이스 정의에는 consumer가 서비스를 소비하는 방법, consumer가 원격으로 호출할 수 있는 메서드, 해당 메서드를 서비스 정의에 지정하는 언어를 인터페이스 정의 언어(IDL)라고 한다.

해당 서비스 정의를 사용하여 낮은 수준의 통신 추상화를 제공하여 서버 측 로직을 단순화하는 서버 skeleton이라는 서버 측 코드를 생성할 수 있다. 또한, client stub이라는 client 측 코드를 생성할 수 있는 데, 이는 추상화를 통해 클라이언트 측 통신을 단순화하여 다른 프로그래밍 언어에 대한 저수준 통신을 숨기는 역할을 한다. 서비스 인터페이스 정의에서 지정한 메서드는 클라이언트 측에서 로컬 함수를 호출하는 것처럼 쉽게 원격으로 호출할 수 있다. 기본 gRPC 프레임워크는 일반적으로 엄격한 서비스 계약, 데이터 직렬화, 네트워크 통신, 인증, 액세스 제어, 통합 가시성 등의 시행과 관련된 모든 복잡성을 처리한다.

https://learning.oreilly.com/api/v2/epubs/urn:orm:book:9798341654778/files/assets/grpc_0101.png

위의 예제에서는 서비스가 Go언어를 사용하여 구현되고 consumer가 java를 사용한다고 가정한다.

서비스 정의는 서버와 클라이언트 양쪽에서 코드를 생성하는 데 사용되는 ProductInfo.proto 파일에 지정되고, 서버와 클라이언트 간의 네트워크 통신은 HTTP/2를 통해 이루어진다.

gRPC 서비스를 구축하는 첫 번째 단계는 입력 매개변수 및 반환 유형과 함께 해당 서비스에서 노출되는 메서드가 포함된 서비스 인터페이스 정의를 만드는 것이다. 서비스 정의의 세부 사항으로 넘어가도록 하자.

서비스 정의

gRPC는 protocol buffer를 IDL(interface definition language)로 사용하여 서비스 인터페이스를 정의한다. protocol buffer는 언어에 구애받지 않고 플랫폼 중립적으며 확장 가능한 구조화된 data를 직렬화하는 매커니즘이다. 서비스 인터페이스 정의는 확장자가 .proto 인 일반 텍스트 파일인 proto 파일에 지정된다. RPC 메서드 매개변수와 반환 유형을 protocol buffer 메세지로 지정하여 일반 protocol buffer 형식으로 gRPC 서비스를 정의한다. 서비스 정의는 protocol buffer 사양의 확장이므로 proto file에서 코드를 생성하기 위해 특수 gRPC 플러그인이 사용된다.

// ProductInfo.proto
syntax = "proto3";
package ecommerce;

service ProductInfo {
    rpc addProduct(Product) returns (ProductID);
    rpc getProduct(ProductID) returns (Product);
}

message Product {
    string id = 1;
    string name = 2;
    string description = 3;
}

message ProductID {
    string value = 1;
}
  1. 서비스 정의는 사용하는 protocol buffer 버전으로 proto3을 지정하는 것으로 시작된다.
  2. 패키지 이름은 protocol message 유형 간의 이름 충돌을 방지하는 데 사용되며 코드를 생성하는 데도 사용된다.
  3. service에 서비스 인터페이스를 정의한다.
  4. addProductrpc를 사용하여 ProductID를 반환하는 것이다.
  5. getProductrpc를 사용하여 Product를 반환하는 것이다.
  6. message는 메세지 유형으로 ProductProductID의 변수 타입, 정의를 나열한다.

위의 서비스는 rpc로 호출할 수 있는 메서드 addProduct, getProduct의 모음인 것이다. 각 메서드에는 서비스의 일부로 정의하거나 protocol buffer 정의로 가져올 수 있는 입력 매개변수와 반환 타입이 있다.

입력 및 반환 매개변수는 Product, ProductID와 같이 사용자 정의 유형이거나 서비스 정의에 정의된 protocol buffer의 잘 알려진 type일 수도 있다. 즉, 잘 알려진 다른 protocol buffer에 정의된 타입을 가져올 수도 있다. 메시지는 각 필드들이 있고 잘 보면 field에 고유한 번호들이 있는 것을 볼 수 있다.

message Product {
    string id = 1;
    string name = 2;
    string description = 3;
}

id는 1, name은 2, description은 3인 것을 볼 수 있다. 이는 field 번호로 protocol buffer에서 message 정의에 있어서 반드시 필요하다. 이 field 번호를 통해서 데이터 크기를 절약하고, 안정적인 식별자 역할을 한다.

이 서비스 정의는 gRPC application의 서버 및 클라이언트 측을 구축하는 데 모두 사용된다.

gRPC 서버

서비스 정의가 준비되면 이를 사용하여 protocol buffer compiler인 protoc를 사용하여 서버 또는 클라이언트 측 코드를 생성할 수 있다. protocol buffer 용 gRPC 플러그인을 사용하면 message 유형을 채우고, 직렬화하고, 검색하기 위한 일반 protocol buffer code뿐만 아니라 gRPC 서버 측 클리언트 측 코드를 생성할 수 있다.

서버 측에서는 서버가 해당 서비스 정의를 구현하고 클라이언트 호출을 처리하기 위해 gRPC 서버를 실행한다. 따라서, 서버 측에서 ProductInfo 서비스가 작동하도록 하려면 다음을 수행해야 한다.

  1. 서비스 base class를 재정의하여 생성된 서비스 스켈레톤의 서비스 로직을 구현한다.
  2. gRPC 서버를 실행하여 클라이언트의 요청을 수신하고 서비스 응답을 반환한다.

서비스 로직을 구현할 때 가장 먼저 해야할 일은 서비스 skeleton을 생성하는 것이다. 아래는 code snippet에서 Go로 구축된 ProductInfo 서비스에 대해 생성된 rpc 함수를 찾을 수 있다. 이러한 rpc 함수의 본문 내부에서 각 함수의 로직을 구현할 수 있다.

import (
  ...
  "context"
  pb "github.com/grpc-up-and-running/samples/ch02/productinfo/go/proto"
  "google.golang.org/grpc"
  ...
)

// ProductInfo implementation with Go

// Add product remote method
func (s *server) AddProduct(ctx context.Context, in *pb.Product) (
      *pb.ProductID, error) {
   // business logic
}

// Get product remote method
func (s *server) GetProduct(ctx context.Context, in *pb.ProductID) (
     *pb.Product, error) {
   // business logic
}

서비스 구현이 준비되면 gRPC 서버를 실행하여 클라이언트의 요청을 수힌하고 해당 요청을 서비스 구현으로 보내고 서비스 응답을 클라이언에 다시 반환해야 한다. 아래는 ProductInfo 서비스 사용 사례에 대한 Go gRPC 서버 구현을 보여준다. 여기서는 TCP port를 열고 gRPC 서버를 시작하고 해당 서버에 ProductInfo 서비스를 등록한다.

func main() {
  lis, _ := net.Listen("tcp", port)
  s := grpc.NewServer()
  pb.RegisterProductInfoServer(s, &server{})
  if err := s.Serve(lis); err != nil {
    log.Fatalf("failed to serve: %v", err)
  }
}

gRPC 클라이언트

서버 측과 마찬가지로 서비스 정의를 사용하여 클라이언트 측 stub을 생성할 수 있다. 클라이언트 stub은 클라이언트 코드가 호출할 수 있는 서버와 동일한 메서드를 제공하며, 클라이언트 stub을 사용하면 서버 측의 remote 함수를 호출할 수 있도록 클라이언트 측의 함수 실행을 네트워크 호출로 변환해준다.

gRPC 서비스 정의는 언어에 구애받지 않으므로 원하는 언어를 통해 지원되는 모든 언어로 클라이언트와 서버를 생성할 수 있다. 따라서 ProductInfo 서비스 사용 사례의 경우 클라이언트 stub은 java로 생성하고 서버 쪽은 go로 구현할 수 있다. 아래는 클라이언트로 java 코드를 사용한 것을 볼 수 있다.

클라이언트와 서버의 프로그래밍 언어에 관계없이 클라이언트 측 구현의 간단한 단계는 원격 서버와의 연결을 설정하고, 해당 연결에 client stub을 연결하고 client stub을 사용하여 rpc 메서드를 소출하는 것이다.

// Create a channel using remote server address
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080)
   .usePlaintext(true)
   .build();

// Initialize blocking stub using the channel
ProductInfoGrpc.ProductInfoBlockingStub stub =
       ProductInfoGrpc.newBlockingStub(channel);

// Call remote method using the blocking stub
StringValue productID = stub.addProduct(
       Product.newBuilder()
       .setName("Apple iPhone 11")
       .setDescription("Meet Apple iPhone 11." +
            "All-new dual-camera system with " +
            "Ultra Wide and Night mode.")
       .build());

클라이언트 - 서버 메시지 흐름

gRPC 클라이언트가 gPRC 서비스를 호출하면 클라이언트 측 gRPC 라이브러리는 protocol buffer를 사용하여 rpc(remote procedure call) protocol buffer 형식을 마샬링한 다음 HTTP/2를 통해 전달한다. 서버 측에서는 요청을 언마샬링하고 protocol buffer를 사용하여 각 procedure 호출이 실행된다. 응답은 서버에서 클라이언트까지 유사한 실행 흐름을 따른다.

https://miro.medium.com/v2/resize:fit:720/format:webp/1*k9oEjGVUY8cvTmMclmV5FA.png

유선 전송 protocol로서 gRPC는 양방향 메시징을 지원하는 고성능 바이너리 메시지 protocol인 HTTP/2를 사용한다. protocol buffer와 함께 gRPC 클라이언트와 서버 간의 메세지 흐름에 대한 낮은 수준의 세부 사항과 gRPC가 HTTP/2를 사용하는 방법에 대해서는 추후에 더 자세히 알도록 하자.

REST와의 차이

RESTful 서비스는 HTTP 1.x와 같은 텍스트 기반 전송 protocol을 기반으로 구축되며 JSON과 같은 사람이 읽을 수 있는 텍스트 형식을 활용한다. 그러나 서비스 간 통신의 경우는 양 당사자가 사람이 읽을 수 있는 텍스트 형식을 사용할 필요가 없기 때문에 JSON과 같은 텍스트를 사용할 필요가 없다.

클라이언트 application은 서버로 보낼 바이너리 content를 생성한 다음, 바이너리 구조를 텍스트로 변환하고 네트워크를 통해 서버 측에 전송한다. 서버 측에서는 이를 파싱하여 다시 바이너리 구조로 변환한 다음에 비지니스를 실행하고 응답으로 보낼 결과 바이너리를 텍스트로 변환한 다음 전송한다. 이러한 과정은 매우 비효율적이며 네트워크에서는 방대한 텍스트 메시지를 전송해야하는 문제가 있다.

사실 json을 사용하는 가장 큰 이유는 사람이 읽기 좋은 도구이기 때문이었지만, 서버 간의 통신에 있어서는 json으로 굳이 통신을 할 필요가 없는 것이다. 이는 RESTful의 문제라기 보다는 도구의 사용에 대한 적절한 상황이 다를 뿐인 것이다.

RESTful 인터페이스의 가장 큰 강력함은 느슨한 결합이지만, 반대로 이것이 단점이 되는 경우도 있다. 가령, 서버에서 스펙을 변경할 경우, 서버 개발자는 OpenAPI와 같은 API 정의를 제공해주어 client 측에서 일방적으로 바꾸어야 하는 경우가 있다. 이는 서버나 클라리언트 측 서로에게 강한 인터페이스 스펙이 있다고 보기 힘들다.

또한, 서버와 서버 간의 통신에 있어서 RESTful한 스펙을 규정하여 통신하는 것이 너무나 큰 짐이 될 때가 있다. 모든 resource들을 RESTful 원칙에 맞게 설계할 수는 없으며 서버와 서버, 즉 시스템 간의 통신에서는 더더욱 지키기 어렵다.

gRPC의 역사

Google은 여러 데이터 센터에서 실행되고 있는 서로 다른 기술로 구축된 수천 개의 마이크로서비스를 연결하기 위해 Stubby라는 범용 RPC 프레임워크를 사용하고 있었다. 핵심 RPC 계층은 초당 수백억 건의 요청을 처리하도록 설계 되었지만, 내부 인프라와 너무 긴밀하게 연결되어 있어 일반적인 프레임워크로 사용하기에는 표준화되어 있지 않았다.

2015년 Google에서는 표준화된 범용 크로스 플랫폼 RPC 인프라스트럭처인 오픈 소스 RPC 프레임워크인 gRPC를 출시했다. gRPC는 Stubby가 제공하는 것과 동일한 확장성, 성능 및 기능을 커뮤니티 전체에 제공하기 위한 것이었다.

이후 gRPC는 Cloud native 생태계에 많이 사용되었고 CNCF 에코시스템 프로젝트에서 많은 주목을 받았다.

gRPC의 장점을 정리하면 다음과 같다.

  1. 프로세스 간 커뮤니케이션이 효율적이다. 이는 JSON이나 XML 방식이 아닌 protocol buffer 기반의 바이너리 protocol을 사용하여 gRPC 서비스 및 클라이언트와 통신하기 때문이다. 또한, HTTP/2 protocol 위에 구현하였기 때문에 process간 통신 속도가 훨씬 빠르다.
  2. 간단하고 잘 정의된 service interface와 schema가 있다. gRPC는 service 간의 interface를 정의한 다음 나중에 세부 구현 사항을 작업한다. 따라서, RESTfUL 처럼 서비스 인터페이스에 대한 결합성이 약하지 않고, 계약 기반의 인터페이스를 굉장히 중요시 한다.
  3. 여러 언어 간의 혼용이 가능하다. 이는 gRPC 플러그인이 자동으로 protocol buffer를 바탕으로 클라이언트, 서버 stub code를 만들어주기 때문에 여러 언어 간의 혼용에도 어려움이 없다.
  4. 다양한 기능도 제공되는데 인증과 암호화, 복원력(기한 및 시간 초과), 메타 데이터 교환, 압축, 로드밸런싱, 서비스 검색 등과 같은 production level의 기능을 지원한다.
  5. 다양한 회사에서 적용하였기 때문에 성숙도가 굉장히 높다.

gRPC의 단점을 정리하면 다음과 같다.

  1. 서비스 간의 통신이 아니라 유저와의 인터렉션에 있어서는 RESTful보다 사용성이 좋지 않다.
  2. 서비스 interface 정의가 달라지면 매번 새로운 interface에 맞게 코드를 수정해야한다. 이는 RESTful보다 더 많은 코드를 바꿔야할 수 있어, 계속된 interface 변경이 개발을 더욱 어렵게 만들 수 있다.

0개의 댓글