RAKSUL TechBlog

ラクスルグループのエンジニアが技術トピックを発信するブログです

Protocol Buffers で快適な API 開発環境を構築した話

はじめに

こんにちは。Raksul でサーバーサイドエンジニアを担当している山本といいます。 2020 年度の新卒として入社し、現在はDTPスクラムに所属しています。

DTPスクラムでは、主に、ラクスルで入稿された印刷データのチェックに関する機能を提供するサービス(データチェック基盤)の開発を行っています。

本ブログでは、データチェック基盤における Web API 開発の一部で利用している Protocol Buffers に焦点を当て、その概要と基本的な使い方を hands-on 形式で紹介いたします。

データチェック基盤の誕生

過去の記事でも取り上げたように、現在、ラクスルではRaksul Platform Project(以下 RPP)を推進しています。 RPP とは簡単に言うと、モノリシックなアプリケーションであった Raksul(EC) からコアとなる機能を切り出してマイクロサービス化するというものです。 そのRPPの一環として、データチェックに関する API & Dashboard としての機能を提供するデータチェック基盤が誕生しました。(データチェック基盤を京都開発合宿で作った話

このマイクロサービス化に伴い、EC(Rails)や npm package されたクライアント(Vue + TypeScript)とのインターフェース定義に IDL (Interface Description Language)を使いたいという声がエンジニアから挙がりました。

そこで、過去にオペレーターチェック入稿システムを刷新した際に新たに採用され、現在までに運用実績のある Protocol Buffers を利用することにしました。(Go+Rails+SQS+S3で社内のオペレーターチェック入稿システムを刷新した話

Protocol Buffers とは

公式では以下のように、定義されています。

Protocol Buffers とは、構造化データをシリアライズするための、言語やプラットフォームに依存しない拡張可能なメカニズムのことで、Google によって開発されています。

Protocol buffers are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data

引用:https://developers.google.com/protocol-buffers/

Protocol Buffers は構造化データを定義する IDL としての役割と、その構造化データをネットワーク経由で送信可能なバイト列へシリアライズする機能、またその逆のデシリアライズする機能を備えています。 Google 製の RPC フレームワークである gRPC は Protocol Buffers をデフォルトで使用しています。

文面だけではイメージが湧きづらいと思いますので、以降のセクションでProtocol Buffers を利用した開発の流れについて詳しく見ていくことにします。

Protocol Buffers を利用した開発

このセクションでは、Protocol Buffers を利用した開発の流れを簡単な hands-on 形式で紹介していきます。 ここでは Protocol Buffers を利用して、最終的に user に関する情報を取得するような Web API を開発することを想定しています。

開発の流れは以下の通りです。

  • .proto ファイルの定義
  • .protoファイルコンパイラの導入
  • .protoファイルのコンパイル
  • 自動生成されたオブジェクトの振る舞い

.proto ファイルの定義

Protocol Buffers を利用した開発では、まず構造化データを.proto ファイルに定義します。 ではここで述べている構造化データとは一体何者なのでしょうか? よりイメージを掴みやすくするために、簡単な例としてuser.protoを定義してみます。

user.proto

syntax = "proto3";

package hands_on;

message GetUserRequest {
  int32 id = 1;
}

message User {
  int32 id = 1;
  string name = 2;
}

service UserApi {
  rpc GetUser (GetUserRequest) returns (User);
}

まず、一行目はproto3 の構文を使っていることを示しています。 この記述を行わないと、コンパイラがproto2の構文とみなしてコンパイルを実行します。

package は名前空間としての役割を果たし、後述する message の名前が衝突するのを防ぎます。

GetUserRequest message には1つのフィールド、User message には2つのフィールドが定義され、各フィールドに型と名前がセットされています。 また、各フィールドにはそれぞれを一意に特定するための数字が割り振られています。

また、RPC(Remote Procedure Call)システムで message を使用する場合は、.proto ファイルに RPC サービスインターフェイスを定義できます。 上記の例で言えば、UserApi という RPC サービスが定義され、その中に GetUserRequest を受け取り、User を返す GetUser メソッドが定義されています。 より詳細な仕様については公式ガイドをご確認ください。

その後、上記の .proto ファイルをコンパイルすることで、目的の言語で利用可能なコードを自動生成することができます。 生成されたコードでは、定義された message がクラスや構造体の役割を果たすことになります。

.protoファイルコンパイラの導入

続いて、.protoファイルをコンパイルできるように必要となる protobuf を導入します。 macOS を利用している方は、brew で protobuf のインストールが可能です。

$ brew install protobuf

$ protoc --version 
                                                              
  libprotoc 3.14.0

ここまでで.protoファイルをコンパイルして、目的の言語で利用可能なコードを自動生成する準備が整いました。

.protoファイルのコンパイル

では以下のコマンドを実行して、目的の言語で利用可能なコードを自動生成してみます。 ここでは、例として Ruby のコードを生成することにします。

$ protoc --ruby_out=lib/hands_on protos/user.proto

コマンド実行がうまくいけば、lib/hands_on 以下に user_pb.rb が出力されます。 生成された実際のコードは以下の通りです。

user_pb.rb

# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: user.proto

require 'google/protobuf'

Google::Protobuf::DescriptorPool.generated_pool.build do
  add_file("user.proto", :syntax => :proto3) do
    add_message "hands_on.GetUserRequest" do
      optional :id, :int32, 1
    end
    add_message "hands_on.User" do
      optional :id, :int32, 1
      optional :name, :string, 2
    end
  end
end

module HandsOn
  GetUserRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hands_on.GetUserRequest").msgclass
  User = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hands_on.User").msgclass
end

自動生成されたオブジェクトの振る舞い

ここで、実際に生成された Ruby オブジェクトがどのような振る舞いをするのかを実際に手を動かして確認してみます。 実際に動かすには google-protobuf という gem が必要になります。

まず、生成されたコードをインスタンス化します。 このとき型の種類によってデフォルト値が入っていることが分かりますが、何が入るのかについての詳細は公式ドキュメントに詳しいです。 以下の例では、整数値には0が、文字列には空文字列がデフォルトで入っています。

オブジェクトの扱いとしては、Struct に似ているという印象でした。

require 'google/protobuf'

get_user_request = HandsOn::GetUserRequest.new
# => <HandsOn::GetUserRequest: id: 0>

get_user_request.id = 1
p get_user_request.id
# => 1

user = HandsOn::User.new
# => <HandsOn::User: id: 0, name: "">

user.id = 1
user.name = 'test'

p user.name
# => "test"

以下のようにフィールドに定義された型と違う型の値を代入しようとすると、エラーが吐かれるため、ヒューマンエラーを検知して事前に防ぐことができます。

user.name = 1
# => Google::Protobuf::TypeError (Invalid argument for string field 'name' (given Integer).)

protobuf へのシリアライズ/デシリアライズは以下のように実行することができます。 また、JSON へのシリアライズ/デシリアライズも可能です。

encoded = user.to_proto
p encoded
# => "\x12\x04test"

decoded = HandsOn::User.decode(encoded)
p decoded
# => <HandsOn::User: id: 0, name: "test">

json = user.to_json
p json
# => "{\"name\":\"test\"}"

HandsOn::User.decode_json(json)
# => <HandsOn::User: id: 0, name: "test">

Protocol Buffers により実現できること

Protocol Buffers で実現できることをまとめると以下の通りです。

.proto ファイルがそのまま Web API の仕様書として機能する

Protocol Buffers を IDL として利用することで、.proto ファイルに API で扱うデータの構造やインターフェイスを簡潔に定義できるようになりました。 実務でもフロントエンドとやりとりをする際に、.proto ファイルをベースに会話をすることで、認識の齟齬を防ぐことができるようになりました。

対応する様々な言語のコードの自動生成が可能

Protocol Buffers が言語に依存しない作りになっているので、異なる言語が導入されたシステム間の連携がスムーズになりました。 Raksul では実際に Ruby、Go、TypeScript のコードを Protocol Buffers から生成して開発を行なっています。

また、コードの自動生成により、仕様と実装の乖離を防ぐことができるようになりました。

型安全にデータをやりとりすることができる

Protocol Buffers を利用した開発でも確認したように、各言語で扱うデータが型に関する情報を持っているので、誤った型のデータを送る/返すといったミスを未然に防ぐことができるようになりました。 特に、Ruby 等の動的型付け言語を使用した際は、とても大きな恩恵を得ることができました。

Protocol Buffers 利用時の注意点

自動生成されたオブジェクトの振る舞いのセクションでも軽く触れましたが、構造化データで定義された全てのフィールドは Optional として扱われ、何も格納しなかった場合は、デシリアライズの時にデフォルトの値が格納されるという点は注意が必要です。

各フィールドについて required もしくは optional として扱うかをフロント・サーバー間であらかじめ取り決めを行っておき、コメント等に残しておく必要がありそうです。 自身も実務でこの仕様に惑わされてしまった苦い経験があります。。

まとめ

本ブログでは、Raksul のWeb API 開発において IDL として利用している Protocol Buffers について取り上げました。

うまく活用できれば大きなメリットを享受でき、開発生産性の向上が見込める一方で、今回のように gRPC での利用が前提ではなく Protocol Buffers 単体で使う場合はまだまだできることが少ないのが現状です。 そこで、Raksul ではより効率的な Web API 開発を促進する RPC フレームワークである Twirp を導入しています。 Twirp を利用した Web API 開発の詳細については今後のブログで詳しく紹介していく予定です。

本ブログを通じて、Protocol Buffers の導入を検討されている方にとって少しでも役立つ情報が提供できていれば幸いです。

最後になりますが、Raksul では伝統産業の仕組みを変えて、日本のDXの一翼を担うサーバーサイドエンジニアを募集しています!