🌐

【完全解説】Golang と GORM そして gRPC で作るメモアプリ

に公開

1. はじめに

これまで、GolangとGORMを用いてSQLiteと連携したメモアプリのAPIを、標準ライブラリを使って実装する方法を学んできました。しかし、実際のシステムでは、HTTP APIだけでは実現しにくい高速な通信や厳密な型安全性が求められることもあります。そこで、この記事では既存のHTTP API実装をgRPC APIに書き換え、より効率的かつスケーラブルなサービス構築方法を解説します。

gRPCは、Protocol Buffersをベースとした高速なリモートプロシージャコール (RPC )フレームワークであり、低レイテンシな通信や強力な型チェック、多言語間の相互運用性が特徴です。この記事では、HTTP APIで実装したメモアプリの基本機能 (メモの追加、一覧表示、削除 )をgRPC APIに移行する手順を、プロトコルバッファの定義からサーバーの実装、動作確認まで、ステップバイステップで詳しく紹介します。

このプロセスを通じて、HTTP APIに慣れた開発者がgRPCの基本概念を理解し、実際に低レイテンシなAPIサーバーを構築できるようになることを目指します。

2. HTTP API の概要と課題の振り返り

これまでのプロジェクトでは、Golangの標準ライブラリとGORMを利用して、SQLiteと連携するシンプルなHTTP APIを実装してきました。具体的には、以下のようなエンドポイントでメモの追加、一覧表示、削除を行っていました。

  • POST /memo: JSON形式のリクエストボディからメモの内容を受け取り、データベースに保存する
  • GET /memo: データベースから全てのメモを取得し、JSON配列として返す
  • DELETE /memo/{id}: URLパスからメモIDを取得し、該当するメモをデータベースから削除する
    これらの実装は、直接SQL文を記述することでシンプルに動作確認ができ、基本的なCRUD操作を学ぶのに非常に役立ちました。

しかし、HTTP API の実装にはいくつかの課題も存在します:

  • コードの冗長性:
    ルーティングやJSONのパース、エラーハンドリングのコードが各ハンドラーに散在するため、コード全体が冗長になりがちです。

  • 保守性の低下:
    エンドポイントごとに異なるエラーチェックやレスポンス生成が必要なため、仕様変更や機能拡張の際に修正箇所が多くなり、保守が難しくなる可能性があります。

  • 一貫性の確保の難しさ:
    HTTP APIでは、各エンドポイントで同じようなエラーハンドリングやレスポンス生成のロジックを記述する必要があるため、全体としての一貫性を保つのが難しい場合があります。

これらの課題により、開発効率やコードの拡張性に影響が出ることが懸念されます。そこで、次章以降では、より効率的なAPI設計と実装を実現するための手法として、gRPCの導入を検討します。gRPCでは、プロトコルバッファを利用した型安全な通信や、サービス定義に基づくシンプルなコード実装が可能になるため、HTTP APIで感じた課題の多くを解決できる可能性があります。

3. gRPC の基礎知識

gRPC は、Google によって開発されたオープンソースの高速な RPC (リモートプロシージャコール) フレームワークです。プロトコルバッファ (Protocol Buffers、通称 Protobuf )をデータのシリアライズフォーマットとして使用するため、通信が非常に効率的かつ低レイテンシで行われます。

gRPC の特徴

  • 高速かつ効率的な通信:
    Protobuf を利用することで、データのシリアライズ・デシリアライズが非常に高速に行われ、ネットワーク帯域も効率的に利用できます。これにより、従来の JSON や XML を用いた通信よりも大幅なパフォーマンス向上が期待できます。

  • 強力な型安全性:
    gRPC のサービスやメッセージは、.proto ファイルに定義されるため、コンパイル時に型チェックが行われます。これにより、実行時エラーを防止し、開発者がより安心してコードを書けるようになります。

  • 多言語対応:
    gRPC は Go をはじめ、Java、Python、C# など多くのプログラミング言語に対応しており、異なる言語間でのシームレスな通信が可能です。これにより、マイクロサービスアーキテクチャでの異言語間連携も容易に実現できます。

  • ストリーミングサポート:
    gRPC は一方向および双方向のストリーミングをサポートしているため、リアルタイム通信やチャットなど、継続的なデータ交換が必要なシナリオにも適しています。

  • 自動コード生成:
    .proto ファイルにサービスとメッセージを定義し、 protoc コマンドを使用して各言語向けのコードを自動生成できるため、API の実装が大幅に効率化され、コードの一貫性も保たれます。

まとめ

gRPC は、低レイテンシで高性能な通信を実現し、多言語間の連携が必要なマイクロサービス環境に最適なソリューションです。これからの記事では、HTTP API で実装していたメモアプリを gRPC に書き換える方法を、プロトコルバッファの定義から実装、動作確認まで丁寧に解説していきます。

4. 環境構築とプロトコルバッファの準備

gRPC API を実装するには、まず Protocol Buffers (プロトコルバッファ )のコンパイラ (protoc )と、Go 用のプラグインをインストールし、.proto ファイルから必要なコードを生成する必要があります。

1. 必要なツールのインストール

protoc のインストール

まず、Protocol Buffers コンパイラ protoc をインストールしてください。
公式サイト ( Protocol Buffers Releases )から最新版のバイナリをダウンロードし、パスの通ったディレクトリに配置します。

Go プラグインのインストール

次に、Go 用のコード生成プラグイン protoc-gen-goprotoc-gen-go-grpc をインストールします。以下のコマンドを実行してください:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

これにより、 $GOPATH/bin にプラグインがインストールされるので、環境変数 PATH にそのディレクトリが含まれているか確認してください。

2. proto ファイルの作成

次に、gRPC サービスの定義を記述する .proto ファイルを作成します。
たとえば、以下のような memo.proto ファイルを用意します。
go_package オプションは、 go mod init で初期化したモジュール名に合わせて設定してください。

syntax = "proto3";

option go_package = "github.com/username/project/memo;memo";

package memo;

// Memo メッセージ定義
message Memo {
    uint32 id = 1;
    string content = 2;
}

message CreateMemoRequest {
    string content = 1;
}

// メモ追加用レスポンス
message CreateMemoResponse {
    Memo memo = 1;
}

// メモ一覧取得用リクエスト (空メッセージ)
message ListMemosRequest {}

message ListMemosResponse {
    repeated Memo memos = 1;
}

// メモ削除用リクエスト
message DeleteMemoRequest {
    uint32 id = 1;
}

// メモ削除用レスポンス
message DeleteMemoResponse {
    string message = 1;
}

// Memo サービスの定義
service MemoService {
    rpc CreateMemo(CreateMemoRequest) returns (CreateMemoResponse);
    rpc ListMemos(ListMemosRequest) returns (ListMemosResponse);
    rpc DeleteMemo(DeleteMemoRequest) returns (DeleteMemoResponse);
}

3. プロトコルバッファコードの生成

上記の .proto ファイルから Go コードを生成するために、次のコマンドを実行します。
ここでは、生成されたファイルを ./memo ディレクトリに出力し、paths=source_relative オプションにより、.proto ファイルの場所を基準に相対パスで生成ファイルが配置されるように指定します。

protoc --go_out=./memo --go_opt=paths=source_relative \
       --go-grpc_out=./memo --go-grpc_opt=paths=source_relative \
       memo.proto

これにより、 memo.pb.gomemo_grpc.pb.go./memo ディレクトリ内に生成されます。

4. go.mod の設定

既に go mod init github.com/username/project でモジュールを初期化している場合、go.mod は以下のような形になっているはずです:

module github.com/username/project

go 1.23.4

require (
	...
)

また、 .proto ファイル内の option go_package オプションが、 github.com/username/project/memo;memo としていることを確認してください。
これにより、生成されたコードは正しいインポートパスで利用できるようになります。

5. gRPC サービスの設計

gRPC では、サービスの定義とメッセージの構造を Protocol Buffers (.proto ファイル )を使って記述します。HTTP API でエンドポイントごとに個別の処理を書いていたのに対し、gRPC ではサービスメソッドとして機能をまとめ、型安全なメッセージでデータをやり取りする点が特徴です。

サービス定義の基本

.gRPC サービスは先ほど作成した memo.proto のように定義します。

先ほどの memo.proto では以下のポイントが示されています:

  • 明確なメッセージ定義:
    各リクエスト・レスポンスのデータ構造が MemoCreateMemoRequest などのメッセージとして定義されています。これにより、クライアントとサーバー間で送受信されるデータの形式がコンパイル時にチェックされ、型安全性が向上します。

  • サービスメソッドの設計:
    MemoService サービスには、 CreateMemoListMemosDeleteMemo の3つのメソッドが定義されています。各メソッドは、HTTP API のエンドポイントに対応する機能を持っています。

  • CreateMemo: 新しいメモをデータベースに追加し、追加したレコード (IDと内容 )を返します。

  • ListMemos: 全てのメモを取得し、一覧として返します。

  • DeleteMemo: 指定したIDのメモを削除し、削除結果のメッセージを返します。

HTTP API との比較

HTTP API では、各エンドポイントに対してルーティング、JSONパース、エラーハンドリングを個別に実装していました。一方、gRPC では以下の点で違いがあります:

  • 統一されたサービス定義:
    サービスメソッドとして機能をまとめることで、APIの仕様が一元管理されます。変更があった場合も、.proto ファイルの修正のみで済むため、保守性が向上します。

  • バイナリ通信による高速化:
    Protocol Buffers によるシリアライズ・デシリアライズは非常に高速で、HTTP+JSON に比べて通信のオーバーヘッドが低減されます。

  • 型安全性と自動生成:
    .proto ファイルから自動生成されるコードにより、クライアントとサーバー間でのデータの整合性が保証され、開発者は型に依存した実装が可能になります。

サービス設計の流れ

  1. .proto ファイルの作成:
    上記の例のように、メッセージとサービスを定義します。
    ここで、各メソッドのリクエスト・レスポンスの形式を明確にします。

  2. コード生成:
    protoc コマンドを用いて、Go のコード (pb.go と memo_grpc.pb.go )を生成します。生成されたコードは、プロジェクト内でインポートし、gRPC サーバーおよびクライアントの実装に利用します。

  3. サーバー実装:
    生成されたサービスインターフェース (例: MemoServiceServer )を実装し、GORM などを使ってデータベースと連携する処理を実装します。

  4. クライアント実装と動作確認:
    gRPC クライアント (grpcurl など )を使って、各サービスメソッドが期待通りに動作するかを検証します。

6. gRPC サーバーの実装

このセクションでは、前章で定義した .proto ファイルから自動生成されたコード ( memo.pb.gomemo_grpc.pb.go )を利用して、gRPC サーバーを実装する方法を解説します。ここでは、GORM を用いて SQLite との連携も行いながら、gRPC サービスの各メソッド (CreateMemo、ListMemos、DeleteMemo )を実装します。

サーバー実装の流れ

  1. データベース接続とマイグレーション
    GORM を用いて SQLite データベースに接続し、 Memo モデルに基づいたテーブルを自動で作成します。

  2. gRPC サーバーの初期化
    net.Listen を使ってリスナーを作成し、 grpc.NewServer() により gRPC サーバーのインスタンスを生成します。

  3. サービスインターフェースの実装
    生成された MemoServiceServer インターフェースを実装する構造体 (ここでは server )を定義し、各メソッド (CreateMemo、ListMemos、DeleteMemo )の処理を実装します。各メソッド内では、GORM を使ってデータベース操作を行い、結果を gRPC のレスポンスメッセージとして返します。

  4. Reflection の有効化
    gRPC Reflection を有効にすることで、grpcurl などのツールを用いてサービスの検証が容易になります。

  5. サーバーの起動
    作成したリスナー上で gRPC サーバーを起動し、リクエストを待ち受けます。

サンプルコード

以下は、上記の流れに沿って実装した gRPC サーバーのサンプルコードです。
このコードは、 memo.proto で定義したサービスに基づき、メモの追加、一覧取得、削除を実装しています。

package main

import (
	"context"
	"fmt"
	"log"
	"net"

	pb "github.com/username/project/memo"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// GORM 用の Memo モデル
type Memo struct {
	ID      uint   `gorm:"primaryKey"`
	Content string `gorm:"not null"`
}

// server は pb.MemoServiceServer の実装
type server struct {
	pb.UnimplementedMemoServiceServer
}

var db *gorm.DB

// Creatememo: 新しいメモをDBに追加する
func (s *server) CreateMemo(ctx context.Context, req *pb.CreateMemoRequest) (*pb.CreateMemoResponse, error) {
	memo := Memo{Content: req.Content}
	if err := db.Create(&memo).Error; err != nil {
		return nil, err
	}
	return &pb.CreateMemoResponse{
		Memo: &pb.Memo{
			Id:      uint32(memo.ID),
			Content: memo.Content,
		},
	}, nil
}

// ListMemos: すべてのメモを取得する
func (s *server) ListMemos(ctx context.Context, req *pb.ListMemosRequest) (*pb.ListMemosResponse, error) {
	var memos []Memo
	if err := db.Find(&memos).Error; err != nil {
		return nil, err
	}
	var pbMemos []*pb.Memo
	for _, m := range memos {
		pbMemos = append(pbMemos, &pb.Memo{
			Id:      uint32(m.ID),
			Content: m.Content,
		})
	}

	return &pb.ListMemosResponse{Memos: pbMemos}, nil
}

// DeleteMemo: 指定されたIDのメモを削除する
func (s *server) DeleteMemo(ctx context.Context, req *pb.DeleteMemoRequest) (*pb.DeleteMemoResponse, error) {
	result := db.Delete(&Memo{}, req.Id)
	if result.Error != nil {
		return nil, result.Error
	}
	if result.RowsAffected == 0 {
		return nil, fmt.Errorf("no memo found with given ID: %d", req.Id)
	}

	return &pb.DeleteMemoResponse{Message: "Memo deleted successfully"}, nil
}

func main() {
	var err error
	// SQLite データベースに接続。ファイルが存在しない場合は自動生成される
	db, err = gorm.Open(sqlite.Open("memo.db"), &gorm.Config{})
	if err != nil {
		log.Fatal("failed to connect database:", err)
	}
	// 自動マイグレーションにより、Memoモデルに基づいたテーブルを作成
	db.AutoMigrate(&Memo{})

	// gRPC サーバーのリスナーを作成 (ポート 50051)
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	// gRPC サーバーを作成
	grpcServer := grpc.NewServer()
	pb.RegisterMemoServiceServer(grpcServer, &server{})

	// gRPC Reflection を有効にする
	reflection.Register(grpcServer)

	fmt.Println("gRPC server is running on :50051")
	if err := grpcServer.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

コードの解説

  • DB接続とマイグレーション:
    gorm.Open を使って SQLite に接続し、 db.AutoMigrate(&Memo{}) により Memo モデルに基づいたテーブルが自動で作成されます。

  • gRPC サーバーの初期化:
    net.Listen("tcp", ":50051") でポート50051でのリスナーを作成し、 grpc.NewServer() で gRPC サーバーを生成しています。

  • サービスの実装:
    server 構造体は生成された MemoServiceServer インターフェースを実装しており、各メソッド内で GORM を用いてデータベース操作を行っています。

    • CreateMemo メソッドでは、リクエストからメモ内容を取得し、DBに保存後に作成したレコードを返します。
    • ListMemos では、DBから全メモを取得して、レスポンスに詰め替えています。
    • DeleteMemo では、指定されたIDのメモを削除し、存在しない場合はエラーを返します。
  • Reflection の有効化:
    reflection.Register(grpcServer) を呼び出すことで、grpcurl などのツールを使用してサービス情報を照会できるようにしています。

  • サーバーの起動:
    最後に、 grpcServer.Serve(lis) でサーバーを起動し、リクエストの待ち受けを開始します。

この実装により、HTTP API から gRPC API への移行が実現され、より効率的かつ型安全な API サーバーが構築されます。次のセクションでは、このサーバーの動作確認方法について説明します。

7. gRPC API のテストと動作確認

ここでは、実装した gRPC サーバーが期待通りに動作するかどうかを確認するためのテスト方法について解説します。gRPC API のテストは、主に以下の方法で行います。

grpcurl を使ったテスト

grpcurl は、gRPC サーバーに対してコマンドラインからリクエストを送信できる便利なツールです。gRPC Reflection を有効にしている場合、サービス情報を簡単に照会でき、各メソッドの動作確認が容易になります。

1. サーバーの起動

まず、gRPC サーバーを起動します。例えば、以下のコマンドでサーバーを実行してください。

go run ./main.go

サーバーが正常に起動すると、コンソールに gRPC server is running on :50051 と出力されます。

2. メモの追加 (CreateMemo)

grpcurl を使って、新しいメモを追加してみます。例えば、以下のコマンドを実行します:

grpcurl -plaintext -d '{"content": "今日のタスクを確認する"}' localhost:50051 memo.MemoService/CreateMemo

このコマンドは、 CreateMemo メソッドに対して JSON データを送信し、新しいメモを追加します。成功すると、作成されたメモのIDと内容が含まれる JSON レスポンスが返されます。

3. メモ一覧の取得 (ListMemos)

次に、すべてのメモを取得するために、以下のコマンドを実行します:

grpcurl -plaintext -d '{}' localhost:50051 memo.MemoService/ListMemos

出力は下記のようになると思います。

{
  "memos": [
    {
      "id": 1,
      "content": "今日のタスクを確認する"
    }
  ]
}

4. メモの削除 (DeleteMemo)

最後に、特定のメモを削除するには、削除対象のメモIDを指定して以下のコマンドを実行します。例えば、IDが1のメモを削除する場合は:

grpcurl -plaintext -d '{"id": 1}' localhost:50051 memo.MemoService/DeleteMemo

成功すると、削除成功のメッセージが含まれる JSON レスポンスが返されます。もし指定したIDのメモが存在しなかった場合は、エラーが返されることも確認してください。

テストのポイント

  • -plaintext オプション: セキュリティ保護 (TLS )を使わずに通信するため、 -plaintext オプションを指定します。開発環境ではこれで十分ですが、本番環境ではTLSの利用を検討してください。

  • リクエストデータの形式: 送信する JSON は、.proto ファイルで定義したメッセージ形式に沿っている必要があります。型の不一致や不正な値を送ると、エラーが発生しますので注意してください。

  • Reflection の利用: gRPC Reflection を有効にしていると、grpcurl がサービス情報を自動で取得してくれるため、利用可能なメソッドやリクエスト形式を確認するのに役立ちます。

これらのテスト手法を通じて、gRPC API が正しく動作していることを確認し、HTTP API からの移行が問題なく行われたことを実感できるはずです。必要に応じて、ユニットテストや統合テストの実装も検討すると、さらに信頼性の高いサービス構築に繋がります。

8. まとめ

今回の記事では、HTTP APIで構築していたメモアプリを、gRPC APIへ移行する手法をステップバイステップで解説しました。
主なポイントは以下の通りです:

  • gRPCの導入:
    HTTP APIでの課題 (コードの冗長性、保守性の低下など )を解決し、低レイテンシで型安全な通信が実現できる点を強調しました。

  • 環境構築とプロトコルバッファの準備:
    protocやGoプラグインのインストール、.protoファイルによるサービス定義と自動コード生成について学びました。

  • gRPCサーバー実装:
    GORMとSQLiteを利用して、gRPCサーバーの実装、サービスメソッド (CreateMemo、ListMemos、DeleteMemo )の実装方法を解説しました。

  • テストと動作確認:
    grpcurlなどのツールを用いて、実際にサービスが期待通りに動作することを確認する手法を紹介しました。

この知識を活用することで、より高速で拡張性のあるAPIサーバーの構築が可能となり、今後のマイクロサービス開発に大いに役立つでしょう。ぜひ、今回の内容を参考にしてgRPCの世界に挑戦してみてください!

Discussion