Back to blog

Protobuf로 MCP 서버 구축하기 1부: Protobuf에서 REST API 생성

August 27, 2026

Charlie Zhang

2025년 9월 3일

이 블로그 시리즈에서는 유용한 도구를 갖춘 모델 컨텍스트 프로토콜(Model Context Protocol, MCP) 서버를 구축하는 방법을 설명합니다. 1부에서는 처음부터 새로 구현하는 대신, 기존 Protocol Buffers(Protobuf)와 Google의 gRPC 트랜스코딩을 활용해 REST API와 OpenAPI 사양을 생성합니다. 이후 맞춤형 protoc(Protocol Buffers 컴파일러) 플러그인으로 MCP 서버 생성을 자동화하여 gRPC 서비스, REST API, OpenAPI 사양 및 MCP 서버를 동일한 소스에서 생성하는 기반을 마련합니다.


MCP 서버 구축 시리즈 구성

  • 1부: (본 기사) gRPC 트랜스코딩을 사용하여 Protobuf에서 REST API 생성하기
  • 2부: Protoc 플러그인으로 MCP 서버 생성 자동화하기
  • 3부: Proto 주석으로 AI 상호작용 향상시키기
  • 4부: 실제로 MCP 도구를 실행하면서 얻은 통찰력

Protobuf에서 REST API를 생성해 구축할 결과

이 블로그를 마치면 다음을 갖게 됩니다:

  • gRPC와 REST API를 모두 생성하는 단일 Protocol Buffers 정의
  • 자동 OpenAPI 사양 생성
  • MCP 서버 생성을 위한 기반 (2부에서 다룰 예정)

이 기사에 언급된 모든 코드는 GitHub 저장소 zhangcz828/proto-to-mcp-tutorial에서 찾을 수 있습니다.


전제 조건

시작하기 전에 다음을 갖추고 있는지 확인하세요:

  • Go 1.19+ 설치
  • Protocol Buffer 컴파일러 (protoc) 설치
  • gRPC 및 REST API에 대한 기본적인 지식

수동 API 관리에서 발생하는 동기화 문제

우리의 아키텍처는 gRPC를 내부 서비스에 사용하고, REST API를 외부 클라이언트에 사용하며, OpenAPI 사양은 별도의 수동으로 유지 관리되는 저장소에 보관하는 방식으로 단순하게 시작했습니다. 시스템이 성장함에 따라 이러한 분리는 골칫거리가 되었습니다. 변경 사항이 생길 때마다 서비스 코드, REST 게이트웨이, 그리고 문서를 여러 곳에서 업데이트해야 했습니다. 이로 인해 불일치, 누락된 업데이트, 많은 반복 작업이 발생했습니다.


gRPC 트랜스코딩이란?

gRPC 트랜스코딩을 사용하면 gRPC 서비스를 REST API로 노출할 수 있으므로, 클라이언트는 익숙한 HTTP 호출을 사용할 수 있고 백엔드 로직은 gRPC 상태로 유지됩니다. 이는 Google API 어노테이션으로 알려진 특수 HTTP 어노테이션을 Protocol Buffers 정의에 추가하여 가능해집니다. 이 어노테이션은 경로, 경로 변수, 요청 본문을 포함하여 HTTP 요청이 gRPC 메서드에 어떻게 매핑되는지 정의합니다.

Google API 어노테이션은 어떻게 작동하나요?

Google API 어노테이션은 gRPC 서비스가 REST API에 매핑되는 방식을 정의하는 Protocol Buffers 확장 집합입니다. 원래 Google이 자체 API를 위해 개발했으며, gRPC-to-REST 트랜스코딩의 표준이 되었습니다.

주요 구성 요소:

  • google/api/annotations.proto: RPC 메서드의 HTTP 옵션에 대한 확장을 제공하는 파일입니다.
  • google.api.http: google/api/http.proto에 정의된 옵션으로, gRPC 메서드의 HTTP 매핑을 지정합니다.
  • 경로 변수: HTTP 경로의 플레이스홀더 (예: {book_id})로, 요청 메시지의 필드에 자동으로 매핑됩니다.
  • 본문 매핑: HTTP 옵션의 body 필드는 요청 메시지의 어느 부분이 HTTP 요청 본문으로 사용되는지 결정합니다.

요약하자면, Google API 어노테이션은 API를 쉽게 유지 관리하고 확장할 수 있도록 하여 모든 것을 동기화 상태로 유지하고 수동 작업을 줄여줍니다.


Protobuf REST API 프로젝트 설정

새 프로젝트를 생성하는 것부터 시작하겠습니다:

mkdir proto-to-mcp-tutorial
cd proto-to-mcp-tutorial
# 디렉토리 구조 생성
mkdir -p {proto,googleapis,generated/{go,openapi},cmd}

프로젝트 구조는 다음과 같아야 합니다:

proto-to-mcp-tutorial/
├── proto/
├── googleapis/
├── generated/
│   ├── go/
│   └── openapi/
└── cmd/

필수 종속성 설치

먼저 Go protoc 플러그인을 설치합니다:

go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.1
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0
go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.24.0

다음으로 HTTP 어노테이션에 필요한 Google API 정의를 다운로드해야 합니다:

# Google APIs용 디렉토리 생성
mkdir -p googleapis/google/api
# 필요한 proto 파일 다운로드
curl -o googleapis/google/api/annotations.proto \
https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/annotations.proto
curl -o googleapis/google/api/http.proto \
https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto

Protocol Buffers 정의 생성

서점 서비스 정의가 포함된 proto/bookstore.proto 파일을 생성합니다:

Protocol Buffers

syntax = "proto3";

package bookstore.v1;

import "google/api/annotations.proto";

option go_package = "generated/go/bookstore/v1";

service BookstoreService {
    // Get a book by ID
    rpc GetBook(GetBookRequest) returns (Book) {
        option (google.api.http) = {
            get: "/v1/books/{book_id}"
        };
    }
     // Create a new book
    rpc CreateBook(CreateBookRequest) returns (Book) {
        option (google.api.http) = {
            post: "/v1/books"
            body: "book"
        };
    }
}

message Book {
    string book_id = 1;
    string title = 2;
    string author = 3;
    int32 pages = 4;
}

message GetBookRequest {
    string book_id = 1;
}

message CreateBookRequest {
    Book book = 1;
}

google.api.http 어노테이션에서 마법이 일어납니다. get: "/v1/books/{book_id}"book_id URL 매개변수가 GetBookRequest 메시지의 해당 필드에 자동으로 매핑되는 GET 엔드포인트를 생성하도록 제너레이터에 지시합니다. 마찬가지로, body: "book"book 필드 전체가 CreateBook 엔드포인트의 HTTP 요청 본문이 되도록 지정합니다.

이 접근 방식의 특히 우아한 점은 동일한 어노테이션이 여러 제너레이터에서 작동한다는 것입니다. gRPC 게이트웨이는 이를 사용하여 REST 엔드포인트를 생성하고, OpenAPI 제너레이터는 이를 사용하여 올바른 경로 매개변수와 요청 본문 스키마를 생성하며, 2부에서 보게 될 것처럼, 우리의 MCP 플러그인도 이를 사용하여 도구 입력을 구조화하는 방법을 이해할 수 있습니다.


gRPC·REST API·OpenAPI 코드 생성

어노테이션이 포함된 proto 파일이 준비되면 빌드 프로세스는 놀라울 정도로 간단해집니다:

export GOOGLEAPIS_DIR=./googleapis

# gRPC 서비스 코드 생성
protoc -I${GOOGLEAPIS_DIR} --proto_path=proto --go_out=./generated/go  --go_opt paths=source_relative --go-grpc_out=./generated/go --go-grpc_opt paths=source_relative bookstore.proto

# REST 엔드포인트용 gRPC 게이트웨이 생성
protoc -I${GOOGLEAPIS_DIR} --proto_path=proto --grpc-gateway_out=./generated/go --grpc-gateway_opt paths=source_relative bookstore.proto

# OpenAPI 사양 생성
protoc -I${GOOGLEAPIS_DIR} -I./proto --openapi_out=./generated/openapi \
      --openapi_opt=fq_schema_naming=true \
      --openapi_opt=version="1.0.0" \
      --openapi_opt=title="Bookstore API" \
      bookstore.proto

protoc 명령은 동일한 bookstore.proto 파일을 읽지만, 사용된 플러그인에 따라 다른 출력을 생성합니다. HTTP 어노테이션은 각 제너레이터가 적절한 인터페이스를 생성하는 데 필요한 라우팅 정보를 제공합니다.


gRPC 서버 구현

cmd/server/main.go를 생성합니다:

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"net/http"

	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/reflection"

	pb "proto-to-mcp-tutorial/generated/go"
)

type server struct {
	pb.UnimplementedBookstoreServiceServer
	books map[string]*pb.Book
}

func (s *server) GetBook(ctx context.Context, req *pb.GetBookRequest) (*pb.Book, error) {
	book, exists := s.books[req.BookId]
	if !exists {
		return nil, fmt.Errorf("book not found")
	}
	return book, nil
}

func (s *server) CreateBook(ctx context.Context, req *pb.CreateBookRequest) (*pb.Book, error) {
	switch {
	case req.Book == nil:
		return nil, fmt.Errorf("book is required")
	case req.Book.Title == "":
		return nil, fmt.Errorf("book title is required")
	case req.Book.Author == "":
		return nil, fmt.Errorf("book author is required")
	case req.Book.Pages <= 0:
		return nil, fmt.Errorf("book pages must be greater than zero")
	}

	book := req.Book

	fmt.Println("Creating book:", book.Author, book.Title, book.Pages)
	if book.BookId == "" {
		book.BookId = fmt.Sprintf("book-%d", len(s.books)+1)
	}
	s.books[book.BookId] = book
	return book, nil
}

func main() {
	// 일부 샘플 데이터로 서버 초기화
	srv := &server{
		books: map[string]*pb.Book{
			"book-1": {
				BookId: "book-1",
				Title:  "The Go Programming Language",
				Author: "Alan Donovan",
				Pages:  380,
			},
		},
	}

	// gRPC 서버 시작
	go func() {
		lis, err := net.Listen("tcp", ":9090")
		if err != nil {
			log.Fatalf("failed to listen: %v", err)
		}

		s := grpc.NewServer()
		pb.RegisterBookstoreServiceServer(s, srv)

		reflection.Register(s)

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

	// REST 게이트웨이 시작
	ctx := context.Background()
	mux := runtime.NewServeMux()

	opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
	err := pb.RegisterBookstoreServiceHandlerFromEndpoint(ctx, mux, "localhost:9090", opts)
	if err != nil {
		log.Fatalf("failed to register gateway: %v", err)
	}

	log.Println("REST server starting on :8080")
	if err := http.ListenAndServe(":8080", mux); err != nil {
		log.Fatalf("failed to serve REST: %v", err)
	}
}

gRPC 및 REST API 구현 테스트

서버를 실행합니다:

# Go 모듈 초기화
go mod init proto-to-mcp-tutorial && go mod tidy
go run cmd/server/main.go

다음과 같은 출력이 표시되어야 합니다:

gRPC server starting on :9090
REST server starting on :8080

테스트를 위해 새 터미널을 엽니다 (서버는 계속 실행 중).

이제 클라이언트는 gRPC 또는 HTTP를 통해 완벽하게 동일한 서비스 로직과 상호작용할 수 있으며, 데이터 모델과 동작은 동일합니다. 예를 들어, 책을 가져오는 curl 요청은 다음과 같습니다:

curl -X GET http://localhost:8080/v1/books/book-1

그리고 예상대로 정확하게 반환합니다:

{
 "bookId": "book-1",
 "title": "The Go Programming Language",
 "author": "Alan Donovan",
 "pages": 380
}

OpenAPI 생성 확인

생성된 OpenAPI 사양을 확인합니다:

cat generated/openapi/openapi.yaml

REST 엔드포인트와 일치하는 완벽한 OpenAPI 사양이 표시되어야 합니다! 다음을 확인하세요:

  • /v1/books/{bookId}/v1/books에 대한 경로 정의
  • bookstore.v1.Book에 대한 스키마 정의
  • HTTP 메서드 사양 (get, post)

Protobuf 기반 통합 API 구축 결과

축하합니다! 방금 다음과 같은 통합 API 시스템을 구축했습니다:

  • 단일 정보 소스: 하나의 proto 파일이 전체 API를 정의합니다.
  • 이중 인터페이스: 서비스는 gRPC와 REST를 통해 모두 액세스할 수 있습니다.
  • 자동 문서화: OpenAPI 사양이 자동으로 생성됩니다.
  • 동기화 문제 없음: 모든 것이 동일한 소스에서 생성되므로 동기화 상태가 유지됩니다.

현재 아키텍처는 다음과 같습니다:

Protobuf에서 gRPC, REST API, OpenAPI 및 MCP 서버로 확장되는 아키텍처


MCP 서버 자동 생성을 위한 다음 단계

이 기반은 2부에서 AI 통합을 위한 MCP 서버를 생성하기 위해 이 동일한 패턴을 확장하는 데 완벽하게 준비되어 있습니다. 여러분이 추가한 HTTP 어노테이션은 MCP 플러그인이 도구 입력 및 출력을 구조화하는 방법을 이해하는 데 도움이 될 것입니다.


Protobuf와 REST API 생성 FAQ

Protobuf 하나로 gRPC와 REST API를 함께 만들 수 있나요?

네. 이 예제처럼 하나의 proto 파일에 서비스와 메시지를 정의하고 google.api.http 어노테이션을 추가하면, protoc 플러그인을 통해 gRPC 서비스 코드와 REST 게이트웨이를 생성할 수 있습니다.

gRPC 트랜스코딩은 어떤 역할을 하나요?

gRPC 트랜스코딩은 HTTP 요청을 gRPC 메서드에 매핑합니다. 따라서 외부 클라이언트는 익숙한 REST 방식으로 호출하면서 백엔드에서는 동일한 gRPC 서비스 로직을 사용할 수 있습니다.

google.api.http 어노테이션에는 무엇을 정의하나요?

HTTP 메서드와 경로, 경로 변수, 요청 본문 매핑을 정의합니다. 예제의 {book_id}는 URL 값을 요청 메시지 필드에 연결하고, body: "book"book 필드를 HTTP 요청 본문으로 지정합니다.

OpenAPI 사양도 같은 proto 파일에서 생성되나요?

네. OpenAPI용 protoc 플러그인이 동일한 bookstore.proto와 HTTP 어노테이션을 읽어 경로, HTTP 메서드 및 스키마 정의가 포함된 사양을 생성합니다.

이 단계에서 MCP 서버까지 바로 생성되나요?

아닙니다. 1부에서는 Protobuf에서 gRPC, REST API 및 OpenAPI 사양을 생성해 MCP 서버 자동 생성에 필요한 기반을 구축합니다. 맞춤형 protoc 플러그인을 이용한 MCP 서버 생성은 2부에서 다룹니다.

메일: salesinquiry@enterprisedb.com

Share this