TT Lab
블로그 시작하기
배우기 러닝패스 코스

gRPC 서비스를 직접 세우고 운영한다

빈 폴더에서 gRPC 서비스를 세우고 .proto 없이 불러 본다

TT Lab 에서 이어서 보기

목표

.proto 한 장에서 시작해 코드를 만들고, 단항·스트리밍 호출을 받는 서버와 그 서버를 부르는 클라이언트를 짠 뒤, 리플렉션을 켜서 .proto 파일이 없는 도구로 서비스를 둘러보고 불러 봅니다.

왜 중요한가

gRPC 의 출발점은 코드가 아니라 계약서입니다. 서버와 클라이언트는 같은 .proto 에서 만든 코드를 쓰고, 와이어에는 이름 대신 번호가 실립니다. 그래서 장애 현장에서 「이 서버가 무엇을 받는지」 를 알려면 .proto 가 필요한데, 운영 중인 서버 앞에 그 파일이 늘 있지는 않습니다. 서버 리플렉션은 서버가 자기 계약서를 직접 내주게 하는 표준 서비스이고, grpcurl 같은 도구가 그것으로 요청을 만듭니다. 이 실습은 그 전체 흐름을 한 번 손으로 이어 봅니다.

단계

  1. 계약서를 쓴다: /root/grpcops/build/proto/lib/v1/catalog.proto 를 쓰세요. syntax 는 proto3, package 는 lib.v1 입니다. 메시지 Book 은 isbn(string, 1)·title(string, 2)·author(string, 3)·year(int32, 4)·copies(int32, 5), GetBookRequest 는 isbn(string, 1), ListBooksRequest 는 author(string, 1) 입니다. 서비스 Catalog 에는 GetBook(GetBookRequest) returns (Book) 단항 호출과 ListBooks(ListBooksRequest) returns (stream Book) 서버 스트리밍 호출을 둡니다.
  2. 계약서에서 코드를 만든다: cd /root/grpcops/build && /opt/rt-lab/bin/python -m grpc_tools.protoc -I proto --python_out=gen --grpc_python_out=gen proto/lib/v1/catalog.proto 로 /root/grpcops/build/gen 아래에 lib/v1/catalog_pb2.py(메시지)와 lib/v1/catalog_pb2_grpc.py(서비스 뼈대와 스텁)를 만드세요. 먼저 mkdir -p gen 이 필요합니다. .proto 를 고치면 이 명령을 다시 돌려야 합니다.
  3. 단항 호출을 받는 서버: /root/grpcops/build/server.py 에 serve(port) 를 만드세요. sys.path 에 /root/grpcops/build/gen 를 더해 생성 코드를 불러오고, /opt/fixtures/grpcops/books.json 의 책 목록을 읽어 CatalogServicer 를 상속한 서비스를 grpc.server 에 붙인 뒤 127.0.0.1:port 에서 듣고 끝날 때까지 기다립니다. GetBook 은 isbn 이 같은 책을 돌려주고, 없는 isbn 이면 NOT_FOUND, 빈 isbn 이면 INVALID_ARGUMENT 로 끝냅니다.
  4. 목록은 흘려보낸다: Catalog 에 ListBooks 를 구현하세요. request.author 가 비어 있으면 모든 책을, 아니면 author 가 정확히 같은 책만 isbn 오름차순으로 하나씩 보냅니다. 맞는 책이 없으면 아무것도 보내지 않고 정상으로 끝납니다.
  5. 스텁으로 부르는 클라이언트: /root/grpcops/build/client.py 에 get_title(target, isbn) 과 count_by_author(target, author) 를 만드세요. get_title 은 GetBook 을 불러 제목을 돌려주되, 상태가 NOT_FOUND 일 때만 None 을 돌려주고 다른 오류는 그대로 올립니다. count_by_author 는 ListBooks 스트림을 끝까지 읽어 받은 책의 수를 돌려줍니다. 둘 다 호출마다 timeout=5 를 겁니다. 채점기는 기준 서버를 상대로 부릅니다.
  6. 리플렉션을 켠다: serve 에서 서버를 시작하기 전에 /opt/fixtures/grpcops/reflect.py 의 enable_server_reflection(이름 목록, server) 를 부르세요. 이름 목록에는 lib.v1.Catalog 와 reflect.SERVICE_NAME(grpc.reflection.v1.ServerReflection) 를 넣습니다. reflect 는 sys.path 에 /opt/fixtures/grpcops 를 더하면 import reflect 로 불러올 수 있습니다.
  7. .proto 없이 둘러보고 부른다: 서버를 백그라운드로 띄운 뒤(cd /root/grpcops/build && /opt/rt-lab/bin/python -c "import server; server.serve(50051)" &) rpcurl 로 세 가지를 파일에 남기세요. 서비스 목록을 /root/grpcops/build/list.txt 에, Catalog 서비스의 정의(describe lib.v1.Catalog)를 /root/grpcops/build/describe.txt 에, isbn 9780000000105 로 부른 GetBook 의 응답 JSON 을 /root/grpcops/build/getbook.json 에 저장합니다. rpcurl 은 /opt/rt-lab/bin/python /opt/fixtures/grpcops/rpcurl.py 입니다.

참고

계약서를 쓴다

/root/grpcops/build/proto/lib/v1/catalog.proto 를 쓰세요. syntax 는 proto3, package 는 lib.v1 입니다. 메시지 Book 은 isbn(string, 1)·title(string, 2)·author(string, 3)·year(int32, 4)·copies(int32, 5), GetBookRequest 는 isbn(string, 1), ListBooksRequest 는 author(string, 1) 입니다. 서비스 Catalog 에는 GetBook(GetBookRequest) returns (Book) 단항 호출과 ListBooks(ListBooksRequest) returns (stream Book) 서버 스트리밍 호출을 둡니다.

package 가 와이어의 서비스 이름 앞부분이 됩니다(/lib.v1.Catalog/GetBook). 번호는 바이트에 남는 유일한 이름이라 과제의 번호를 그대로 씁니다. 응답을 여러 개 흘려보내는 호출은 returns 쪽에 stream 을 붙입니다.

계약서에서 코드를 만든다

cd /root/grpcops/build && /opt/rt-lab/bin/python -m grpc_tools.protoc -I proto --python_out=gen --grpc_python_out=gen proto/lib/v1/catalog.proto 로 /root/grpcops/build/gen 아래에 lib/v1/catalog_pb2.py(메시지)와 lib/v1/catalog_pb2_grpc.py(서비스 뼈대와 스텁)를 만드세요. 먼저 mkdir -p gen 이 필요합니다. .proto 를 고치면 이 명령을 다시 돌려야 합니다.

-I 로 준 폴더가 import 경로의 뿌리가 됩니다. -I proto 로 주면 생성 코드가 lib/v1/ 아래에 놓이고, 파이썬에서는 from lib.v1 import catalog_pb2 로 부릅니다. -I proto/lib/v1 로 주면 파일 이름이 catalog.proto 로 바뀌어 디스크립터가 달라집니다.

단항 호출을 받는 서버

/root/grpcops/build/server.py 에 serve(port) 를 만드세요. sys.path 에 /root/grpcops/build/gen 를 더해 생성 코드를 불러오고, /opt/fixtures/grpcops/books.json 의 책 목록을 읽어 CatalogServicer 를 상속한 서비스를 grpc.server 에 붙인 뒤 127.0.0.1:port 에서 듣고 끝날 때까지 기다립니다. GetBook 은 isbn 이 같은 책을 돌려주고, 없는 isbn 이면 NOT_FOUND, 빈 isbn 이면 INVALID_ARGUMENT 로 끝냅니다.

없는 책에 빈 Book 을 돌려주면 상태는 OK 이고 클라이언트는 제목이 빈 책을 받습니다. 「없다」 는 값이 아니라 상태 코드로 알려야 합니다. context.abort(grpc.StatusCode.NOT_FOUND, "...") 는 예외를 던져 처리기를 끝냅니다.

목록은 흘려보낸다

Catalog 에 ListBooks 를 구현하세요. request.author 가 비어 있으면 모든 책을, 아니면 author 가 정확히 같은 책만 isbn 오름차순으로 하나씩 보냅니다. 맞는 책이 없으면 아무것도 보내지 않고 정상으로 끝납니다.

처리기를 제너레이터로 만들어 yield 하면 응답이 하나씩 나갑니다. 「결과 없음」 은 오류가 아니라 빈 스트림입니다. NOT_FOUND 로 끝내면 클라이언트는 목록이 비었는지 저자 이름이 틀렸는지 구별할 수 없습니다.

스텁으로 부르는 클라이언트

/root/grpcops/build/client.py 에 get_title(target, isbn) 과 count_by_author(target, author) 를 만드세요. get_title 은 GetBook 을 불러 제목을 돌려주되, 상태가 NOT_FOUND 일 때만 None 을 돌려주고 다른 오류는 그대로 올립니다. count_by_author 는 ListBooks 스트림을 끝까지 읽어 받은 책의 수를 돌려줍니다. 둘 다 호출마다 timeout=5 를 겁니다. 채점기는 기준 서버를 상대로 부릅니다.

except grpc.RpcError 로 모든 오류를 None 으로 바꾸면 서버가 내려간 것(UNAVAILABLE)과 책이 없는 것이 같아집니다. e.code() == grpc.StatusCode.NOT_FOUND 로 가르세요. 세는 일은 스트림으로 해야 합니다. 채점기는 기준 서버에서 ListBooks 가 불렸는지도 봅니다.

리플렉션을 켠다

serve 에서 서버를 시작하기 전에 /opt/fixtures/grpcops/reflect.py 의 enable_server_reflection(이름 목록, server) 를 부르세요. 이름 목록에는 lib.v1.Catalog 와 reflect.SERVICE_NAME(grpc.reflection.v1.ServerReflection) 를 넣습니다. reflect 는 sys.path 에 /opt/fixtures/grpcops 를 더하면 import reflect 로 불러올 수 있습니다.

서비스 이름은 생성 코드에서 꺼내면 오타가 없습니다. catalog_pb2.DESCRIPTOR.services_by_name["Catalog"].full_name 이 lib.v1.Catalog 입니다. 실제 프로젝트에서는 grpcio-reflection 패키지의 같은 이름 함수를 씁니다.

.proto 없이 둘러보고 부른다

서버를 백그라운드로 띄운 뒤(cd /root/grpcops/build && /opt/rt-lab/bin/python -c "import server; server.serve(50051)" &) rpcurl 로 세 가지를 파일에 남기세요. 서비스 목록을 /root/grpcops/build/list.txt 에, Catalog 서비스의 정의(describe lib.v1.Catalog)를 /root/grpcops/build/describe.txt 에, isbn 9780000000105 로 부른 GetBook 의 응답 JSON 을 /root/grpcops/build/getbook.json 에 저장합니다. rpcurl 은 /opt/rt-lab/bin/python /opt/fixtures/grpcops/rpcurl.py 입니다.

rpcurl -plaintext 127.0.0.1:50051 list 처럼 grpcurl 과 같은 모양으로 부릅니다. 요청 본문은 -d '{"isbn": "..."}' 로, 메서드는 lib.v1.Catalog/GetBook 처럼 적습니다. 끝나면 kill %1 로 서버를 내리세요.