Go에서 간단히 gRPC 서버 구현해보기

gRPC

소개

gRPC는 고성능의 RPC 프레임워크다. HTTP/2 및 TCP 기반으로 동작하고, 일반 REST API 기반의 HTTP 서버가 JSON/XML을 활용해 데이터를 교환하는 것과 다르게 protocol buffer라는 더 작고 직렬화시 효율적인 IDL을 사용한다.

사용 예

보통 실무에서는 gRPC는 MSA 환경에서 마이크로서비스끼리 데이터를 주고 받을 때 많이 사용한다. 한편, iOS, Android 등 모바일 네이티브 앱도 gRPC 통신을 지원하기 때문에, 모바일 앱에 데이터를 주고 받을 때도 사용하기도 한다.

다만, 현재까지는 웹과 데이터를 주고받을 때는 사용이 제한적이다.

사용 방법 요약

gRPC를 서빙하는 방법은 아래와 같다.

  1. proto 파일을 작성한다.
  2. protoc로 컴파일한다.
  3. 메서드를 구현한다.
  4. 서버를 만들고 서빙한다.

위 4개 단계를 거치면 gRPC로 서버를 제공할 수 있다.

Protocol Buffer

프로토콜 버퍼(Protocol Buffer)는 gRPC의 핵심 요소이며, IDL(Interface Definition Language)의 역할을 함과 동시에 메세지 교환의 포멧의 역할을 한다.

프로토콜 버퍼는 구조화된 데이터를 serialize하는 매커니즘의 일종이다. JSON이나 XML보다 효율적으로 직렬화할 수 있다.

사용방법

1. proto 파일 작성

먼저 .proto 파일에 데이터의 구조를 정의한다. 프로토콜 버퍼 데이터는 메세지 로 구조화되는데, 키-밸류 페어로 구성되는 필드로 이루어져 있다.

다음으로 gRPC 서비스를 만든다. gRPC 서비스는 평범한 proto 파일에 생성하며 RPC 메서드의 파라미터와 리턴 타입은 프로토콜 버퍼여야 한다.

책을 조회하는 간단한 서비스를 생각해보자. bookId로 특정 책을 조회하거나 모든 책을 리스트업 할 수 있다.

  • FindBookByID: 특정 책을 ID 기반으로 찾는다.
  • FindAllBooks: 모든 책을 가져온다.
syntax = "proto3";

package book;
option go_package = "simple/protos/book";

service Book {
  rpc FindBookByID (FindBookRequest) returns (FindBookResponse);
  rpc FindALlBooks (FindALlBooksRequest) returns (FindAllBooksResponse);
}

message BookMessage {
  uint64 book_id = 1;
  string title = 2;
  string summary = 3;
  string category = 4;
}

message FindBookRequest {
  uint64 book_id = 1;
}

message FindBookResponse {
  BookMessage book_message = 1;
}

message FindALlBooksRequest {}

message FindAllBooksResponse {
  repeated BookMessage book_message = 1;
}

서비스에 제공할 메서드를 정의하고, 필요한 메세지를 정의하면 된다.

2. 컴파일

protoc라는 컴파일러를 이용해 go server와 client, protocol buffer를 생성한다.

% protoc  --go_out . --go_opt paths=source_relative \
        --go-grpc_out . --go-grpc_opt paths=source_relative \
        protos/book.proto

--go_out 만 명시하면 protocol buffer만 생성되고, --go-grpc_out을 같이 명시하면 server, client의 인터페이스도 함께 생성된다.

3. 메서드 구현

다음으로 필요한 메서드를 실제 구현하자.

원래대로라면 데이터베이스에서 데이터를 추출하겠으나, 여기서는 mockData를 사용하도록 한다.

data/book.go

package data

import bookpb "simple/protos"

var Book = []*bookpb.BookMessage{
	{
		BookId:   1,
		Title:    "상실의 시대",
		Summary:  "젊음과 사랑, 상실의 아픔을 그려낸 무라카미 하루키의 대표작",
		Category: "소설",
	},
	{
		BookId:   2,
		Title:    "구글 엔지니어는 이렇게 일한다",
		Summary:  "구글에서 전세계를 상대로 수많은 엔지니어가 뛰어난 제품을 개발하고 유지보수하며 협업하는 방법",
		Category: "기술",
	},
}

이제 mock data를 이용하는 메서드를 구현한다.

main.go

type bookServer struct {
	bookpb.BookServer
}

func (b *bookServer) FindBookByID(ctx context.Context, req *bookpb.FindBookRequest) (
	*bookpb.FindBookResponse, error,
) {
	res := bookpb.FindBookResponse{}
	for _, book := range data.Book {
		if book.BookId == req.BookId {
			res.BookMessage = book
			break
		}
	}
	return &res, nil
}

func (b *bookServer) FindAllBooks(ctx context.Context, req *bookpb.FindALlBooksRequest) (
	*bookpb.FindAllBooksResponse, error,
) {
	books := []*bookpb.BookMessage{}

	for _, book := range data.Book {
		books = append(books, book)
	}
	res := bookpb.FindAllBooksResponse{}
	res.BookMessage = books
	return &res, nil
}

4. 서버를 만들고 서빙

마지막으로 서버를 생성한다.

main.go

package main

import (
	"context"
	"log"
	"net"
	"simple/data"
	bookpb "simple/protos"

	"google.golang.org/grpc"
)

const Port = "18080"

// ... 위에 구현한 bookServer 구조체와 메서드들 위치

func main() {
	lis, err := net.Listen("tcp", ":"+Port)
	if err != nil {
		log.Fatal(err)
	}

	grpcServer := grpc.NewServer()

	bookpb.RegisterBookServer(grpcServer, &bookServer{})

	log.Printf("start gRPC server on %s port", Port)

	if err = grpcServer.Serve(lis); err != nil {
		log.Fatal(err)
	}
}

go run main.go 명령어로 실행한 후 POSTMAN으로 gRPC 요청을 보내고 잘 동작하는지 확인해보자.

참고자료