Table of Contents
Unary RPC 통신 방식 예제
gRPC 통신 방법 중 하나로 클라이언트에서 단일 요청을 보내고 서버에서 단일 응답을 반환하는 형태이다.
1. proto 파일 생성
1syntax = "proto3";
2package ecommerce;
3option go_package = "productinfo/service/ecommerce";
4
5service ProductInfo {
6 rpc addProduct(Product) returns (ProductId);
7 rpc getProduct(ProductId) returns (Product);
8}
9
10message Product {
11 string id = 1;
12 string name = 2;
13 string description = 3;
14 float price = 4;
15}
16
17message ProductId {
18 string value = 1;
19}
전체 서비스를 정의해준다. Product를 생성하고 id 값을 사용하여 Product 값을 반환받는 서비스를 작성할 예정이다.
1protoc productinfo/service/ecommerce/product.proto \
2 --go_out=. \
3 --go_opt=paths=source_relative \
4 --go-grpc_out=. \
5 --go-grpc_opt=paths=source_relative
입력한 proto 파일을 토대로 protoc 명령어를 사용하여 protocol buffer 메시지와 gRPC 서비스 코드가 포함되어 있는 pb.go 파일과 grpc.pb.go 파일을 생성해준다.
2. 서버 로직 구현
proto 파일에서 정의한 service 구현 작업이 필요하다. addProduct와 getProduct 메서드를 구현해준다.
1type server struct {
2 pb.UnimplementedProductInfoServer // 상위 호환성 위해 추가
3 productMap map[string]*pb.Product
4}
5
6// Product 생성 기능 구현
7func (s *server) AddProduct(ctx context.Context, in *pb.Product) (*pb.ProductId, error) {
8 out, err := uuid.NewV4()
9 if err != nil {
10 return nil, status.Errorf(codes.Internal, "Error while generating Product ID", err)
11 }
12 in.Id = out.String()
13 if s.productMap == nil {
14 s.productMap = make(map[string]*pb.Product)
15 }
16 s.productMap[in.Id] = in
17 return &pb.ProductId{Value: in.Id}, status.New(codes.OK, "").Err()
18}
19
20// Product 조회 기능 구현
21func (s *server) GetProduct(ctx context.Context, in *pb.ProductId) (*pb.Product, error) {
22 value, exists := s.productMap[in.Value]
23 if exists {
24 return value, status.New(codes.OK, "").Err()
25 }
26 return nil, status.Errorf(codes.NotFound, "Product does not exist.", in.Value)
27}
이때, protoc 명령어를 통해 생성된 인터페이스에 맞게 구현을 해야 한다.
또한 server 구조체에 대해 pb.UnimplementedProductInfoServer
를 추가해준 것을 볼 수 있다. 그 이유는 mustEmbedUnimplemented 가 추가됨에 따라 모든 구현체는 상위 호환성을 위해 Unimplemented 메서드를 추가해주어야 하기 때문이다.
이제 gRPC 서버를 구현하는 코드를 작성해준다.
1func main() {
2 // tcp 연결을 위한 port binding
3 lis, err := net.Listen("tcp", ":50051")
4 if err != nil {
5 log.Fatalf("failed to listen: %v", err)
6 }
7 // gRPC server 생성 및 서비스 등록
8 s := grpc.NewServer()
9 pb.RegisterProductInfoServer(s, &server{})
10
11 log.Printf("Starting gRPC listener on port " + port)
12 if err := s.Serve(lis); err != nil {
13 log.Fatalf("failed to serve: %v", err)
14 }
15}
50051 포트에 대해 TCP 연결을 수신 대기하고 새로운 gRPC 서버를 생성한다. 그리고 protoc 명령어를 통해 생성한 ProductInfoServer
서비스를 등록한다. 이때, &server{}
는 ProductInfoServer 인터페이스를 구현한 구조체이다. -> 이 구조체를 사용하여 실제 클라이언트 요청을 처리하는 로직을 구현한다.
마지막으로 s.Serve(lis)
를 호출하여 서버를 실행한다.
3. 클라이언트 로직 구현
1func main() {
2 conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
3 if err != nil {
4 log.Fatalf("did not connect: %v", err)
5 }
6 defer conn.Close()
7
8 // 서버와 통신할 클라이언트 인스턴스 생성
9 c := pb.NewProductInfoClient(conn)
10
11 name := "Apple iPhone 11"
12 description := "Meet Apple iPhone 11."
13 price := float32(1000.0)
14
15 // 호출 시간 제한 : 1초
16 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
17 defer cancel()
18
19 r, err := c.AddProduct(ctx, &pb.Product{Name: name, Description: description, Price: price})
20 if err != nil {
21 log.Fatalf("Could not add product: %v", err)
22 }
23 log.Printf("Product ID: %s added successfully", r.Value)
24
25 product, err := c.GetProduct(ctx, &pb.ProductId{Value: r.Value})
26 if err != nil {
27 log.Fatalf("Could not get product: %v", err)
28 }
29 log.Printf("Product: %s", product.String())
30}
localhost의 50051 포트의 gRPC 서버와의 연결을 시도하는 클라이언트 코드이다.
AddProduct 메서드를 호출하여 Product 정보를 추가하고, GetProduct 메서드를 호출하여 추가한 Product 정보를 조회하였다. 서버가 1초 이상 응답을 반환하지 않으면 오류가 발생한다. 마지막으로 defer 키워드를 사용하여 클라이언트 연결을 종료한다.
위 코드는 Unary RPC를 이용한 통신을 보여준다. 이 코드를 통해 클라이언트와 서버가 서로 단일 요청과 단일 응답을 주고 받는 1:1 관계라는 것을 알 수 있다. 또한 클라이언트가 요청을 보내고 서버로부터 응답을 받을 때까지 대기하는 동기적 호출을 한다는 것을 보여준다.
즉, unary 통신은 단순하고 신뢰성이 높은 통신을 주고 받을 경우 많이 사용한다.