개요

  • gRPC의 리플렉션(reflection) 개념을 정리한다.

gRPC 리플렉션이란

  • proto 파일이 없어도 grpc서버의 기능을 사용할 수 있게 해준다.
  • 주로 디버깅 목적으로 많이 쓰인다. (grpcurl에서 많이 쓴다.)
  • REST API의 Open API 스펙 문서 페이지나, graphql의 introspection과 비슷한 개념이다.
  • 서버의 API(RPC)가 어떤 스펙으로 되어 있는지 친절하게 알려주는 기능이다.
  • 프로덕션 환경과 같은 곳에서는 보안을 위해서 당연히 OFF로 해두는 게 좋다.

gRPC 리플렉션 기능 사용하기

  • gRPC 리플렉션 기능을 사용하려면 Go기준으로 서버측의 코드에 reflection.Register(s)를 추가해주면 된다고 한다.
func main() {
    flag.Parse()
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    fmt.Printf("server listening at %v\n", lis.Addr())

    s := grpc.NewServer()

    // Register Greeter on the server.
    hwpb.RegisterGreeterServer(s, &hwServer{})

    // Register RouteGuide on the same server.
    ecpb.RegisterEchoServer(s, &ecServer{})

    // Register reflection service on gRPC server.
    reflection.Register(s) // 이 부분이다.

    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

gRPC 리플렉션의 API 정의

  • 리플렉션 기능 자체도 gRPC 서비스로서 구현되어 있다.
  • 여기에서 gRPC 리플렉션 서비스의 proto정의파일을 볼 수 있다.

gRPC 리플렉션 기능 테스트

gRPC 클라이언트 툴 evans를 리플렉션 모드로 기동

  • evans에 -r 옵션을 주면 리플렉션 모드로 기동할 수 있다.
  • localhost:50051 에 리플렉션 기능을 켠 서버가 동작중이다.
  • 패키지 grpc.reflection.v1alpha에서 서비스 ServerReflection을 선택하고, ServerReflectionInfo 메서드를 호출한다.
> evans -r --port 50051

  ______
 |  ____|
 | |__    __   __   __ _   _ __    ___
 |  __|   ' ' / /  / _. | | '_ '  / __|
 | |____   ' V /  | (_| | | | | | '__ ,
 |______|   '_/    '__,_| |_| |_| |___/

 more expressive universal gRPC client


helloworld.Greeter@127.0.0.1:50051> package grpc.reflection.v1alpha

grpc.reflection.v1alpha@127.0.0.1:50051> service ServerReflection

grpc.reflection.v1alpha.ServerReflection@127.0.0.1:50051> call ServerReflectionInfo

노출(expose)된 서비스 조사

  • 호스트에 localhost를 입력하면 다음과 같이 어떤 기능을 사용할 것인지 물어본다. 먼저 노출된 서비스명을 조사하기 위해 list_services를 선택한다.
  > file_by_filename
    file_containing_symbol
    file_containing_extension
    all_extension_numbers_of_type
    list_services

두 가지 서비스 grpc.reflection.v1alpha.ServerReflectionhelloworld.Greeter가 노출되어 있는 것을 알 수 있다.

...snip...

host (TYPE_STRING) => localhost
v list_services
list_services (TYPE_STRING) =>
host (TYPE_STRING) => {
  "list_services_response": {
    "service": [
      {
        "name": "grpc.reflection.v1alpha.ServerReflection"
      },
      {
        "name": "helloworld.Greeter"
      }
    ]
  }
}

서비스에서 사용할 수 있는 메서드 조사

  • file_containing_symbol을 사용한다.
  • 심볼은 helloworld.Greeter를 입력한다.
  • 그러면 base64로 인코딩된 FileDescriptor가 회신된다. descriptor 란 Protocol Buffers의 심볼을 인코딩, 디코딩하기 위한 메타데이터의 집합니다.파일 디스크립터는 그 이름대로 Protocol Buffers 의 정의가 적혀있는 파일에 대한 디스크립터다. 이 샘플의 경우는 helloworld.proto 파일이다.
host (TYPE_STRING) =>
v file_containing_symbol
file_containing_symbol (TYPE_STRING) => helloworld.Greeter
host (TYPE_STRING) => {
  "file_descriptor_response": {
    "file_descriptor_proto": [
      "ChBoZWxsb3dvcmxkLnByb3RvEgpoZWxsb3dvcmxkIhwKDEhlbGxvUmVxdWVzdBIMCgRuYW1lGAEgASgJIh0KCkhlbGxvUmVwbHkSDwoHbWVzc2FnZRgBIAEoCTJJCgdHcmVldGVyEj4KCFNheUhlbGxvEhguaGVsbG93b3JsZC5IZWxsb1JlcXVlc3QaFi5oZWxsb3dvcmxkLkhlbGxvUmVwbHkiAEI2Chtpby5ncnBjLmV4YW1wbGVzLmhlbGxvd29ybGRCD0hlbGxvV29ybGRQcm90b1ABogIDSExXYgZwcm90bzM="
    ]
  }
}

base64을 디코딩해본다. Base64인코딩된 문자열은 다음 Go 프로그램으로 파싱할 수 있다. (고 한다.)

package main

import (
    "encoding/base64"
    "fmt"
    "log"

    "github.com/golang/protobuf/protoc-gen-go/descriptor"
    "google.golang.org/protobuf/proto"
)

func main() {
    var out []byte
    in := "ChBoZWxsb3dvcmxkLnByb3RvEgpoZWxsb3dvcmxkIhwKDEhlbGxvUmVxdWVzdBIMCgRuYW1lGAEgASgJIh0KCkhlbGxvUmVwbHkSDwoHbWVzc2FnZRgBIAEoCTJJCgdHcmVldGVyEj4KCFNheUhlbGxvEhguaGVsbG93b3JsZC5IZWxsb1JlcXVlc3QaFi5oZWxsb3dvcmxkLkhlbGxvUmVwbHkiAEI2Chtpby5ncnBjLmV4YW1wbGVzLmhlbGxvd29ybGRCD0hlbGxvV29ybGRQcm90b1ABogIDSExXYgZwcm90bzM="
    out, err := base64.StdEncoding.DecodeString(in)
    if err != nil {
        log.Fatal(err)
    }

    var m descriptor.FileDescriptorProto
    if err := proto.Unmarshal(out, &m); err != nil {
        log.Fatal(err)
    }

    fmt.Println(*m.Name) // examples/helloworld/helloworld/helloworld.proto
    fmt.Println(*m.Service[0].Name) // Greeter
    fmt.Println(*m.Service[0].Method[0].Name) // SayHello
}

실행해보려고 했지만 이 두 라인에서 참조에러가 발생했다.

	"github.com/golang/protobuf/protoc-gen-go/descriptor"
	"google.golang.org/protobuf/proto"
)

음..

go run .\go-reflection-parse.go
go-reflection-parse.go:8:2: no required module provides package github.com/golang/protobuf/protoc-gen-go/descriptor: go.mod file not found in current directory or any parent directory; see 'go help modules'
go-reflection-parse.go:9:2: no required module provides package google.golang.org/protobuf/proto: go.mod file not found in current directory or any parent directory; see 'go help modules'

다음 두 명령을 실행해서 모듈을 인스톨한 후에도 결과는 동일했다. 막혔다..😓

$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

Go 기초(특히 모듈이나 패키지 관리 부분)부터 정리하는게 좋겠다.

일단 그냥 base64 디코딩해서보면 다음과 같은 형태다.

Go Modules를 개념정리한 후 프로젝트 폴더로 이동 후 다음 명령어를 실행했다. 그러자 에러가 없어졌다!! 🧁

go mod init grpc-test
go get google.golang.org/protobuf
go get github.com/golang/protobuf

실행결과는 다음과 같다. 파싱에 성공하여 proto 파일명과 서비스명 메서드명을 추출하는데 성공하였다.

PS D:\projects\go-sample-grpc> go run .\go-reflection-parse.go
helloworld.proto
Greeter
SayHello

참고

  • https://syfm.hatenablog.com/entry/2020/06/23/235952 <– Evans를 작성한 사람의 블로그 글이다. 자세하게 설명해주고 있어서 도움이 된다.
  • https://grpc.io/docs/guides/reflection/
  • https://github.com/grpc/grpc/blob/master/doc/server-reflection.md