このアーティクルでは、RESTful HTTPの基礎に関する簡潔なオーバービューを提供し、RESTful HTTPアプリケーションを構築する際に開発者が直面する典型的な問題について議論する。これらを通じて、RESTアーキテクチャ・スタイルを実際にどうのように利用すればいいかを示す。この中には、URIの決め方や、リソースへ統一インターフェースを通じてどのようにアクセスするかといった議論、PUTやPOSTを使ってどのようにCRUD以外の操作をサポートするかなど、共通的に利用されるアプローチが含まれる。
RESTはスタイルであり標準ではない。RESTのRFCもなければ、RESTプロトコルの仕様というようなものもない。RESTアーキテクチャスタイルはHTTPおよびURIの仕様の主要な著者の一人であるRoy Fielding氏の博士論文の中ではじめて述べられた。RESTのようなアーキテクチャスタイルは、アプリケーションによって実装される複数のハイレベルなアーキテクチャ上の解決策からなる。特定のアーキテクチャスタイルを実装したアプリケーション群は同じパターンを利用し、キャッシュや分散戦略などのアーキテクチャの要素を同じように利用する。Roy Fielding氏は「RESTを遅延時間やネットワークを最小限にしながら、同時にコンポーネント実装の独立性と拡張性を最大限に高める」ように試みたアーキテクチャスタイルであると述べている。
RESTはWebテクノロジーに非常に強く影響されているものの、理論的にはHTTPに依存していない。しかし実際には、HTTPがRESTの唯一の実装である。この理由により、このアーティクルでは、HTTPによって実装されたRESTをとりあげる。これは、しばしばRESTful HTTPと呼ばれるものだ。
RESTful HTTPの背後にあるアイデアは、WEBの既存の機能や能力を利用するというものだ。RESTは新しいテクノロジーやコンポーネント、サービスを開発していない。RESTful HTTPは既存のWeb標準をより良く利用するための原則や制約を定義しているのだ。
リソース
リソースはRESTのなかでキーとなる概念だ。これは、リモートでアクセス可能なアプリケーションのオブジェクトのことだ。リソースは識別子からなる。リモートからアクセスもしくは操作可能なものはすべてリソースになりえる。静的なリソースも存在する。つまりリソースの状態は時間がたっても変化しない。一方、他のリソースは時間がたつにつれ、状態が劇的に変化することもありえる。どちらのタイプのリソースも正しい。
例えば、図1で示されているクラスは簡単にこのようなリソースにマッピングできる。Hotel
や
といったエンティティクラスをリソースにマッピングするのは、オブジェクト指向の設計者にとって、理解しやすいとはいえないだろう。コーディネーションやトランザクション、他のクラスの制御をおこなうコントロールクラスについても同じことが言える。Room
図1: 分析モデル例
この分析モデルは、リソースを識別するための良い出発点だ。しかし、必ずしも1対1のマッピングが存在するわけではない。例えば、
という操作は、リソースとして設計することもできる。さらに言うと、複数のエンティティ(の一部)を表すリソースも存在しえる。リソース設計の主要な目的は、ネットワークに関わる側面であり、オブジェクトモデルではない。
重要なリソースはすべて、一意の識別子によってアクセス可能である。RESTful HTTPは、URIをリソースの識別に利用する。URIは、Webでは共通の識別子を提供する。クライアントが参照されたリソースにアクセスするために必要なすべてがURIには含まれている。
リソース識別子にどのように名前をつけるか?
RESTful HTTPでは、URIパスをどのように構成しなければならないかを規定していないが、実際には、特定のURIパスの命名方式がよく利用される。URIの命名方式によって、アプリケーションのデバッグやトレースが容易になる。URIはリソースの種類の名称のあとに、特定のリソースを指し示す識別子を含むことが多い。このようなURIは処理すべき業務上の操作を示す動詞を含まない。リソースの所在を示すためだけに利用されるのだ。図2の(a1)は、 Hotel
リソースを指し示すURIの一例だ。同じHotel
リソースは(a2)のURIでもアクセス可能だ。リソースは一つ以上のURIによって参照される。
(a1) http://localhost/hotel/656bcee2-28d2-404b-891b (a2) http://127.0.0.1/hotel/656bcee2-28d2-404b-891b (b) http://localhost/hotel/656bcee2-28d2-404b-891b/Room
/4 (c) http://localhost/hotel/656bcee2-28d2-404b-891b/Reservation
/15 (d) http://localhost/hotel/656bcee2-28d2-404b-891b/Room
/4/Reservation
/15 (e) http://localhost/hotel/656bcee2-28d2-404b-891b/Room
/4/Reservation
/15v7 (f) http://localhost/hotel/656bcee2-28d2-404b-891bv12
図2: リソースのアドレスの例
URIは、リソース表現の間の関係を定義するためにも利用される。例えば、Hotel
表現は、割り当てられたRoom
をRoom
のidによって参照するのではなく、Room
リソースのURIを使って参照する。単純なidを利用すると、呼び出し元がリソースにアクセスする際にURIを構築しなければならない。呼び出し元は、ホスト名やベースURIパスといった追加のコンテクスト情報なしではリソースにアクセスすることができない。
ハイパーリンクは、リソースによってナビゲートを行うためにクライアントによって利用される。RESTful APIはハイパーテキスト主導である。つまり、Hotel
の表現を得ることで、クライアントは割り当てられたRoom
表現や、割り当てられた
表現にアクセスすることが可能となるのだ。Reservation
実際には、図1で示されるようなクラスはビジネスオブジェクトという意味でマップされることが多い。このことは、ビジネスオブジェクトのライフサイクルを通してURIは変わらないことを意味する。新しいリソースが作成されたときには、新しいURIが割り当てられる。リソースを削除した後には、対応するURIは無効となる。(a)、 (b)、 (c)、(d)のURIはそのような識別子の例だ。一方、オブジェクトのスナップショットを参照するために、URIが利用されることもある。例えば、(e)および(f)のURIは、URIの中にバージョン識別子を含むことで、このようなスナップショットを参照している。
URIは、"サブ"リソースを表現するためにも利用される。(b)、(c)、(d)、(e)がその例だ。集約されたオブジェクトはサブリソースとしてマップされることが多い。例としては、Room
は、Hotel
によって集約されている。集約オブジェクトは、独自のライフサイクルを持っておらず、親オブジェクトが削除された際には、すべての集約オブジェクトは同時に削除される。
しかし、“サブ"リソースがある親リソースから他の親リソースに移動する可能性があるのであれば、URIの中に親リソースの識別子を含むべきではない。例えば、図1のReservation
は他のRoom
に割り当てられる可能性がある。(d)で示されているようなRoom
識別子を含むReservation
のURIは、もしRoom
インスタンスの識別子が変わった際には、無効になってしまう。もしそのようなReservation
のURIが、他のリソースから参照されていた場合には問題となる。URIが無効になることを避けるためには、Reservation
は、(c)のようなアドレスで表現できる。
通常は、リソースURIはサーバによって制御される。クライアントはリソースにアクセスするためにリソースの名前空間の構造を理解する必要はない。例えば、(c)のようなURI構造を利用しようが、(d)のようなURI構造を利用しようが、クライアントにとっては同じなのだ。
統一リソースインターフェース
システムアーキテクチャ全体をシンプルにするために、RESTアーキテクチャスタイルは統一インターフェースというコンセプトを含んでいる。統一インターフェースはリソースにアクセスおよび操作を行うための、洗練された操作からなる。同じインターフェースがリソースにかかわらず利用される。クライアントが、Hotel
リソースとやりとりする際にも、Room
リソースや、CreditScore
リソースとやりとりする際にも、インターフェースは同じだ。統一インターフェースは、リソースURIとは独立している。利用可能なメソッドを記述するために、IDLのようなファイルは必要ない。
RESTful HTTPのインターフェースは、広く利用されており、非常に一般的なものだ。それは、ブラウザがページを取得したりデータを送信したりする際に利用されるGETやPUT、POSTといった標準のHTTPメソッドからなる。残念なことに、多くの開発者がRESTfulアプリケーションを実装することとは、HTTPを直接利用することだと信じているが、それは違う。例えば、HTTPメソッドはHTTP仕様に従って実装されなければならない。GETメソッドをオブジェクトの作成や変更に利用することは、HTTP仕様に違反している。
統一インターフェースの適用
Fielding氏の論文には、いつ、どのように異なるHTTPの動詞を利用すればよいのかを詳細に記述した表や、リストなどは含まれない。GETやDELETEなどの多くのメソッドは、HTTP仕様を読めば明確になる。しかしPOSTや部分的な更新などに関しては、そうではない。実際には、部分的な更新処理を行うにはいくつかのアプローチがあり、後にそのことについて議論する。
もっとも重要なメソッドであるGET、DELETE、PUT、POSTの典型的な利用方法について表1に記載する。
重要 |
典型的な利用方法 |
典型的なステータスコード |
安全か? |
冪等か? |
GET |
- 表現を取得する - 更新されていた場合、表現を取得する (キャッシング) |
200 (OK) - レスポンスとして 204 (no content) - リソースが 301 (Moved Permanently) - 303 (See Other) - ロードバランシングなど 304 (not modified) - リソースが 400 (bad request) - 不正なリクエストを 404 (not found) - リソースが 406 (not acceptable) - サーバは 500 (internal server error) - 汎用的な 503 (Service Unavailable) - サーバが |
はい |
はい |
DELETE |
- リソースの削除 |
200 (OK) - リソースが削除された 301 (Moved Permanently) - リソース URIが 400 (bad request) - 不正なリクエストを 500 (internal server error) - 汎用的な |
いいえ |
はい |
PUT |
- クライアント側で管理するインスタンスIDでリソースを作成する - 置き換えによってリソースを更新する - 更新されていなければ、置き換えによってリソースを更新する(楽観的ロック) |
200 (OK) - 既存のリソースが 301 (Moved Permanently) - リソースURIが 303 (See Other) - ロードバランシングなど 400 (bad request) - 不正なリクエストを 404 (not found) - リソースが 406 (not acceptable) - サーバは 409 (conflict) - 一般的な衝突 412 (Precondition Failed) 条件付きの更新時の 415 (unsupported media type) - 500 (internal server error) - 汎用的な エラーレスポンス 503 (Service Unavailable) - サーバは 現在、リクエストを 処理できない |
いいえ |
はい |
POST |
- サーバ側で管理した(自動生成の)インスタンスidでリソースを作成する - サブリソースを作成する - リソースを部分的に更新する - 更新されていなければ、リソースを部分的に更新する(楽観的ロック) |
200 (OK) - 既存のリソースが 301 (Moved Permanently) - リソースURIが 400 (bad request) - 不正なリクエストを 500 (internal server error) - 汎用的な |
いいえ |
いいえ |
表1: 統一インターフェースの例
表現
リソースは常に表現によって操作される。リソースがネットワーク越しに送信されることはない。その代わりにリソースの表現が送信される。表現は、該当データを表すデータとメタデータからなる。例えば、HTTPメッセージのContent-Typeヘッダは、メタデータ属性の一例だ。
図3はJavaによる表現の取得方法を表している。この例では、著者によってメンテナンスされているJavaのHTTPライブラリであるxLightwebのHTTPClientを利用している。
HttpClient httpClient = new HttpClient(); IHttpRequest request = new GetRequest(centralHotelURI); IHttpResponse response = httpClient.call(request);
図3: 表現を取得するJavaによる例
HTTPクライアントのcallメソッドを実行することにより、HTTPリクエストが送信される。このリクエストではHotel
リソースの表現をリクエストしている。レスポンスとして返ってきた表現(図4)はContent-Typeヘッダを含んでおり、ボディ全体のメディアタイプを示している。
リクエスト: GET /hotel/656bcee2-28d2-404b-891b HTTP/1.1 Host: localhost User-Agent: xLightweb/2.6 レスポンス: HTTP/1.1 200 OK Server: xLightweb/2.6 Content-Length: 277 Content-Type: application/x-www-form-urlencoded classification=Comfort&name=Central&Room
URI=http%3A%2F%2Flocalhost%2Fhotel%2F 656bcee2-28d2-404b-891b%2FRoom
%2F2&Room
URI=http%3A%2F%2Flocalhost%2Fhotel%2F6 56bcee2-28d2-404b-891b%2FRoom
%2F1
図4: RESTful HTTPのやりとり
どのようにして特定の表現をサポートするか?
大きなデータを受信することを避けるために、限定した属性の組だけを受信すべき時もある。実際には、図5に示されているように特定の属性だけの指定をサポートすることによって、表現のどの属性を受け取るか決定するアプローチもある。
リクエスト: GET /hotel/656bcee2-28d2-404b-891b/classification HTTP/1.1 Host: localhost User-Agent: xLightweb/2.6 Accept: application/x-www-form-urlencoded レスポンス: HTTP/1.1 200 OK Server: xLightweb/2.6 Content-Length: 26 Content-Type: application/x-www-form-urlencoded; charset=utf-8 classification=Comfort
図5: 属性のフィルタリング
図5で示されているGET呼び出しは、一つの属性だけを要求している。複数の属性を要求するためには、要求する属性を図6のようにコンマで区切ればよい。
リクエスト: GET /hotel/656bcee2-28d2-404b-891b/classification,name HTTP/1.1 Host: localhost User-Agent: xLightweb/2.6 Accept: application/x-www-form-urlencoded レスポンス: HTTP/1.1 200 OK Server: xLightweb/2.6 Content-Length: 43 Content-Type: application/x-www-form-urlencoded; charset=utf-8 classification=Comfort&name=Central
図6: 複数の属性のフィルタリング
要求する複数の属性を特定するための他の方法としては、クエリ・パラメータを使って要求する属性を図7のように並べる方法がある。クエリ・パラメータは検索条件や、より複雑なフィルタや検索基準を表すのにも利用される。
リクエスト: GET /hotel/656bcee2-28d2-404b-891b?reqAttr=classification&reqAttr=name HTTP/1.1 Host: localhost User-Agent: xLightweb/2.6 Accept: application/x-www-form-urlencoded レスポンス: HTTP/1.1 200 OK Server: xLightweb/2.6 Content-Length: 43 Content-Type: application/x-www-form-urlencoded; charset=utf-8 classification=Comfort&name=Central
図7: 検索文字列
上の例では、サーバは常にapplication/x-www-form-urlencoded
メディア・タイプでエンコードされた表現を返している。本質的には、このメディア・タイプはエンティティをキーと値のペアの組としてエンコードする。キーと値によるアプローチは非常に理解しやすい。しかし残念なことに、より複雑なデータ構造をエンコードしなければならない場合には、うまくフィットしない。さらに、このメディア・タイプはIntegerやBoolean、Date
といったスカラデータ型のバインディングをサポートしない。この理由により、XMLやJSON、Atomなどがリソース表現として、よく利用される(JSONもデータ
型のバインディングを定義しないが)。
HttpClient httpClient = new HttpClient(); IHttpRequest request = new GetRequest(centralHotelURI); request.setHeader("Accept", "application/json"); IHttpResponse response = httpClient.call(request); String jsonString = response.getBlockingBody().readString(); JSONObject jsonObject = (JSONObject) JSONSerializer.toJSON(jsonString);Hotel
Hotel
= (Hotel) JSONObject.toBean(jsonObject, Hotel.class);
図8: JSON表現のリクエスト
リクエストのacceptヘッダーを設定することにより、クライアントは特定の表現エンコーディングを要求することができる。図8では、どのようにしてapplication/json
メディア・タイプをリクエストするのかを示している。図9に示したレスポンスメッセージは、JSONlibライブラリを利用して、Hotel
Beanにマッピングされている。
リクエスト: GET /hotel/656bcee2-28d2-404b-891b HTTP/1.1 Host: localhost User-Agent: xLightweb/2.6 Accept: application/json レスポンス: HTTP/1.1 200 OK Server: xLightweb/2.6 Content-Length: 263 Content-Type: application/json; charset=utf-8 {"classification":"Comfort", "name":"Central", "Room
URI":["http://localhost/hotel/656bcee2-28d2-404b-891b/Room
/1", "http://localhost/hotel/656bcee2-28d2-404b-891b/Room
/2"]}
図9: JSON表現
どのようにエラーを伝えるべきか?
サーバが要求された表現をサポートしていない場合には何がおこるだろう?図10では、リソースのXML表現が要求された際のHTTPのやりとりを示している。サーバが要求された表現をサポートしていない時には、リクエストに対する処理を拒否することを示すHTTP 406
レスポンスを返す。
リクエスト: GET /hotel/656bcee2-28d2-404b-891b HTTP/1.1 Host: localhost User-Agent: xLightweb/2.6 Accept: text/xml レスポンス: HTTP/1.1 406 No match for accept header Server: xLightweb/2.6 Content-Length: 1468 Content-Type: text/html; charset=iso-8859-1HTTP ERROR: 406
No match for accept header...
図10: サポートされない表現
RESTful HTTPサーバアプリケーションは、HTTP仕様に従ってステータスコードを返さなければならない。ステータスコードの最初の桁は、結果の種別を示す。1xx
は暫定的なレスポンスを示し、2xx
は成功したレスポンスを表す。3xx
はリダイレクト、4xx
はクライアントエラー、5xx
はサーバエラーをそれぞれ表す。レスポンスコードを返さなかったり、常に200
レスポンスを返しておいて、ボディにアプリケーション固有のレスポンスを含めるのは悪い発想だ。
Clientエージェントや仲介者は常にレスポンスコードを評価しなければならない。例えば、xLightwebのHttpClientは、デフォルトでHTTP接続をプールする。HTTPのやりとりが終了したあとに、永続的なHTTP接続を内部のプールに返却し、再利用する。この動作は、健全な接続の場合のみ行われる。例えば、5xxステータスを受け取った場合には、接続をプールに返さない。
特定のクライアントは、より正確なステータスコードを必要とすることがある。これを実現するための一つのアプローチはX-Headerを付け加えて、そこで図11のようにHTTPステータスコードを詳細化するというものだ。
リクエスト: POST /Guest/ HTTP/1.1 Host: localhost User-Agent: xLightweb/2.6 Content-Length: 94 Content-Type: application/x-www-form-urlencoded zip=30314&lastName=Gump&street=42+Plantation+Street&firstName=Forest&country=US& city=Baytown&state=LA レスポンス: HTTP/1.1 400 Bad Request Server: xLightweb/2.6 Content-Length: 55 Content-Type: text/plain; charset=utf-8 X-Enhanced-Status: BAD_ADDR_ZIP AddressException: bad zip code 99566
図11: 拡張されたステータスコード
詳細なエラーコードはプログラミングのエラーを調査するためにのみ必要であることが多い。HTTPステータスコードは、詳細なエラーコードよりも表現力で劣ることが多いが、たいていの場合、クライアントがエラーを正しく処理するためには十分である。他のアプローチとしては、詳細なエラーコードをレスポンスのボディに含めることが考えられる。
PUTを使うべきか、POSTを使うべきか?
よくあるRPCアプローチとは異なり、HTTPメソッドはメソッド名が変わらない。冪等性や安全性などのプロパティがHTTPメソッドでは重要な役割を果たす。冪等性と安全性はHTTPメソッドによって変わってくる。
HttpClient httpClient = new HttpClient(); String[] params = new String[] { "firstName=Forest", "lastName=Gump", "street=42 Plantation Street", "zip=30314", "city=Baytown", "state=LA", "country=US"}; IHttpRequest request = new PutRequest(gumpURI, params); IHttpResponse response = httpClient.call(request);
図12: PUTメソッドの実行
例えば、図12と図13では、新しいGuest
リソースを作成するためのPUTによるやりとりを示している。PUTメソッドは、与えられたリクエストURIに、封入されたリソースを保存する。URIはクライアント側で決定される。もしリクエストURIが既に存在しているリソースを示していたとすると、このリソースは新しいリソースで置き換えられる。この理由からPUTメソッドは新しいリソースを作成するためだけではなく、既存のリソースを更新するためにも利用される。しかし、PUTを使うとリソースの完全な状態を送信しなければならない。zip
フィールドを更新するリクエストには、Guest
リソースのfirstName
やcity
といった、その他のすべてのフィールドを含まなければならない。
リクエスト: PUT Hotel/guest/bc45-9aa3-3f22d HTTP/1.1 Host: localhost User-Agent: xLightweb/2.6 Content-Length: 94 Content-Type: application/x-www-form-urlencoded zip=30314&lastName=Gump&street=42+Plantation+Street&firstName=Forest&country=US& city=Baytown&state=LA レスポンス: HTTP/1.1 200 OK Server: xLightweb/2.6 Content-Length: 36 Content-Type: text/plain; charset=utf-8 Location: http://localhost/guest/bc45-9aa3-3f22d guestリソースは更新されました
図13: HTTP PUTによるやりとり
PUTメソッドは冪等である。冪等なメソッドとは、正常に実行されたリクエストの結果が、実行された回数と独立であることを意味している。例えば、Hotel
リソースを更新するためにPUTメソッドを好きな回数だけ実行しても、正常に実行された結果は常に同じである。もし2つのPUTメソッドが同時に実行されたとすると、そのうちの1つが勝ち、リソースの最終的な状態を決定する。DELETEメソッドも同様に冪等である。もし、PUTメソッドとDELETEメソッドが同時に実行された場合は、リソースは更新されるか削除されるかのどちらかで、その間であることはない。
PUTやDELETEの実行が成功したかどうかわからず、409 (Conflict)
や417 (Expectation Failed)
といったステータスコードを受け取っていないのであれば、再実行すればよい。リクエストの重複を避けるために追加の信頼性のあるプロトコルは必要ない。一般的には、リクエストの重複は問題とならない。
このことはPOSTメソッドにはあてはまらない。なぜならPOSTメソッドは冪等ではないからだ。同じPOSTメソッドを2度実行する際には気をつけなければならない。冪等でないので、ブラウザはPOSTリクエストをリトライする際には警告ダイアログを常にポップアップさせるのだ。POSTメソッドはクライアント側でインスタンス固有のIDを指定することなしにリソースを作成するために利用される。例えば、図14は、POSTメソッドを実行することでHotel
リソースを作成する際のHTTPのやりとりを示している。典型的には、クライアントはURIベースパスとリソースの種類の名前を含んだURIを使ったPOSTリクエストを送信する。
リクエスト: POST /Hotel
HTTP/1.1 Host: localhost User-Agent: xLightweb/2.6 Content-Length: 35 Content-Type: application/x-www-form-urlencoded; charset=utf-8 Accept: text/plain classification=Comfort&name=Central レスポンス: HTTP/1.1 201 Created Server: xLightweb/2.6 Content-Length: 40 Content-Type: text/plain; charset=utf-8 Location: http://localhost/hotel/656bcee2-28d2-404b-891bHotel
リソースは作成されました。
図14: HTTP POSTのやりとり (作成)
POSTメソッドは、リソースの一部分を更新するために利用されることもよくある。例えば、Hotel
リソースを更新するためにclassification
だけを含んだPUTリクエストを送信することはHTTPに違反している。これはPOSTメソッドには当てはまらない。POSTメソッドは冪等でなければ、安全でもない。図15は、このようなPOSTメソッドを使った部分的な更新を示している。
リクエスト:
POST /hotel/0ae526f0-9c3d HTTP/1.1
Host: localhost
User-Agent: xLightweb/2.6
Content-Length: 19
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Accept: text/plain
classification=First+Class
レスポンス:
HTTP/1.1 200 OK
Server: xLightweb/2.6
Content-Length: 52
Content-Type: text/plain; charset=utf-8
Hotel
リソースは更新されました(classification)
図15: HTTP POSTのやりとり(更新)
部分的な更新はPATCHメソッドを利用しても実現できる。PATCHメソッドはリソースに部分的な変更を反映させるための特別なメソッドだ。PATCHリクエストは、リクエストURIで指定されるリソースに対して適用されるパッチドキュメントを含む。しかし、PATCHのRFCはまだドラフトの段階だ。
HTTPのキャッシュを利用する
拡張性を改善し、サーバに対する負荷を軽減するためにRESTful HTTPアプリケーションは、WEB基盤のキャッシュ機能を利用することができる。HTTPは、キャッシュをWEB基盤の不可欠な部分だととらえている。例えば、HTTPプロトコルはキャッシュをサポートするための特定のメッセージヘッダを定義している。もしサーバがそのようなヘッダを設定したとすると、HTTPクライアントやWebキャッシング・プロキシなどのクライアントは効率のよいキャッシュ戦略をサポートすることができるようになる。
HttpClient httpClient = new HttpClient(); httpClient.setCacheMaxSizeKB(500000); IHttpRequest request = new GetRequest(centralHotelURI + "/classification"); request.setHeader("Accept", "text/plain"); IHttpResponse response = httpClient.call(request); String classification = response.getBlockingBody.readString(); // ... その後、リクエストを再実行する response = httpClient.call(request); classification = response.getBlockingBody.readString();
図16: クライアント側のキャッシュのやりとり
例えば、図16は繰り返し実行されるGET呼び出しを示している。キャッシュ最大サイズを0より大きく設定することにより、HTTPClientのキャッシュサポートが有効になる。もしレスポンスが、Expires
やCache-Control: max-age
のような新鮮さ(フレッシュネス)を表すヘッダを含んでいる場合、レスポンスはHTTPClientによってキャッシュされる。これらのヘッダは関連する表現がどれだけの期間、新鮮なのかを示している。期間中に同じリクエストが発行された場合、HTTPClientはキャッシュを使ってリクエストを処理し、ネットワーク経由の呼び出しを繰り返さないようにする。ネットワークに関して言えば、図17のように、全体でたった一回だけのHTTPのやりとりが発生するだけだ。WEBプロキシのようなキャッシュの仲介役も同様の動作をする。この場合は、キャッシュはクライアント間で共有される。
リクエスト: GET /hotel/656bcee2-28d2-404b-891b/classification HTTP/1.1 Host: localhost User-Agent: xLightweb/2.6 Accept: text/plain レスポンス: HTTP/1.1 200 OK Server: xLightweb/2.6 Cache-Control: public, max-age=60 Content-Length: 26 Content-Type: text/plain; charset=utf-8 comfort
図17: 期限ヘッダを含むHTTPレスポンス
静的なリソースに関しては、この期限モデルはうまく機能する。残念なことに、動的なリソースでリソースの状態が頻繁かつ不定期に変更されるようなものに関しては向かない。HTTPは、動的なリソースのキャッシュをサポートするために、Last-Modified
とETag
といった検証ヘッダを利用する。期限モデルとは異なり、検証モデルでは、ネットワーク越しのリクエストを省くことはしない。その代わり条件付きのGETを使うことでコストの高い処理であるレスポンス・ボディの生成や送信を省くのだ。図18 (2.リクエスト)の条件付きGETでは、キャッシュされたレスポンスの最終更新日を示すLast-Modifiedヘッダを追加で含んでいる。もしリソースに変化がなければ、サーバは304 (Not Modified)
レスポンスを返信する。
1. リクエスト: GET /hotel/656bcee2-28d2-404b-891b/Reservation
/1 HTTP/1.1 Host: localhost User-Agent: xLightweb/2.6 Accept: application/x-www-form-urlencoded 1. レスポンス: HTTP/1.1 200 OK Server: xLightweb/2.6 Content-Length: 252 Content-Type: application/x-www-form-urlencoded Last-Modified: Mon, 01 Jun 2009 08:56:18 GMT from=2009-06-01T09%3A49%3A09.718&to=2009-06-05T09%3A49%3A09.718&guestURI= http%3A%2F%2Flocalhost%2Fguest%2Fbc45-9aa3-3f22d&Room
URI=http%3A%2F%2F localhost%2Fhotel%2F656bcee2-28d2-404b-891b%2FRoom
%2F1 2. リクエスト: GET /hotel/0ae526f0-9c3d/Reservation
/1 HTTP/1.1 Host: localhost User-Agent: xLightweb/26. Accept: application/x-www-form-urlencoded If-Modified-Since: Mon, 01 Jun 2009 08:56:18 GMT 2. レスポンス: HTTP/1.1 304 Not Modified Server: xLightweb/2.6 Last-Modified: Mon, 01 Jun 2009 08:56:18 GMT
図18: 検証によるキャッシュ
サーバ側にアプリケーションの状態を保持しない
RESTful HTTPでのインタラクションはステートレスでなければならない。このことが意味するのは、それぞれのリクエストが、リクエストを処理するのに必要なすべての情報を含んでいなければならないということだ。クライアントがアプリケーションの状態に関して責任をもつのだ。RESTfulサーバはリクエストをまたいで、アプリケーションの状態を保持する必要はない。サーバはリソースの状態に責任をもつのであって、アプリケーションの状態には責任をもたない。サーバや、仲介役はそれぞれのリクエストとレスポンスを独立したものとして理解することができる。Webキャッシュ・プロキシはメッセージを正しく処理し、キャッシュを管理するための情報をすべて持っている。
このステートレスのアプローチは、高い拡張性と可用性をもったアプリケーションを実装するためには、基礎となる原則だ。一般的に、ステートレスであることによって、それぞれのクライアントのリクエストを、異なるサーバで処理することが可能となる。それぞれのリクエストに対して、あるサーバの処理が他のサーバの処理によって置き換えられる。トラフィックが増えてくれば、新しいサーバを追加すればよいのだ。もしサーバが故障すれば、クラスタから取り除けばよい。ロードバランシングとフェイルオーバに関してより詳細な説明が必要であれば、Server load balancing architecturesという記事を参照のこと。
CRUD以外の操作のサポート
開発者は、CRUD(Create-Read-Update-Delete)以外のリソースに対する操作をどのようにマップしたらよいのか疑問を持つことが多い。明らかにCreate、Read、Update、Delete
の各操作は、リソースに対するメソッドとしてうまくマッピングできる。しかしRESTful HTTPは、CRUD中心のアプリケーションに限定されるものではない。
図19: RESTful HTTPリソース
例えば、図19のcreditScoreCheck
クラスは、CRUD以外のオペレーションとしてcreditScore
(...)を提供している。このオペレーションは、住所を受け取り、スコアを計算し、その値を返している。このようなオペレーションは、計算結果をあらわすCreditScoreResource
によって実装される。図20では、処理すべき住所情報を渡して、CreditScoreResource
を受け取るGET呼び出しを示している。クエリ・パラメータは、CreditScoreResource
を特定するために利用されている。GETメソッドは安全でキャッシュできるので、CreditScore CheckのcreditScore
(...)メソッドの非機能的な振る舞いに非常に適合する。スコアの計算結果は一定期間キャッシュすることができる。図20で示されているように、レスポンスにはクライアントや仲介役がレスポンスをキャッシュできるようにキャッシュヘッダを含んでいる。
リクエスト:
GET /CreditScore
/?zip=30314&lastName=Gump&street=42+Plantation+Street&
firstName=Forest&country=US&city=Baytown&state=LA HTTP/1.1
Host: localhost
User-Agent: xLightweb/2.6
Accept: application/x-www-form-urlencoded
レスポンス:
HTTP/1.1 200 OK
Server: xLightweb/2.6
Content-Length: 31
Content-Type: application/x-www-form-urlencoded
Cache-Control: public, no-transform, max-age=300
scorecard=Excellent&points=92
図20: CRUD以外のHTTP GETによるやりとり
この例は、GETメソッドの限界も示している。HTTP仕様のなかではURLの長さの最大値を明記していないが、実際的な限界が、クライアント側、仲介役、サーバにより存在する。従って、GETのクエリ・パラメータを利用して大きなエンティティを送信した場合、URLの長さに制約をもつ仲介役やサーバによって処理が失敗する可能性がある。
代替案は、指示されれば同じくキャッシュ可能であるPOSTメソッドを使うというものだ。図21のようにまずは、POSTリクエストで仮想的なリソースCreditScoreResource
を作成する。入力となる住所は、text/card mimeタイプでエンコードさている。スコアを計算し終えた後に、サーバは作成されたCreditScoreResource
のURIを含む201(created)レスポンスを返す。この例で示されいるようにPOSTメソッドのレスポンスは、指示されればキャッシュすることができる。GETリクエストを実行することで、クレジットスコアが取得される。このGETレスポンスもキャッシュ制御のヘッダを含んでいる。もしクライアントがこの2つのリクエストをすぐに再実行すると、すべてのレスポンスはキャッシュによって処理される。
1. リクエスト: POST /CreditScore
/ HTTP/1.1 Host: localhost User-Agent: xLightweb/2.6 Content-Length: 198 Content-Type: text/x-vcard Accept: application/x-www-form-urlencoded BEGIN:VCARD VERSION:2.1 N:Gump;Forest;;;; FN:Forest Gump ADR;HOME:;;42 Plantation St.;Baytown;LA;30314;US LABEL;HOME;ENCODING=QUOTED-PRINTABLE:42 Plantation St.=0D=0A30314 Baytown=0D=0ALA US END:VCARD 1. レスポンス: HTTP/1.1 201 Created Server: xLightweb/2.6 Cache-Control: public, no-transform, max-age=300 Content-Length: 40 Content-Type: text/plain; charset=utf-8 Location: http://localhost/CreditScore
/l00000001-l0000005c the credit score resource has been created 2. リクエスト: GET /CreditScore
/l00000001-l0000005c HTTP/1.1 Host: localhost User-Agent: xLightweb/2.6 2. レスポンス: HTTP/1.1 200 OK Server: xLightweb/2.6 Content-Length: 31 Content-Type: application/x-www-form-urlencoded Cache-Control: public, no-transform, max-age=300 scorecard=Excellent&points=92
図21: CRUD以外のHTTP POSTによるやりとり
このアプローチの類似パターンもいくつか存在する。201
レスポンスを返すかわりに、301 (Moved Permanently)
リダイレクト・レスポンスを返すこともできる。301リダイレクト・レスポンスはデフォルトでキャッシュ可能だ。他の類似パターンとしては、新しく作成されたCreditScoreResource
の表現を201
レスポンスに含めることで、2度目のリクエストを行わなくてよいようにするというものもある。
結論
SOAPやCORBAといった多くのSOAアーキテクチャは、図1で示したようなクラスモデルを、多かれ少なかれリモートアクセスのために1対1でマップしようとする。典型的には、このようなSOAアーキテクチャはプログラミング言語のオブジェクトを透過的にマッピングすることに注力している。このマッピングは理解しやすく、処理を追いやすい。しかし分散や拡張性といったものは二の次になってしまっている。
対照的に、RESTアーキテクチャスタイルの主要な目的は分散と拡張性である。RESTful HTTPインターフェースの設計は、言語へのバインディングという面ではなく、ネットワークの側面から行われる。RESTful HTTPはカプセル化を行おうとしない。それを行うとネットワークの遅延や、安定性、帯域などを隠蔽するのが難しくなる。
RESTful HTTPアプリケーションはHTTPプロトコルを直接利用し、抽象化レイヤを置かない。エラー項目や、セキュリティ・トークン項目といったREST特有のデータ項目は存在しない。RESTful HTTPアプリケーションはWEBの能力をただ利用するだけだ。RESTful HTTPインターフェースを設計するとなると、リモートインターフェスの設計者はHTTPを考慮しなければならない。しばしば、このことによって開発サイクルのなかで余分やステップが発生することとなる。
しかしRESTful HTTPを利用することで、拡張性が高く、堅牢なアプリケーションを実装できる。特にWebメールやソーシャル・ネットワーク・アプリケーションなどの非常に多くのユーザを対象としたWebアプリケーションを提供する会社は、RESTアーキテクチャ・スタイルから得られるものが大きい。この手のアプリケーションは、高い拡張性を短期間で実現することをもとめられることが多い。さらに、こういった会社では、広く利用されている標準的なコンポーネントとソフトウェアからなる低予算の基盤上でアプリケーションを運営しなければならないことが多い。
著者について
Gregor Roth氏はxLightweb HTTPライブラリの作者であり、GMXや1&1、Web.deなどを提供するヨーロッパを代表するインターネット・サービス・プロバイダであるUnited Internet groupにソフトウェア・アーキテクトとして勤務している。関心をもっている分野は、ソフトウェアおよびシステム・アーキテクチャ、エンタープライズ・アーキテクチャ管理、オブジェクト指向設計、分散コンピューティング、開発方法論など。
文献
Roy Fielding氏 - Architectural Styles and the Design of Network-based Software Architectures
Steve Vinoski氏 - REST Eye for the SOA Guy
Steve Vinosk氏 - Presentation: Steve Vinoski on REST, Reuse and Serendipity
Stefan Tilkov氏 -REST入門
Wikipedia氏 - Fallacies of Distributed Computing
Gregor Roth氏 - Server load balancing architectures
Gregor Roth氏 - Asynchronous HTTP and Comet architectures