빈 폴더에서 gRPC 서비스를 세우고 .proto 없이 불러 본다
목표
.proto 한 장에서 시작해 코드를 만들고, 단항·스트리밍 호출을 받는 서버와 그 서버를 부르는 클라이언트를 짠 뒤, 리플렉션을 켜서 .proto 파일이 없는 도구로 서비스를 둘러보고 불러 봅니다.
왜 중요한가
gRPC 의 출발점은 코드가 아니라 계약서입니다. 서버와 클라이언트는 같은 .proto 에서 만든 코드를 쓰고, 와이어에는 이름 대신 번호가 실립니다. 그래서 장애 현장에서 「이 서버가 무엇을 받는지」 를 알려면 .proto 가 필요한데, 운영 중인 서버 앞에 그 파일이 늘 있지는 않습니다. 서버 리플렉션은 서버가 자기 계약서를 직접 내주게 하는 표준 서비스이고, grpcurl 같은 도구가 그것으로 요청을 만듭니다. 이 실습은 그 전체 흐름을 한 번 손으로 이어 봅니다.
단계
- 계약서를 쓴다: /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) 서버 스트리밍 호출을 둡니다.
- 계약서에서 코드를 만든다: 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 를 고치면 이 명령을 다시 돌려야 합니다.
- 단항 호출을 받는 서버: /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 로 끝냅니다.
- 목록은 흘려보낸다: Catalog 에 ListBooks 를 구현하세요. request.author 가 비어 있으면 모든 책을, 아니면 author 가 정확히 같은 책만 isbn 오름차순으로 하나씩 보냅니다. 맞는 책이 없으면 아무것도 보내지 않고 정상으로 끝납니다.
- 스텁으로 부르는 클라이언트: /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 를 겁니다. 채점기는 기준 서버를 상대로 부릅니다.
- 리플렉션을 켠다: 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 로 불러올 수 있습니다.
- .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 입니다. mkdir -p /root/grpcops/build 로 먼저 만드세요.
- 서버를 직접 띄워 보려면 cd /root/grpcops/build && /opt/rt-lab/bin/python -c "import server; server.serve(50051)" 를 씁니다. 다른 터미널 창이나 & 로 백그라운드에 두고 클라이언트나 rpcurl 로 부릅니다.
- rpcurl 은 grpcurl 과 같은 모양으로 부르는 작은 도구입니다. alias rpcurl='/opt/rt-lab/bin/python /opt/fixtures/grpcops/rpcurl.py' 로 줄여 두세요. 이 실습 이미지에는 grpcurl 이 없어서 같은 일을 하는 파이썬 판을 두었습니다.
- 흔한 실수 두 가지입니다. .proto 를 고친 뒤 코드 생성을 다시 하지 않아 옛 코드로 도는 것, 그리고 없는 책에 빈 응답을 돌려주어 상태가 OK 가 되는 것입니다.
- 이론 배경은 블로그 글 gRPC 가 푸는 문제와 전체 그림 과 디버깅과 관측: grpcurl, 리플렉션 에 있습니다.
- 파이썬은 /opt/rt-lab/bin/python 으로 실행합니다. grpcio·grpcio-tools·protobuf 가 그 가상환경에만 있어서, 그냥 python3 로 돌리면 ModuleNotFoundError 가 납니다. alias rpy=/opt/rt-lab/bin/python 처럼 줄여 두면 편합니다.
- 코드 생성기는 /opt/rt-lab/bin/python -m grpc_tools.protoc 입니다. protoc 과 같은 깃발(-I, --python_out, --grpc_python_out)을 받고, google/protobuf 의 기본 타입(any·duration 등)을 찾는 경로를 스스로 더합니다.
- 실습 파드는 바깥으로 나가는 연결이 막혀 있습니다. 모든 통신은 같은 파드 안의 127.0.0.1 에서 일어나며, 설치나 다운로드는 필요 없습니다.
- 채점기는 여러분의 코드를 별도 프로세스로 띄워 실제로 호출해 봅니다. 서버를 미리 켜 둘 필요는 없고, 채점기는 빈 포트를 골라 따로 띄웁니다. 앞 단계에서 완성한 함수는 지우지 마세요.
- 실습 세션이 끝나면 /root 의 파일은 남지 않습니다. 필요한 코드는 끝내기 전에 따로 보관하세요.
계약서를 쓴다
/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 로 서버를 내리세요.