はじめに
Stripe のエンジニアの方が書いた API デザインに関するブログ記事が勉強になったので、その紹介をさせていただきたいと思う。
ID にはタイプがある
ここで言っている「タイプ」とは、ID のプログラミング言語/DB 的な意味での「型」や、その ID がどういう構成を取っているか、どうやって生成されたか、などを含むとする。
そう考えた時、DB に格納するデータを保存したり検索したりする際の ID には、3 つの重要な性質がある:
- 容易に生成できる
- ユニークである
- 人が(それが何を表しているのか)認識しやすい
このうち、「人が認識しやすい」に関しては、他の二つの条件よりも注意が払われることが少ないのではないだろうか。
どういうタイプがあるか
インクリメンタルな ID(Integer)
ID を検討する上で、真っ先に上がるのが、これではないだろうか?
例えばユーザーテーブルがあった時、最初に作成されるユーザーは userId=1
, 次は userId=2
.. と言うふうに、1(または 0)から始まり、1ずつカウントアップしていく方式である。
このタイプの場合、先ほど挙げた 1, 2 の条件は満たすが、3 の条件は満たさない。生成ロジックは、現在の最も大きな値を取得してそれに 1 を足していけばよく、順にカウントアップされるため簡単にユニーク性も担保できる。しかし、32
や 31415
と言う数字を見ただけではそれが何の ID であるのかはわからない。
また、このタイプの ID には、セキュリティの文脈での危険性を孕んでいる。
例えば、自分があるサービスのアカウントを作成したとき、プロフィールページの URL が /profiles/55
だったとしよう1。この時、「自分が 55 だから、54 番目までのユーザーは存在するだろう」と言う推察がなされる。想像するだけなら良いが、仮に悪意を持ったユーザーが、「よっしゃ、ID=XX と ID=YY のユーザーの情報盗み見たるで」と奮起し、たまたまそれらの情報を取得するための API エンドポイントにも脆弱性があった、という偶然が重なると、標的となったユーザーの情報が漏洩してしまうことになる。
さらに、今回の場合、「このサービスは、ユーザーが55人であるほどの規模のサービスである」というビジネス上の重大な情報も丸見えとなる。これが全ての場合において必ずしもマイナスであるとは限らないかもしれないが、基本的には相当の理由がない限りは見せない方が無難であろう。
UUID
UUID は、Universally Unique Identifier
の略であり、32 ビットの英数文字列およびそれらを繋ぐハイフン-
からなる形式で表現される。(e.g. ce3ccc45-02f3-4917-a9ed-2acf1ad3a80d
)
このタイプの場合、先ほどの条件に対してやはり 1, 2 の条件は満たすが、3 の条件は満たさない。生成もツール等を利用して容易に行えるし、ユニーク性も極めて高い。2幸い、インクリメンタルな ID のようなセキュリティ上のリスクはないと考えられて入るが、しかしながら、上の英数文字列の羅列を見て、それが何を表現しているのかは分かりようがない。「UUID こそ最強の ID」と考えていた人々にとっては、受け入れ難い現実かもしれないが、Stripe エンジニアによるとさらに良い ID の実装方法があるとのことなので、次で見て行くことにする。
prefix
+ ランダム値
このタイプは、2パートから構成される。前半部分は prefix
として、それが何のデータであるかをわかるような識別子を付与する。後半部分は、ランダムに生成された英数文字列であり、それらを繋いだ形だ。(e.g. pi_3LKQhvGUcADgqoEM3bh6pslE
3)
同様に、条件を見ていこう。
容易に生成できるか
prefix
パートは、今の処理内容に応じて付与すればよく、ランダムパートは単にランダム英数文字列を生成するだけなので、難なくできるだろう。
ユニークであるか
prefix
パートは関係なく、ランダムパートがあるが故に UUID と同様現実的なユースケースではほぼ問題ないと言っていいだろう。
人が(それが何を表しているのか)認識しやすいか
ここが本題である。
prefix
パートにそのデータが何についてのものであるかという情報を搭載することが可能になっている。 それによって、この ID をみた人間に取って識別子やすい(=human readable)なものとなっている。
この prefix
パートがあるおかげでどんなメリットがあるかについて、API 利用者側の視点、API 提供者側の視点でそれぞれ見ていきたい。
API 利用者側にとってのメリット
API 利用者、つまりこの API を利用してサービス開発を行う Developer の視点に立つ。
ある API リクエストした結果、4xx エラーが返ってきたとする。何かリクエストに不備があった可能性が考えられるため、自分のリクエスト内容を確認してみる。
$pi = $stripe->paymentIntents->retrieve(
$id,
[],
['stripe_account' => 'cus_1KrJdMGUcADgqoEM']
);
ふむ、stripe_account
になぜか誤って customer
の ID を渡してしまっていることが原因のようだ。
そのためどうしてその ID が渡ってしまったのかの調査に移ることができる。
一方で、次のようなリクエストだとしたらどうだろう?
$pi = $stripe->paymentIntents->retrieve(
$id,
[],
['stripe_account' => '1KrJdMGUcADgqoEM']
);
少なくともこのリクエスト内容からだけでは、「stripe_account
に誤って customer
の ID を渡してしまっていること」は判別できない。そのため、原因調査により時間と労力を割くことになるだろう。
API 提供者側にとってのメリット
ここでいう API 提供者とは、IT サポートに携わる方々のことである。
あるサービスをとあるユーザーが利用しているときに問題が発生して一時的に利用できなくなった。IT サポートに連絡を入れると、L1 → L2 に問題がエスカレーションされた。
まず、次のようなリクエスト内容だった場合:
# 前提: このメソッドでは、payment_method として、'pm_**', 'card_**', 'src_**' を指定できるとする
$pi = $stripe->paymentIntents->create([
'amount' => 1000,
'currency' => 'usd',
'payment_method' => 'card_1LaRQ7GUcADgqoEMV11wEUxU',
]);
payment_method
に対してカードの ID が指定されていることがわかる。そのため、とりあえずカード情報のテーブルを確認しに行ったりといった調査に繋げることができる。
一方で、次のようなリクエストだとしたらどうだろう?
# 前提: このメソッドでは、payment_method として、'pm_**', 'card_**', 'src_**' を指定できるとする
$pi = $stripe->paymentIntents->create([
'amount' => 1000,
'currency' => 'usd',
'payment_method' => '1LaRQ7GUcADgqoEMV11wEUxU',
]);
payment-method
に指定しているのがどの種類のメソッドか判別できないため、少なくとも当たりが出るまで関連テーブルを全検索しに行く、という作業が必要になるかもしれない。
また、余談だが、Stripe では、独自ブラウザー上で対象の Object をクリックすると、その種別に応じたページに遷移することができ、デバッグがかなり効率的にできるようになっているらしい。いかに「ぱっと見でわかる」ことがデバッグにおける負荷を下げることができるかが垣間見れる。
「prefix
+ ランダム値」と同様の効果を別の表現方法で実現
prefix
パートを ID そのものに入れるのではなく、独立した情報としてもつ。
以下の例では、type
にその情報を詰め込んでいる。
# シンプル
'payment_method' => 'card_1LaRQ7GUcADgqoEMV11wEUxU'
# (意味もなく)複雑
'payment_method' => [
'type' => 'card',
'id' => '1LaRQ7GUcADgqoEMV11wEUxU'
]
しかし、これは API(と DB)のデザインの複雑度を上げているだけで、無駄なデザインである。「prefix + ランダム値
」がベターなソリューションであることは明らかである。4
終わりに
今回紹介したものは、比較的容易に自分の携わるサービスに導入できそうな事例であり、労力に比べてインパクトは大きい、まさに ROI の高いものであると感じた。
API デザインにおいては、利用者のことのみでなく、API およびサービス提供者側の運用までも考慮に入れたデザインをすることの重要性を感じた。
ほんの小さなことであるが、このような積み重ねが、サービス全体の価値を高めていくのだろう。