gRPC-Go 시작하기

1. 소개

이 Codelab에서는 gRPC-Go를 사용하여 Go로 작성된 경로 매핑 애플리케이션의 기반을 형성하는 클라이언트와 서버를 만듭니다.

튜토리얼이 끝나면 gRPC를 사용하여 원격 서버에 연결하여 지도에서 특정 좌표에 있는 항목의 이름이나 우편 주소를 가져오는 클라이언트가 있습니다. 완전한 애플리케이션은 이 클라이언트-서버 설계를 사용하여 경로를 따라 관심 장소를 열거하거나 요약할 수 있습니다.

서비스는 프로토콜 버퍼 파일에 정의되며, 이 파일은 클라이언트와 서버가 서로 통신할 수 있도록 상용구 코드를 생성하는 데 사용되므로 이 기능을 구현하는 데 드는 시간과 노력을 절약할 수 있습니다.

이 생성된 코드는 서버와 클라이언트 간의 복잡한 통신뿐만 아니라 데이터 직렬화 및 역직렬화도 처리합니다.

학습할 내용

  • 프로토콜 버퍼를 사용하여 서비스 API를 정의하는 방법
  • 자동 코드 생성을 사용하여 프로토콜 버퍼 정의에서 gRPC 기반 클라이언트와 서버를 빌드하는 방법
  • gRPC를 사용한 클라이언트-서버 통신에 대한 이해

이 Codelab은 gRPC를 처음 접하거나 gRPC를 다시 한번 살펴보고 싶은 Go 개발자 또는 분산 시스템을 빌드하는 데 관심이 있는 모든 사용자를 대상으로 합니다. 이전 gRPC 경험은 필요하지 않습니다.

2. 시작하기 전에

기본 요건

다음이 설치되어 있는지 확인합니다.

  • Go 도구 모음 버전 1.24.5 이상 설치 안내는 Go의 시작하기를 참고하세요.
  • 프로토콜 버퍼 컴파일러 protoc 버전 3.27.1 이상 설치 안내는 컴파일러의 설치 가이드를 참고하세요.
  • Go 및 gRPC용 프로토콜 버퍼 컴파일러 플러그인 이러한 플러그인을 설치하려면 다음 명령어를 실행하세요.
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

프로토콜 버퍼 컴파일러가 플러그인을 찾을 수 있도록 PATH 변수를 업데이트합니다.

export PATH="$PATH:$(go env GOPATH)/bin"

코드 가져오기

처음부터 완전히 시작하지 않아도 되도록 이 Codelab에서는 애플리케이션 소스 코드의 스캐폴드를 제공하여 이를 완성할 수 있습니다. 다음 단계에서는 프로토콜 버퍼 컴파일러 플러그인을 사용하여 상용구 gRPC 코드를 생성하는 등 애플리케이션을 완료하는 방법을 보여줍니다.

GitHub에서 이 소스 코드를 .ZIP 보관 파일로 다운로드하고 콘텐츠의 압축을 풉니다.

구현을 입력하는 것을 건너뛰려면 완료된 소스 코드를 GitHub에서 확인하세요.

3. 서비스 정의

첫 번째 단계는 프로토콜 버퍼를 사용하여 애플리케이션의 gRPC 서비스, RPC 메서드, 요청 및 응답 메시지 유형을 정의하는 것입니다. 서비스에서 제공하는 기능:

  • 서버가 구현하고 클라이언트가 호출하는 RPC 메서드 GetFeature
  • GetFeature 메서드를 사용할 때 클라이언트와 서버 간에 교환되는 데이터 구조인 메시지 유형 PointFeature 클라이언트는 서버에 대한 GetFeature 요청에서 지도 좌표를 Point로 제공하고 서버는 해당 좌표에 있는 항목을 설명하는 해당 Feature로 응답합니다.

이 RPC 메서드와 메시지 유형은 모두 제공된 소스 코드의 routeguide/route_guide.proto 파일에 정의됩니다.

프로토콜 버퍼는 일반적으로 protobufs로 알려져 있습니다. gRPC 용어에 대한 자세한 내용은 gRPC의 핵심 개념, 아키텍처, 수명 주기를 참고하세요.

메시지 유형

소스 코드의 routeguide/route_guide.proto 파일에서 먼저 Point 메시지 유형을 정의합니다. Point는 지도상의 위도-경도 좌표 쌍을 나타냅니다. 이 Codelab에서는 좌표에 정수를 사용합니다.

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

12message 구조의 각 필드에 대한 고유 ID 번호입니다.

다음으로 Feature 메시지 유형을 정의합니다. FeaturePoint로 지정된 위치에 있는 항목의 이름이나 우편 주소에 string 필드를 사용합니다.

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

서비스 메서드

route_guide.proto 파일에는 애플리케이션 서비스에서 제공하는 하나 이상의 메서드를 정의하는 RouteGuide라는 service 구조가 있습니다.

RouteGuide 정의 내에 rpc 메서드 GetFeature를 추가합니다. 앞서 설명한 것처럼 이 메서드는 지정된 좌표 집합에서 위치의 이름이나 주소를 조회하므로 GetFeature가 지정된 Point에 대해 Feature를 반환하도록 합니다.

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

이는 단항 RPC 메서드입니다. 클라이언트가 서버에 요청을 보내고 로컬 함수 호출과 마찬가지로 응답이 돌아오기를 기다리는 간단한 RPC입니다.

4. 클라이언트 및 서버 코드 생성

그런 다음 프로토콜 버퍼 컴파일러를 사용하여 .proto 파일에서 클라이언트와 서버 모두의 상용구 gRPC 코드를 생성합니다. routeguide 디렉터리에서 다음을 실행합니다.

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       route_guide.proto

이 명령어는 다음 파일을 생성합니다.

  • route_guide.pb.go: 애플리케이션의 메시지 유형을 만들고 데이터에 액세스하는 함수가 포함되어 있습니다.
  • route_guide_grpc.pb.go: 클라이언트가 서비스의 원격 gRPC 메서드를 호출하는 데 사용하는 함수와 서버가 해당 원격 서비스를 제공하는 데 사용하는 함수가 포함되어 있습니다.

다음으로 클라이언트가 요청을 보낼 때 서버가 답변으로 응답할 수 있도록 서버 측에서 GetFeature 메서드를 구현합니다.

5. 서비스 구현

서버 측의 GetFeature 함수는 주요 작업이 이루어지는 곳입니다. 이 함수는 클라이언트로부터 Point 메시지를 가져와 알려진 장소 목록에서 해당하는 위치 정보를 Feature 메시지로 반환합니다. 다음은 server/server.go의 함수 구현입니다.

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
  for _, feature := range s.savedFeatures {
    if proto.Equal(feature.Location, point) {
      return feature, nil
    }
  }
  // No feature was found, return an unnamed feature
  return &pb.Feature{Location: point}, nil
}

원격 클라이언트의 요청에 따라 이 메서드가 호출되면 함수에 RPC 호출을 설명하는 Context 객체와 해당 클라이언트 요청의 Point 프로토콜 버퍼 객체가 전달됩니다. 이 함수는 조회된 위치의 Feature 프로토콜 버퍼 객체와 필요한 경우 error를 반환합니다.

메서드에서 지정된 Point에 적합한 정보로 Feature 객체를 채운 다음 nil 오류와 함께 return하여 RPC 처리가 완료되었으며 Feature 객체를 클라이언트에 반환할 수 있음을 gRPC에 알립니다.

GetFeature 메서드에는 클라이언트의 위치 조회 요청이 해당 함수로 라우팅될 수 있도록 routeGuideServer 객체를 생성하고 등록해야 합니다. 이 작업은 main()에서 실행됩니다.

func main() {
  flag.Parse()
  lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }

  var opts []grpc.ServerOption
  grpcServer := grpc.NewServer(opts...)

  s := &routeGuideServer{}
  s.loadFeatures()
  pb.RegisterRouteGuideServer(grpcServer, s)
  grpcServer.Serve(lis)
}

main()에서 이루어지는 작업은 다음과 같습니다.

  1. lis, err := net.Listen(...)를 사용하여 원격 클라이언트 요청을 수신하는 데 사용할 TCP 포트를 지정합니다. 기본적으로 애플리케이션은 port 변수에 의해 지정되거나 서버를 실행할 때 명령줄에 --port 스위치를 전달하여 지정된 TCP 포트 50051를 사용합니다. TCP 포트를 열 수 없으면 애플리케이션이 심각한 오류로 종료됩니다.
  2. grpc.NewServer(...)를 사용하여 gRPC 서버 인스턴스를 만들고 이 인스턴스의 이름을 grpcServer로 지정합니다.
  3. 애플리케이션의 API 서비스를 나타내는 구조체인 routeGuideServer에 대한 포인터를 만들고 포인터 이름을 s.로 지정합니다.
  4. s.loadFeatures()를 사용하여 GetFeature를 통해 조회할 수 있는 위치로 s.savedFeatures 배열을 채웁니다.
  5. GetFeature에 대한 RPC 호출이 적절한 함수로 라우팅되도록 gRPC 서버에 API 서비스를 등록합니다.
  6. 포트 세부정보를 사용하여 서버에서 Serve()을 호출하여 클라이언트 요청을 차단 대기합니다. 이는 프로세스가 종료되거나 Stop()이 호출될 때까지 계속됩니다.

loadFeatures() 함수는 server/testdata.go에서 좌표-위치 매핑을 가져옵니다.

6. 클라이언트 만들기

이제 클라이언트 코드를 구현할 client/client.go를 수정합니다.

원격 서비스의 메서드를 호출하려면 먼저 서버와 통신할 gRPC 채널을 만들어야 합니다. 클라이언트의 main() 함수에서 서버의 타겟 URI 문자열 (이 경우 주소와 포트 번호)을 grpc.NewClient()에 전달하여 이를 만듭니다.

conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
        log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()

serverAddr 변수로 정의된 서버의 주소는 기본적으로 localhost:50051이며, 클라이언트를 실행할 때 명령줄의 --addr 스위치로 재정의할 수 있습니다.

클라이언트가 TLS 또는 JWT 사용자 인증 정보와 같은 인증 사용자 인증 정보가 필요한 서비스에 연결해야 하는 경우 클라이언트는 필요한 사용자 인증 정보가 포함된 DialOptions 객체를 grpc.NewClient에 매개변수로 전달할 수 있습니다. RouteGuide 서비스에는 사용자 인증 정보가 필요하지 않습니다.

gRPC 채널이 설정되면 Go 함수 호출을 통해 RPC를 실행할 클라이언트 스텁이 필요합니다. 애플리케이션의 .proto 파일에서 생성된 route_guide_grpc.pb.go 파일에서 제공하는 NewRouteGuideClient 메서드를 사용하여 스텁을 가져옵니다.

import (pb "github.com/grpc-ecosystem/codelabs/getting_started_unary/routeguide")

client := pb.NewRouteGuideClient(conn)

서비스 메서드 호출

gRPC-Go에서 RPC는 차단/동기 모드로 작동합니다. 즉, RPC 호출은 서버가 응답할 때까지 기다리며 응답 또는 오류를 반환합니다.

Simple RPC

단순 RPC GetFeature를 호출하는 것은 이 경우 로컬 메서드 client.GetFeature를 호출하는 것만큼 간단합니다.

point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

// Call GetFeature method on the client.
feature, err := client.GetFeature(context.TODO(), point)
if err != nil {
  log.Fatalf("client.GetFeature failed: %v", err)
}

클라이언트는 이전에 생성된 스텁에서 메서드를 호출합니다. 메서드의 매개변수에 대해 클라이언트는 Point 요청 프로토콜 버퍼 객체를 만들고 채웁니다. 필요한 경우 호출 시간 제한을 정의하거나 진행 중인 RPC를 취소하는 등 RPC의 동작을 변경할 수 있는 context.Context 객체도 전달합니다. 호출에서 오류가 반환되지 않으면 클라이언트는 첫 번째 반환 값에서 서버의 응답 정보를 읽을 수 있습니다.

log.Println(feature)

전체적으로 클라이언트의 main() 함수는 다음과 같습니다.

func main() {
        flag.Parse()

        // Set up a connection to the gRPC server.
        conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
                log.Fatalf("fail to dial: %v", err)
        }
        defer conn.Close()

        // Create a new RouteGuide stub.
        client := pb.NewRouteGuideClient(conn)

        point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
        log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

        // Call GetFeature method on the client.
        feature, err := client.GetFeature(context.TODO(), point)
        if err != nil {
                log.Fatalf("client.GetFeature failed: %v", err)
        }
        log.Println(feature)
}

7. 사용해 보기

애플리케이션의 작업 디렉터리에서 다음 명령어를 실행하여 서버와 클라이언트가 서로 올바르게 작동하는지 확인합니다.

  1. 한 터미널에서 서버를 실행합니다.
cd server
go run .
  1. 다른 터미널에서 클라이언트를 실행합니다.
cd client
go run .

다음과 같은 출력이 표시됩니다. 명확성을 위해 타임스탬프는 생략했습니다.

Getting feature for point (409146138, -746188906)
name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
Getting feature for point (0, 0)
location:<>

8. 다음 단계