最近は、モバイルアプリとサーバーの通信にgRPCを使っています。gRPCは、サーバー同士の通信では徐々に使われ始めている印象がありますが、モバイルアプリでの使用例はまだ少ないと思うので、動機とか、感想とか、ウチはこうしてるというものを共有します。

リクエストとレスポンスの定義を1箇所にまとめる

今のプロジェクトでは、同じデータをサーバー, iOS, Android, Webで扱う予定がありました。普通のREST APIでは同じデータを4つの言語に翻訳する必要がありましたが、これをprotoへの翻訳の1回だけで済ませたいというのが、gRPCを使う最初の動機でした。

gRPCでは、リクエストとレスポンスの全ての情報をprotoファイル上で表現し、それを元に各言語のコードを自動生成します。APIドキュメントを人間が各言語に翻訳する場合と比べると、コードを書く手間が省けますし、誤解や打ち間違いなどによるミスもなくなります。

実際には、以下のようなprotoファイルを定義します。

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

Goのサーバーにはserviceのinterfaceのコードが生成され、これに準拠した実装するとRPCが提供できるようになります。インターフェースに準拠できていなければコンパイルエラーになり、間違いを検出できます。

type RecipeServiceServer interface {
	SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

SwiftのクライアントにはRPCを実行するコードが生成され、以下のように使用できます。クライアントでも間違った名前や型の指定はコンパイルエラーになります。

var request = HelloRequest()
request.name = "ishkawa"

let greeter = Greeter(address: "grpc.example.com")
let reply = try greeter.sayhello(request)

RPCを利用するためのコードが自動生成できるという点ももちろん気に入っているのですが、RPCのメソッド名も、RPCが使うリクエストとレスポンスの型も、その型の実体も、すべてproto上で表されていることによって、サーバーとクライアントの連携におけるミスの多くをコンパイル時に発見できる点も素晴らしいと思います。

仕様変更に素早く対応する

サーバーとクライアントの連携を型で検証できる状況は、ミスを発見しやすくするだけでなく、開発中の変更の適用が簡単になるというメリットもありました。

今のチームでは何かの機能を作るときに、とりあえず動くものを素早く実装し、それを触ってみてから更に機能を検討するというやり方をしています。検討結果によっては、広範囲に変更が必要になったりするのですが、それらはほとんどコンパイルエラーによって洗い出されるため、効率良く変更に対応できるというわけです。

Nullabilityは諦める

proto3にはnullabilityを表現する仕様がありません。messageにフィールドの値が含まれていない場合、そのフィールドは0や空文字列などのデフォルト値で埋められる仕様となっており、存在しないこととデフォルト値であることを区別できません。これは、nullableな型とnon-nullableな型を区別するSwiftやKotlinのような言語を普段書いている身からすると、モヤモヤするところだと思います。

Swiftに限って言えば、oneofを使えばOptional<Wrapped>型のようなものを表現できなくはないのですが、他の言語でも同様に扱える保証はないですし、独自ルールをなるべく入れたくなかったので、今回は素直にnullabilityの表現を諦めました。

Firebase AuthenticationとCloud Endpointsによる認証

Firebaseには、Firebase Authenticationというユーザー認証を行うサービスがあります。ログイン状態の管理やトークンのリフレッシュなど、クライアント側の認証の仕組みの多くはFirebase Authenticationに任せることができます。

GCPには、Cloud EndpointsというAPI管理を行うサービスがあります。Cloud Endpointsには、Firebase Authenticationのトークンを検証し、結果をmetadataに付与してgRPCサーバーにリクエストを送るという機能があります。

この2つを組み合わせると、クライアントでもサーバーでも認証状態を管理するシステムを自前で持たなくても、gRPCのユーザー認証ができるようになります。

HTTP/1.1 + JSONへの変換

Cloud Endpointsは、gRPCをHTTP/1.1 + JSONなAPIに変換する機能も提供しています。今のプロジェクトにはちょっとだけWebがあって、そのAPIにはgRPCを変換したものを使っています。

終わりに

動機がリクエストとレスポンスの定義を1箇所にまとめることだったので、現時点ではunary RPCしか使っていないのですが、機会があればstreaming RPCも使ってみて、また感想を書いてみようと思います。

書き終わってみたらモバイルアプリ固有の話があまりなかった。