- バックエンド / リーダー候補
- PdM
- Webエンジニア(シニア)
- Other occupations (19)
- Development
- Business
はじめに
ここでは、モノリシックなサーバーは書いたことあるけど、マイクロサービス・アーキテクチャやそれに類するシステムの開発はしたことがない、というアプリケーション・エンジニア向けに Wantedly でのマイクロサービス開発において予め知っておくと良い考え方やプラクティスをまとめていきます。
第1回は デザインドキュメントを書こう という話をしました。今回は第2回ということで、インターフェースについて書きます。
インターフェースを考えよう
なぜインターフェースは重要か
インターネットサービスにおいてユーザーインターフェースの重要性に意を唱える人はあまりいないと思います。ユーザーが内部の実装の都合を推し量らないと使えないようなサービスは、使い勝手の良いサービスとは言えないからです。
だからと言って、ユーザーが何の想定も持たずにサービスを利用しているかと言うとそうではなく、一定のメンタルモデルを持って使っています。ユーザーがそのサービスを上手く使えるためには、メンタルモデルが対象を上手く抽象化したものになっている必要があります。そして、メンタルモデルは多くの場合、ユーザーインターフェースを通じたインタラクションによって形成されるため、概してユーザーインターフェースは重要なものだと認識されています。
これは日常生活におけるユーザーインターフェースの立ち位置の話ですが、マイクロサービスにおいても同じことが言えます。つまり、 良いマイクロサービスは良いインターフェースを備えている ことが必要なのです。
マイクロサービスの構築プロセス
本題に入る前に、ここで一度マイクロサービスの構築プロセスについて見てみましょう。原則としては以下のようなフローになると思います。
ここで、左上「サービスの責務を定義」が前回取り扱ったトピックです。今回は一つ下がって「インターフェースを定義」、つまり定義した責務からインターフェースを導くことが主題になります。
多少この図について補足します。左側が、他のマイクロサービスから見た抽象としてのマイクロサービスです。マイクロサービスのモデリング、モデル化されたマイクロサービスなど色々な言い方がありますが、同義です。右側が、実際に動作するコードやデータベース・スキーマを含んだ、具体としてのマイクロサービスです。上段がマイクロサービスの全体に関わる設計で、下段が個別のインターフェースに関わる設計です。
矢印は各プロセスが与える影響を表わしています。最初は実線だけを見る方が理解しやすいでしょう。つまり、「サービスの責務を定義」→「インターフェースを定義」、「サービスの責務を定義」→「サービス内部の基本設計」などです(ここで最初に考える「サービス内部の基本設計」は、どういったフレームワークやデータストレージを使うのかというような、「インターフェースの実装」や「サービスの責務」の実現可能性に関わる大まかな設計です)。
しかし、そんなに綺麗に行くでしょうか?実際には、欲しいインターフェースを定義してみてから、欲しいマイクロサービスの全体像がより鮮明に見える、あるいはインターフェースを実装する過程で、より適切なインターフェースの定義にたどり着く、と言ったことはあるはずです。これをフィードバックと呼び、破線で書いています。
フィードバック、と言っているのは、例えば「"インターフェース定義の集合"すなわち"マイクロサービスの責務"」ではないからです。下位のところで遭遇した問題はあくまで上位のことを再考する必要があるかもしれないというシグナルであって、上位のものから下位のものがより具体的に導かれているという関係は維持すべきです。
なおここでは特にスタンスとして、トップダウンな開発を推奨しているわけでも、ボトムアップな開発を推奨しているわけでもありません。それは使い分ければ良いでしょう。ただ、良いソフトウェアを作るのであれば、 全体の設計と個別の設計・抽象的な事柄と具体的な事柄はそれぞれは分けて考えよう ということと 分けたものの間を必要に応じて行き来しよう ということを言っています。
コラム:他のマイクロサービスとの相互作用
上記の話は、作ろうとしている単一のマイクロサービス上での相互作用の話でしたが、実は「サービスの責務」「インターフェース」のそれぞれのレベルで、マイクロサービス間での相互作用があります。
「サービスの責務」の場合、各マイクロサービスは自律していて上位・下位という関係はないのが通常なので、対等なフィードバックという形になると思います。「インターフェースの定義」の場合、これもマイクロサービスの自律性から、インターフェースを提供する側からの影響は直接的に、インターフェースを利用する側からの影響はフィードバックという形で現れます。
LinkTracker の例
それでは簡単な例を見てみましょう。前回登場したマイクロサービス LinkTracker の責務はこうでした。
責務
一言で:トラッキング可能な URL を発行し、ハンドリングする。
より詳しく:
発行される URL は特定のソースに紐付けることができる。例えば、実際に送信された一通のメールをソースとみなし、そこに含まれる10個のリンクを全てそのメールと紐付けてトラックできる。
ソースは様々な情報をメタ情報として持ち、API リクエストを通じてそれを保存できる。
ここであらかじめ断っておくと、いきなりこの責務を定義できたわけではありません。実は LinkTracker は当初、MailTracker という名前でした。元々、メーラーという JavaScript の制御が及ばない外部のアクションをトラッキングするために作ったからです(そういう機能が Wantedly の Mother Rails にありました)。しかし、プロトタイプを作ってインターフェースを定義するうちに、「メール」という要素は本質的には不要であることが分かり、LinkTracker としました。結果的にメール以外の箇所でも使われているので、適切な抽象化だったと思います。
この話は、これほどシンプルなマイクロサービスであっても、振る舞いと実装・抽象的な事柄と具体的な事柄の間を行き来することが必要だったという例でした。ちなみに、前回紹介したもう一つ、UsersService の場合は、Wantedly のユーザーというドメインにあらかじめ習熟していたため、完全にトップダウンに定義しました。
話を戻しましょう。仮に実装の影響を受けたとしても、一度定義した責務は尊重し、そこからインターフェースを考える方が良いです。実際に必要な API は3つです。順に見ていきましょう。
1) トラッキングリンクを発行する API
責務から、まずトラッキングリンクを発行する API は必要でしょう。インターフェース定義言語である Protocol Buffer で書くと、以下のようになります(Wantedly では Golang のマイクロサービスは grapi という Protocol Buffer ベースのフレームワークを利用しています)。
rpc CreateTrackingLink (CreateTrackingLinkRequest) returns (TrackingLink) {
option (google.api.http) = {
post: "/sources/{source_id}/tracking_links"
body: "*"
};
}
message CreateTrackingLinkRequest {
string source_id = 1;
string original_url = 2;
}
message TrackingLink {
string url = 1;
}
ここでは入力として、対象となる URL の他に source_id という uuid を受け取れるようになっています。これは責務として書いた「発行される URL は特定のソースに紐付けることができる」に対応します。返却値である TrackingLink
はリダイレクト URL を含んでいます(ここでは表現されていませんが、この URL は token
というパラメータを持っています)。
2) リダイレクトを行う API
リダイレクトを行う API が、次の定義です(厳密に言えば API ではないですが)。
rpc GetTrackingLink (GetTrackingLinkRequest) returns (Redirection) {
option (google.api.http) = {
get: "/t/**"
};
}
message GetTrackingLinkRequest {
string token = 1;
}
message Redirection {
string x_redirect_url = 1;
}
token を受け取ってリダイレクトするというシンプルな定義です。
3) ソース情報を保存する API
最後に、「ソースに関する様々な情報を保存できる」というのが以下の API で実現できます。ここでは、どういった種類のメールがどういったユーザーに送られているのか、というのを保存できるようにしています。
rpc CreateSource (CreateSourceRequest) returns (Source) {
option (google.api.http) = {
post: "/sources"
body: "*"
};
}
message CreateSourceRequest {
message SourceParams {
google.protobuf.UInt32Value user_id = 1;
google.protobuf.StringValue mailer_class = 2;
google.protobuf.StringValue mailer_action = 3;
google.protobuf.StringValue mailer_variation = 4;
google.protobuf.StringValue post_uuid = 5;
}
google.protobuf.StringValue source_id = 1;
SourceParams params = 2;
}
LinkTracker の実装に必要な API はこれで全てです。
grapi を利用したワークフローでは、この Protocol Buffer の定義を所定の箇所に起き、 grapi protoc
を実行して Golang 用の定義などを生成した後、Pull Request にしてレビューします。インターフェースを実装から切り離してレビューすることで、インターフェースが理解しやすいものか、利用する側の要求を満たしているかをチェックしましょう。
UsersService の例
同様に前回紹介した UsersService の例を紹介したいところですが、これはこの記事に書くにはあまりに大きすぎるので省略させてください。
コラム:インターフェースはディテールが大事
最後に少し強調しておきたいことがあります。それは、インターフェースの良し悪しというのはなにができる API かと言う機能面だけではなく、リクエスト・ボディの型(構造)やちょっとした命名などの詳細にかなり影響を受けるということです。
例えば、Suica の自動改札機はなぜ全国津々浦々、少し角度が付いたものになっているのか。設計した山中先生によれば、初めて利用する人でも間違いなく使えるためには、あの UI が重要だったと言います。
実験では驚くような光景がたくさん見られました。今では考えられないことですが、カードを縦に当てる人、アンテナの上で激しく振る人、有人の改札機のようにカードを機械に見せて通ろうとする人、ともかく光っている所にかざす人…。
いろいろな形のアンテナを試してみると、解決策は意外にシンプルな所にありました。「手前に少し傾いている光るアンテナ面」、それだけで多くの人がちゃんと当ててくれることがわかったのです。
...
それらの結果をふまえて作られた改良型による1999年の実験では、読み取り率は劇的に向上し、5割近かったエラー率が、1%以下に下がりました。
http://lleedd.com/blog/2010/11/25/suica_2/
この「ちょっとしたことで使い勝手が変わる」ということについてはソフトウェアも同じです。たとえ論理的に等価なものであっても、良いインターフェースと悪いインターフェースでは関わる人の生産性に十倍程度の差が出ることは普通にあります。なので頻繁に参照されるインターフェースのディテールは特に大事にした方が良いでしょう。
今回は、マイクロサービスの重要な側面であるインターフェースについて話しました。
ところで、設計と言えばパフォーマンス要件を満たすための設計などもありますよね。実は、これまで意図してそのあたりには触れてきませんでした。次回はちょっと足を止めて、これまで扱った責務の定義とインターフェースの設計を、ソフトウェアの設計という観点から全体の中に位置づけて整理しようと思います。