SOAの実現: サービス設計の原則

柔軟なITのためのサービス設計

ビジネス敏捷性(agility)のビジョンを実現するための、SOA(Service-Oriented Architecture)設計原則を学びましょう。

David J.N. Artus (artusd@uk.ibm.com), Consulting IT Specialist, IBM

David ArtusDavid Artusは、イギリスのIBM Hursley LabにあるIBM Software Services for WebSphereチームのメンバーです。1999年にIBMに入社以来、WebSphereに関するコンサルタントおよび助言者として業務を行ってきました。IBMに入社する前には、投資銀行や旅行業界、ITコンサルティング業界など、様々な業界を経験してきています。関心を持っている分野は、分散システムの設計、オブジェクト技術、サービス指向アーキテクチャーなどです。



2006年 2月 17日

はじめに

SOAによってITの柔軟性を実現することができ、またビジネスの敏捷性(agility)も実現することができます。この記事ではITの柔軟性に関する2つの側面、つまり分離(decoupling)と、プロセス実装の容易性、という2つに焦点を当てます。個々のサービスをいかに規定し、いかに実現するかによって、こうした側面は大きな影響を受け、従ってビジネスの敏捷性も影響を受けます。この記事の目標は、SOAのビジョンを実現するために、サービスの規定方法や実現方法に関する指針を示すことにあります。

  • 最初に、サービスとサービス・オペレーションを規定し、実現するためのコンテキストを説明します。また、SOAインフラの責任と、サービスの責任についても考えます。
  • 次に、個々のサービス・オペレーションではなく、全体としてのサービス仕様に適用すべき設計原則について考えます。
  • 最後に、個々のサービス・オペレーションに適用すべき設計原則について説明します。

ここで概説する設計原則は、ITの柔軟性向上を、分離の促進とプロセス実装の容易性促進の両方によって実現することを目的としています。そこでまず、これらの概念を詳細に検討することにしましょう。

分離について

SOAの原則では、サービス・コンポーネントとサービス・プロバイダーとの分離を重視しており、そうした分離が実際に何を意味するのかに関して、非公式ながら有益な合意ができています。分離の背景となっている基礎概念の1つとして、サービス・プロバイダーに変更を加えても、必ずしも対応してサービス・コンシューマーを変更する必要はない、ということが挙げられます。例えば、ある特定なOS(operating system)で現在実行しているサービスを別のプラットフォームにデプロイし直す場合、サービス・コンシューマーに対しては何ら変更を要求しなくても当然である、という考え方です。コンシューマーとプロバイダーとの間の依存関係を減少させる、ということが、SOAの重要な原則なのです。

分離を技術的なレベルで考えると、コンシューマーが行う実装や可用性に関する選択を、サービス・プロバイダーとは独立に行える技術(Webサービスや非同期メッセージ送達など)が重視されます。またSOAインフラも、様々な方法で技術的な分離を実現できるように期待されています。下記はそうした方法の幾つかを示しています。

表1.分離技術
依存関係望ましい分離の姿分離手法
プラットフォームサービス用のハードウェア、OS、実装言語などの選択は、サービス・コンシューマー側での選択肢を制限しない。サービスは、サービス・コンシューマーのプラットフォームを制限しないような、標準的なサービス公開機構を使用する。一般的にはWebサービス技術を使用する。
ロケーションサービス実装ロケーション(位置)を変更してもクライアントに与える影響は最小限にとどまる。SOAインフラは、ランタイム・サービス・ロケーションとリクエスト・ルーティング機構を提供する。
可用性個々のサービスの可用性特性は、ビジネス・プロセス全体の可用性には影響しない。例えば、サービスとコンシューマーのメンテナンス・スケジュールを同期させる必要はない。SOAインフラは、適切な「保存・転送(store-and-forward)機構」を備えた信頼性の高い非同期呼び出し機構を提供する。
バージョンサービスに関して新しいバージョンを導入する場合、すべてのサービス・コンシューマーを同時にアップグレードする必要はない。SOAインフラは、メッセージ変換とメッセージ強化機能を提供する。サービス・コンシューマーは、(たとえサービス・プロバイダー・インターフェース自体が変更されても、インフラがコンシューマーとプロバイダーとの間を調停することによって)インフラが提供する一貫したインターフェースにアクセスできる。

サービスは、サービスのデプロイ対象となるSOAインフラに準拠して設計されている必要があり、特に、不必要な結合が回避されるように設計されている必要があります。悪い見本として、ステートフル・サービス・インターフェースはコンシューマーを特定なプロバイダー・インスタンスと関連付けるため、コンシューマーとプロバイダーとの結合を増加させる傾向があります。

分離の概念は、非技術的な、ビジネス・レベルにも当てはまります。サービス・コンシューマーは、サービス・プロバイダーが実装するビジネス・ロジックの詳細から、できるだけ遠く分離されている必要があります。この点でも、こうした分離を実現するために、サービス設計には十分注意する必要があります。下記の サービス設計の原則 では、粒度の細かな原始的方法の観点からではなく、意味を持つビジネス・オペレーションの観点からサービス・インターフェースを表現する利点について学びます。

プロセス実装について

SOAでは、個々のサービスをプログラム的に、あるいはBPEL(Business Process Execution Language)に基づくツールを使って振り付けることによって、ビジネス・プロセスを実装します。SOAのビジョンを実現するためには、プロセスを作成し、集成するためのタスク、つまりサービス・コレオグラフィーのタスクを単純化しなければなりません。

ビジネス・プロセス・コレオグラフィーの目標は、必要とするビジネス・ロジックを、正確に、そして効率的に実装することです。プロセス実装者が直面する問題には、次のようなものがあります。

  • 適切なサービス・オペレーションの選択
  • 適切なパラメーターを使ってのサービス呼び出し処理
  • (エラー・レスポンスを含めて)可能性のある様々なレスポンスの処理

サービス品質に関する設計の良否が、コレオグラフィーの使いやすさに大きな影響を与えうることが明確に分かると思います。つまりサービスの名前やサービスの数、オペレーションやパラメーター、ドキュメンテーションの質、サービス・オペレーション同士の間の相互依存関係などに関する判断は、どれも設計の問題として、コレオグラフィーに大きな影響を与えます。


SOA設計の原則

このセクションでは、SOAシステム全体に関する指針として、システムを構築する上での判断ポイントを説明します。皆さんは、サービスの設計者や実装者に対して、どんな処方や指針を与えるでしょうか。そのSOAインフラはどんな機能を提供しているのでしょうか。ここでは推奨される設計原則を幾つか挙げますが、エンジニアリング的に見ると、どの原則にもトレードオフがあります。企業によっては、ある特定な要求から、ここで挙げる一般的な推奨事項とは異なる選択が必要かも知れません。ここで設計原則を挙げる目的は、判断ポイントを示すことです。こうした判断に関する責任は、アーキテクトにあります。ただしここで挙げる設計原則のリストは、それほど長々としたものではありません。皆さんが実際に企業の中でSOAを実装する際には、さらに他の原則を追加することが多いはずです。私達は、そうした皆さんからの意見を聞きたいと思っています。

SOAは一貫性を持つべきである

サービスを作成し、公開し、発見し、呼び出すための技術の候補は数多くあります。SOAは、サービス・プロバイダーやコンシューマーが使用する特定な機構を規定するための、基準アーキテクチャーを提供する必要があります。つまりSOAに参加するものすべてに対して、一貫性を保つ必要があります。そうした一貫性を保つことによって、開発や統合、維持管理のための作業を減少させることができます。

基準アーキテクチャーに対して例外が必要な場合には、補完的な方法をとるべきです。例えば、サービス公開や発見用の機構としてUDDIが選択されたものの、ある特定な開発チームは、他のリポジトリー技術に基づく確立された開発プロセスを持っている、という状況を考えてみてください。そうした場合には、そのチームのサービスを、両方のリポジトリーに対して公開するような努力をすべきです。そうすれば、既存のコンシューマーは、(標準ではないにせよ)自分たちが慣れたリポジトリーを使える一方、共通のSOAインフラでオペレーションを行うコンシューマーは、全てのサービスに対して標準の(恐らくUDDI)リポジトリーを使うことができます。

SOAは開発を単純化すべきである

企業規模でのSOAインフラは、スケーラブルであり、かつ弾力的な(resilient)ことが期待されます。また、産業用の堅牢さを備えたESB(Enterprise Service Bus)とセキュリティー技術も含んでいる必要があります。別の言い方をすると、SOAインフラをターゲットとするサービスやプロセスの開発者は、

SOAインフラをターゲットとするサービスやプロセスの開発者は、高度なミドルウェアを利用し、SOAインフラに依存することによって、問題に対するソリューション(認証やメッセージ変換、高信頼性メッセージ送達など)を提供する必要があります。

こうしたミドルウェア機能実現の基礎に置くべき、重要な原則が1つあります。つまりサービスやプロセスの開発者は、ミドルウェア実装に関わる複雑さから分離されている必要がある、という原則です。理想的なゴールとしては、SOA環境で作業する開発者は、ビジネス・ドメインに関する知識と、基本的なプログラミング言語スキルさえ持っていればよいのです。

このゴールを実現するための方法には、下記のように幾つかのものがあります。

  • 宣言的方法: J2EEプログラミング・モデルは、この一例です。宣言的方法を使うことによって、アプリケーション・ロジックとミドルウェア・コンフィギュレーションとの間でコンサーンを分離することができます。例えば、アプリケーションを組み立てる人は、EJBメソッドのロール(role)に対するセキュリティー許可を、コードの中ではなく、デプロイメント記述子にエントリーを追加することによって適用します。そしてデプロイする人は、ユーザーやグループに対してロールをマップします。開発者は何ら許可コードを書く必要がないことに注意してください。
  • 抽象化: 場合によると、SOAインフラが、ある特定な目的用にAPIを提供している場合があります。例えばSOAインフラは、エラー・レポート機能や監査機能を持っているかも知れません。こうしたAPIは、使いやすさに注意しながら、十分に注意して設計する必要があります。また、こうしたAPIに対しては、プログラム的なコンフィギュレーションよりも宣言的方法を使う方が得策です。また標準API(例えばJavaロギングAPIなど)がある場合には、そうした標準APIを通してSOAインフラの持つ機能を公開すべきであり、独自のAPIを作ることは避けるべきです。
  • コード生成: コードが複雑になってしまうことが避けられない場合には、コード生成の方法を使うことができます。例えば、WSDL(Web Services Definition Language)を使うと、SOAPやHTTP、JMSなどにまつわる複雑さを開発者から見えなくすることができます。これを実現するためには、WSDLで表現された機械読み取り可能なインターフェース定義と、関係する呼び出しコードに関する言語特有の実装をWSDLから生成するツールを組み合わせるのです。
  • ツーリング(Tooling): SOAインフラの詳細が開発者のコードの中に入り込むことが避けられない場合、適切なツーリングによって開発環境を拡張すれば、開発者にとっての複雑さを減少させることができます。IBM Rational® Software Development Platform製品が提供するEclipseベースの環境は、カスタムのプラグインやコード断片、ユーザー・ガイドなどによって、容易に拡張できるようになっています。
  • モデル駆動開発: モデル駆動開発の方法は、上記の2つ(ツーリング機能とコード生成機能)を特に高度な形で組み合わせ、開発作業を簡単化するもの、と考えることができます。開発者はUML(Unified Modeling Language)モデルを作成し、このモデルを、SOAインフラを利用するために必要なコードを含むコードに変換することができます。

要約すると、SOAとそのインフラを定義する際には、開発者が必要とするものに充分な注意を払う必要がある、ということです。サービスの開発方法や使用法に関する指針を開発者に提供する場合には、開発者が進んで指針に従うように、何らかの機構を提供すべきです。一般的な原則としては、「簡単に行えるということは、正しく行えること」です。言い換えれば、「指針に従うとは、最も抵抗の少ない方法を選択することだ」と思わせるべきなのです。SOAが成功するためには、SOAにおけるガバナンスが決定的な要素となります。

これを開発者の視点から見ると、開発者はSOAのインフラと指針を理解する責任があり、また、そうした指針を避けず、指針に従って作業する責任がある、ということです。

サービスのインターフェースは、標準化され、正式に定義され、機械読み取り可能なものであるべきである

上記のツーリングとコード生成がSOA実装に重要な役割を持つ、ということが理解できたらば、今度は機械読み取り可能なインターフェースを使うことの重要性を強調する必要があります。よく定義された、機械読み取り可能な言語を使ってインターフェースを記述できれば、広範なツーリング機能が利用できるようになります。分離を推進すべきであることを踏まえ、専用フォーマットではなく、正式に定義されたオープン・スタンダード言語(WSDLなど)を強く推奨すべきなのです。

機械読み取り可能な方法という概念は、サービス・インターフェース(WSDLなど)の記述だけではなく、他のあらゆる形式による宣言的情報やメタデータにまで拡張することができます。この、宣言的方法と機械読み取り可能なメタデータの両方を強調することによって、複雑なものをビジネス・アプリケーション開発者から遠ざけ、標準ベースのミドルウェアの中に押し込むことができます。こうした方法を実現する上で、新興技術(WS-Policyなど)は重要な役割を担っています。

サービスは再利用を考慮して設計すべきである

サービス設計者は、自分たちが設計するサービスは、どれも再利用可能な資産になりうる、という可能性を念頭に置く必要があります。開発者は、当初のサービス・コンシューマーからの要求のみに集中するのではなく、より完全な要求がないかどうか判断するために、充分なビジネス分析を行うべきなのです。開発者はサービスに関して、次のような進化パターンを考慮すべきでしょう。

  • スループットの増加に対応できる設計でなければならない。あるサービスが成功すれば、それを利用するサービスの数も増大し、従って桁違いの使用増加が起きる可能性があります。
  • 利用サービスの数が増大すると、データ量や並行データ・アクセス・パターンも、当初とは大きく異なる可能性があります。
  • サービスに対する拡張要求が起きることを想定する必要があります。新しいコンシューマーは、機能の追加や、既存機能の変更を要求するかも知れません。

ここから先で解説する設計原則の多くは、サービスのスケーラビリティーと維持管理の容易性を確保する上で、特に重要です。ただしここで、一言警告しておくべきでしょう。つまり、再利用可能なサービスを設計しようとするあまり、「過度な」実装を行ってしまう可能性がある、という警告です。サービス・インターフェースの設計に関しては、スケーラビリティーを確保した上で当初の焦点を重視すべきであり、ここで挙げる設計原則も、そのために役立つはずです。そして、現在の、既知の要求を充分に満足するような戦術的インターフェース実装を作成するのです。インターフェースがうまく設計されていれば、何らかの必要が生じた場合にも、よりスケーラブルな実装で置き換えることができるはずです。


サービス設計の原則

「サービスは、ある合意されたフォーマットに従ってインターフェースが公開されたサービス・オペレーションを論理的にグループ分けしたもの」ということを念頭に置いて、サービス全体に適用可能な設計原則について解説しましょう。個々のオペレーションの設計に関しては、下記のサービス・オペレーション設計の原則のセクションで解説することにします。

サービスは、最も利用しやいように命名すべきである

サービスやオペレーション、データ型やパラメーターの名前を選ぶ上で、指針とすべき1つの原則があります。つまり、サービスが最も使いやすいように命名すべきだ、ということです。プロセス開発者がビジネス・プロセスを実装するために必要なサービスやオペレーションを、彼らに規定しやすくすべきなのです。そのため、サービス・コンシューマーの専門性から見て意味をなす名前、つまり技術的な概念ではなくビジネス概念を優先した名前を使うように、強く推奨します。

この推奨事項の定義を言い換えると、サービス名には名詞を使うべきであり、オペレーション名には動詞を使うべきなのです。

リスト1リスト2 に示す2つのサービス定義を比べてみてください。ここではプログラミング言語による「乱雑さ」を軽減するために、単純化した擬似コードを使っています。

リスト1.動詞句とIT構成体を使ったサービス定義
ManageCustomerData {

InsertCustomerRecord()

UpdateCustomerRecord()

// etc ...
}
リスト2.名詞句と動詞句、そしてビジネス概念を使ったサービス定義
CustomerService {

CreateNewCustomer()

ChangeCustomerAddress()

CorrectCustomerAddress()

EnableOverdraftFacilityForCustomer()

// etc ...

}

リスト1 では、IT概念を使って定義が表現されており、サービスとオペレーションの両方に対して動詞句が使われていることに注意してください。リスト2 では、サービスは名詞として表現されており、オペレーションは、明確なビジネス上の意味を持つ動詞を句として使って表現されています。この場合では、2番目の例の方が、より利用しやすいことが分かります。さらに2番目の例では、結果だけではなく、サービスの持つビジネス上の意図も明確になっています。つまり、任意の理由による任意の更新を含んでしまう「update customer record(顧客レコードを更新する)」ではなく、「enable overdraft facility(預金者側への当座貸し越し機能をイネーブルにする)」を規定しています。同様に、顧客が引っ越した場合には「change customer address(顧客住所を変更する)」メソッドがあり、また無効なデータを修正したい場合には、「correct customer address(顧客住所を集成する)」メソッドがあり、こうした2つのアクションが別々なサービス・ロジックを持っていることは明確に分かります。

ビジネス概念の観点からサービス名やオペレーション名を表現する場合には、こうした名前の決定方法に十分注意する必要があります。受け付け可能な用語に関する正式な用語集が強く求められますが、これはビジネスを分析することによって得られるものです。また、この用語集の正式な所有者も規定する必要があります。

サービスは適切に選択された粒度を持つべきである

SOAに関する解説の中で、『粒度(granularity)』という言葉は、幾つか別々な意味で使われます。サービス設計に関するこの記事では、サービスの粒度そのもの、つまり、ある1つのサービスが行いうるオペレーションの数について解説します。

サービス粒度を決定するために使用できるような単純な経験的方法はありません。代わりにここでは、サービスを設計する際に考慮すべき要因として、下記の2つの例を挙げます。

  • サービスは通常、テストとリリースの単位です。粒度が粗すぎると、多数のサービスを1つのグループにした場合、そのサービスに対するコンシューマーが増加しがちです。そのため、サービスの一部のみに修正を加えたい場合(例えばコンシューマー全体のうちの、一部のサブセットのみを修正する、など)、サービス全体を再リリースしなければならず、その結果すべてのコンシューマーに影響を与えてしまう可能性があります。
  • サービス・コンシューマーが直面する困難の1つは、正しいオペレーションを見つけることです。通常、コンシューマーは、サービスのリストをブラウズする必要があります。そして適切なサービスが特定できたら、今度はサービス・オペレーションのリストをブラウズする必要があります。極端なサービス粒度、つまり数多くのサービスがわずかなメソッドしか持たないサービス粒度や、あるいは、わずかなサービスが何十、何百ものオペレーションを持つようなサービス粒度は、使いやすさを制限しがちなことを助言として挙げておきます。

これはつまり、サービス粒度の選択に際して、維持管理の容易性やオペレーションの容易性、利用の容易性などといった要因をトレードオフしなければならないことを示しています。どんなSOAであっても、サービス設計者がそうしたトレードオフを考慮できるよう、指針を提供する必要があります。

サービスは結束性を持ち、かつ完全であるべきである

サービス粒度の決定に際して充分考慮すべきだということは分かりましたが、サービスを構成するオペレーションを決定する上で、何を考慮すべきなのでしょう。それを考慮するためには、結束性(cohesion)と完全性(completeness)という2つのオブジェクト設計概念を利用することです。これらの概念は、サービス・インターフェースに適用できるのです。

機能的に結束性を持ったインターフェースを作成するためには、同じような機能を持つ一連のオペレーションをグループ化します。結束性の程度を判断するためには、サービス・コンシューマーの観点からサービスを考えることが有効です。コンシューマーの観点から考えることによって、サービスの機能に注目するのです。この考え方を、下記のような判断方法と比べてみてください。

  • 機能の実装の結束性を元に判断してしまう。1つのグループとしてオペレーションをまとめる理由は、同じアルゴリズムを使っているからなのか、あるいは、同じホスト上でCICSトランザクションを利用して実装されているためなのでしょうか。こうした実装の詳細は、インターフェースの設計に影響を与えるべきではありません。
  • 一時的な結束性を元に判断してしまう。つまり短期間一緒に使用されるオペレーションを、1つのグループとして扱ってしまう。例えば銀行のビジネス・プロセスでは、RetrieveCustomerDetails(顧客詳細の検索)やCheckCreditRating(信用度チェック)、CreateLoanFacility(ローン機能作成)、TransferFunds(送金)などのオペレーションは連続して行われることが多いものです。しかし、一時的な結束性があるからといって、こうしたオペレーションを同じサービスとして提供すべきではありません。例えばCheckCreditRatingとTransferFundsには結束性はないのです。

サービスやオペレーションに対して名詞と動詞で名前を付けると、サービス・インターフェースの機能的結束性に注目できるものです。つまり、「名詞」が「動詞」を行う、というようになっているかを考えてみるのです。

オブジェクト設計概念の2番目は、完全性です。完全性は、既知のコンシューマーに対するサービスを作成する際に、特に重要です。この場合には、既知のコンシューマーからの要求に合致することのみに注目しがちです。しかしサービスは再利用可能であるべきなのであり、従って将来のコンシューマーからの潜在的要求についても考慮することが重要です。簡単な例として、CellPhone(携帯電話)というサービスが、CreateやUpdate、Query、Delete、Deactivateなどのオペレーションを提供している場合を考えてみてください。当然ながら、非アクティブにされた携帯電話は、再度アクティブにする必要があるはずです。それを考えた上で、Deactivateと対照的なActivateメソッドを提供すべきか否か判断すべきなのです。

完全性を考慮する場合には、適切な思慮分別が必要です。例えば、既知のコンシューマーからの要求をよく調べないと、正しい機能は提供できないものです。また、使われないオペレーションを作成するために開発やテストに労力を使ってしまう危険性もあります。

サービスは実装の詳細をカプセル化すべきである

もう1つのオブジェクト設計原則、つまりカプセル化は、サービス・インターフェースの設計にも当てはまります。サービス実装の詳細(つまり使用されるアルゴリズムやリソースなど)をカプセル化する目的は、サービス・コンシューマーとプロバイダーとの分離を促進し、将来の柔軟性向上を容易にすることです。

サービスは、複数の呼び出しパターンに対応すべきである

Webサービス技術(例えばWebSphere® が提供する技術など)によって、カプセル化のレベルをさらに高めることができます。サービス・コンシューマーは、異なる呼び出しパターンを使うWebサービスを、同じコーディング手法を使って呼び出すことができます。こうした呼び出しパターンには、次のようなものがあります。

  • HTTP経由のSOAPを使った、伝統的な同期呼び出し
  • JMS経由のSOAPを使った、非同期でメッセージ・ベースの呼び出し
  • Javaプロシージャー・コールを使ったローカル呼び出し

Webサービス・インフラによって呼び出しの詳細はカプセル化され、従ってコードは単純化されますが、サービスの設計は、呼び出しのスタイルによって影響を受けるはずです。例えば「ローカル呼び出し」と「リモート呼び出し」の比較を考えてみてください。リスト3 に示すようなサービス設計は、一見すると貴重なビジネス機能を提供していますが、多くのSOA環境でデプロイするには不適切です。

リスト3.「ビジーな」サービス・インターフェース
LibraryCatalogService { // search operations elided

String getBookTitle(String isbn)

String getBookAuthor(String isbn)

Date getBookPublicationDate(String isbn)

// further operations elided }

リスト3に示すサービス・インターフェースは、ローカルに呼び出された場合には、うまく動作するでしょう。しかし、このサービスがコンシューマーからリモートで提供された場合には、一般的な使用シナリオでのパフォーマンスは、ひどいものになるかも知れません。例えば、このサービスを使って、ある本のカタログ・エントリー表示画面に追加するためのデータ検索を行うことを考えてみてください。標題や著者、発刊日などを検索するためには、リモート・コールを別々に行う必要があります。こうした呼び出しを行うと、大きなパフォーマンス・コストが要求されます。リモート・サービスは、本に関する全情報を1つの呼び出しで検索できるような、粒度の粗いオペレーションを提供すべきなのです。

この設計原則、つまりリモート呼び出しサービスに関する設計の原則は、広く知られています。ここでも、カプセル化されたサービス呼び出しの詳細がサービス設計に重大な影響を持っていることを、改めて強調しておきます。またサービス・インターフェースの設計に際して、同期呼び出しと非同期呼び出しのどちらを選択するかということも、同じくらい大きな影響を持っていることも強調しておきます。

ここで重要な問題が浮上します。つまりサービスを設計する際に、使用する呼び出しスタイルをどうやって決めるのか、という問題です。サービス設計者は、ローカル呼び出しとリモート呼び出しを自由に選択でき、また同期呼び出しと非同期呼び出しを自由に選択できるべきなのでしょうか。この点に関しては、SOAで細かく規定することを推奨します。その理由には2つあります。第1に、一貫性を確保することによって使いやすさが向上するためです。プロセス・コレオグラフィーを行う際には、サービス特性が予測できた方が望ましいものです。第2に、コンシューマーとプロバイダーを分離することによって柔軟性が向上するためです。リモート呼び出しを推奨すれば、ロケーションやプラットフォーム、プログラミング言語などを分離することができます。また非同期呼び出しを推奨すれば、コンシューマーとプロバイダーの可用性特性を分離することができます。

SOAで細かく規定するのであれば、すべてのサービスはリモートで非同期の呼び出しを許すように設計すべきなのでしょうか。この規定に関しては、粒度の細かな手法を推奨します。可能性のあるサービス・タイプには、PlaceOrderのようにビジネス関連の重要なオペレーションを提供するものもあれば、CheckUserInRoleなどのように技術的なオペレーションもあり、非常に幅があります。このように異なるカテゴリーのサービスに対してSOAが別々の規定をすることは、まったく妥当なことです。ビジネス関連のオペレーションであれば非同期に呼び出される場合が多く、一方技術的なオペレーションはローカルで呼び出される場合が多いと言えるでしょう。

サービスはステートレス・インターフェースを持つべきである

「サービスは再利用を考慮して設計すべきである」の中で、サービスはスケーラブルに設計すべきこと、また高可用性インフラにデプロイ可能であるべきことを説明しました。この一般則から導かれる補則として、サービスをステートフルとすべきはない、ということが挙げられます。つまりサービスは、コンシューマーとプロバイダー間の長期間の持続関係に依存すべきではなく、またオペレーション呼び出しは、それまでの呼び出しに暗黙的に依存すべきではない、ということです。この点を、電話での会話の例を使って説明しましょう。

リスト4.ステートフルな会話
Q:What is Dave's account balance?

A: It's £320

Q:What's his credit limit?

A:It's £2,000

この例は、会話におけるステートフルな側面を2つ示しています。2番目の質問は、「his(彼の)」という言葉を使うことによって最初の質問を参照しています。これは、会話というコンテキストに依存するオペレーションの例です。今度は答えを考えてみてください。応答の言葉の中には、何らコンテキスト情報がないことに注意してください。この答えは、質問者が、先に行われた質問を知っているからこそ意味をなします。この例では、回答を解釈できるように、コンシューマーは会話状態を維持することが要求されています。このようにステートフルな関係(つまり連続呼び出しの間の関係や、リクエストとレスポンスとの間の関係)は、どちらもSOAのサービス設計に関係するものです。

まず、それまでのオペレーションによって確立されたコンテキストに依存するオペレーションの意味を考えてみましょう。この会話を、コール・センターとの会話だと考えてみてください。同じオペレーターとの間で会話が行われている場合には、この会話は無事に完了します。しかし次のように、会話が中断されたらどうなるでしょう。

リスト5.中断された、ステートフルな会話
Q:What is Dave's account balance?

A: It's £320

Q:What's his credit limit?

A:It's £2,000

中断が発生することによってコンテキストが失われ、2番目の質問が意味をなさなくなります。電話での会話であれば、「私はDaveの銀行口座の詳細を尋ねていたところです。彼のクレジット限度額を教えてくれませんか」のようにコンテキストを再確立することによって、中断を補うことができます。しかし、スケーラブルなサービス呼び出しにおけるステートフルな会話は、コンテキストの再確立が技術的に不可能であったり、過度なパフォーマンス・コストが要求されたりなど、問題が起きがちです。

一般的に言って、スケーラブルで信頼性の高いインフラを構築することとステートフルな対話動作を許すことは、相反するものです。技術的には、ステートフルなサービス呼び出しをサポートするSOAインフラを構成することは可能です。そのための方法としては、次のようなものが考えられます。

  • httpクッキーを使ってセッション・コンテキストを維持する
  • ステートフル・セッションEJBを使う。beanへのハンドルはSAOPヘッダーに渡される

しかし、その結果としてインフラのスケーラビリティーや信頼性がどのような影響を受けるか、十分に注意する必要があります。親和性(affinity)は要求されるのか。つまり、同じコンシューマーからの連続リクエストは、同じプロバイダー・インスタンスに送達すべきなのか。親和性要求があると、スケーラビリティーや信頼性と、ステートフル要求とが競合する1つの要因となります。もしインフラが、どれか1つのプロバイダー・インスタンスにリクエストを送達すればよいのであれば、ロード・バランシングは単純化され、個々のプロバイダー・インスタンスに対する信頼性要求を緩めることができます。

親和性要求が無い場合、もしインフラが、コンシューマーからの連続要求を別々なプロバイダー・インスタンスに送達してよいのであれば、すべてのセッション・ステートは、すべてのプロバイダー・インスタンスで利用可能でなければなりません。アプリケーション・サーバー・インフラは、セッション複製機構を提供します。この機構によってセッション・ステートが利用できるようになりますが、パフォーマンス・コストも増大します。さらに、私達がWeb開発で経験してきたことから考えると、強硬な指針を設けておかない限り、開発者はセッション・ステートを濫用しがちです(不必要に大きなHTTPセッションが低パフォーマンスの原因になるのは、よくあることです)。この点に関しては、JoinesとWillenborg、Hyghとによる「Performance Analysis for Java Web Sites」(Addison-Wesley刊、ISBN 0201844540)の59ページから60ページを見てください。

改めて強調しておきますが、サービスは、セッション・コンテキストを維持せずに済むように設計すべきなのです。

先ほどの会話の、もう1つのステートフル面、つまりリクエストとレスポンスとの関係について考えてみましょう。もしサービスを、この会話のようなスタイルで設計すると、つまり「What is Dave's credit limit?(Daveのクレジット限度額はいくらですか)・・・£320(320ポンドです)」というように会話コンテキストからレスポンスを解釈するように設計してしまうと、ここでもSOAインフラを制約することになります。

インフラは、一時的な障害が発生した場合に一部コンシューマーは会話状態を保持できない、という可能性に対応している必要があります。

会話状態が必要となる状況を回避するためには、レスポンスの中に適切な相関情報が含まれるようにサービスを設計することです。これを下記に示します。

リスト6.相関情報を含む会話
Q: What is Dave's credit limit?

A: Dave's credit limit is £2000

このレスポンスは、人を特定しているだけではなく、具体的なデータも提供しています。レガシー・システムをラップする場合、こうした相関情報を提供する責任は、アダプターが担う場合が多いものです。当然の予想として、既存の同期APIは相関データを提供しないかも知れません。レスポンスに相関情報を含めることは良い習慣ですが、それには幾つかの理由があります。まず、弾力性を持ったスケーラブル・ソリューションの構築が容易になるだけではなく、そうした情報は貴重な診断補助となります。またオリジナルのリクエスターにエラー・レスポンスを送達できない状況では、そうした情報は致命的重要性を持ちます。送達されなかったメッセージはエラー・キューに置かれるかも知れませんが、そうしたメッセージの解釈にはコンテキスト情報が必要です。

要約すると、サービス設計を注意深く行えばステートフルな会話が必要となる状況は回避でき、従ってスケーラブルで信頼性の高いSOAインフラの構築が容易になるのです。

サービスは、ステート・トランザクションを使ってモデル化すべきである

上記では一般的な助言として、会話状態に依存する状況を回避すべき事を述べました。これに加えて、コンピューター・システムが有用であるためには、(一般的にはビジネス・オブジェクトのライフサイクルを反映して)ステートフルであるべきなのです。

例えば、Webショッピングにおける、ある注文のライフサイクルを考えてみてください。まず、ある注文が作成されます。ユーザーの視点から見ると、空の買い物カゴが作成されます。次にユーザーが注文にアイテムを追加すると、アイテムは買い物カゴの中に置かれます。やがて注文はサブミットされ、注文処理部門に渡されます。図1 はこのライフサイクルを、単純化した状態遷移図で示しています。

図1.状態遷移としての注文のライフサイクル
図1.状態遷移としての注文のライフサイクル

このモデルを見ると、一部のステートフル振る舞いが明確になります。例えば、注文がOpen状態にあればアイテムを追加できますが、サブミットされた後には追加できません。

では、Orderというサービスを設計することを考えてみてください。これには リスト7 に示すようなものが考えられるでしょう。

リスト7.Orderというサービスの設計
OrderService {

void addLineItemToOrder(int orderId, int productId, int quantity)

void assignOrderToPacker(int packerId) 

int createOrder(int customerId) // returns id of new order

int packItemForOrder(int orderId, int quantity) // returns quantity left to ship

boolean shipOrder(int orderId) // returns whether all order is now shipped

void submitOrder(int orderId)

// ... query operations elided ...

このインターフェースの利用しやすさを考えてみましょう。(より現実的なものにするためには、アイテムの追加削除用など、もっと多くのメソッドを持った完全なインターフェースを考えるべきでしょう。)このように小さな例であっても、状態図を見ないと、メソッドが呼び出される順序を判断することは非常に困難です。ですからサービス設計者は、コンシューマーの作業を単純化するように作業すべきなのです。そのための手法として、幾つかを下記に挙げます。

最初に、オペレーションとパラメーターの名前を考えます。上記の例では、名前は注意深く選ばれており、ちょっと見れば、メソッドが使われそうな順序を推定することができます。リスト8リスト9 に示す例を見てください。両者は同じものですが、オペレーションとパラメーターの名前が違います。

リスト8.不適切に選ばれたオペレーション名とパラメーター名
ZettuylService {
int wibble(int wibId, int wobId, String which);
int wobble(int quibId);
boolean wrubble(int wibId);
void quibble(int widId)
void quash(int wibId)
Stuff[] getStuff(int wibId );
void  quite(int wibId);
Things[] getThings(int wibId);
void hinge(int wibId, intwobId);
int henge(int wibId , Stuff someStuff)
}
リスト9.適切に選ばれたオペレーション名とパラメーター名
ExpenseService {
int approveClaimItem(int claimId, int itemId, String comment);
int createClaim(String userId);
boolean auditClaim(int claimId);
void approveClaim(claimId)
void returnClaim(claimId)
ClaimItemDetails[] getClaimItems(int claimId );
void  payClaim(int claimId);
ClaimErrors[] validateClaim(int claimId);
void removeClaimItem(int claimId, int itemId);
int addClaimItem(int claimId, ClaimItemDetails details)
}

リスト8 の中の名前からは、何も分かりません。ところが リスト9 での名前では、サービスの目的が明確に分かり、オペレーションの呼び出し順序を大まかに推測することができます。例えばcreateClaim() はapproveClaim() よりも前に使われ、approveClaim() はpayClaim() よりも前に使われるでしょう。先の「サービスは、最も利用しやいように命名すべきである」で述べた通り、名前の選び方によって、利用しやすさが大きく影響を受けるのです。

次に、Orderの状態遷移図によって、注文のステートフル振る舞いが明確になったことを思い出してください。この遷移図は、注文の状態と、各状態における適切なオペレーションを示す、有効なドキュメンテーションとなっています。

使いやすさを向上するための手法の2番目は、ドキュメンテーションの価値をよく考えることです。すべてのドキュメンテーションをサービス・インターフェース定義の中に提供することが最善とは限りません。ドキュメンテーションが完備したWSDLファイルは貴重なものですが、それに付随する図や例も、同じくらい大きな価値を持っているのです。

使いやすさを向上するための他の手法として、ビジネス・オブジェクトのライフサイクル状態を反映したサービス・インターフェースを作成することが挙げられます。図1 に示した支払い請求の例では、各請求のライフサイクルには4つの状態があります。

図2. 支払いオブジェクトにおける4つの状態
図2. 支払いオブジェクトにおける4つの状態

状態が重要な意味を持つのは、2つの理由からです。まず、それぞれの状態は、別々のシステム・ユーザーに関係する場合が多いためです。例えば、支払い請求が構築中(building)状態であれば、主なシステム・ユーザーは請求の詳細を入力中の請求者であり、もし監査(auditing)状態であれば、その請求は請求を承認する権限を持った人によって検査を受けていることになります。

第2に、主な状態間での遷移は、別々のITシステム間でのデータの流れを反映している場合が多いためです。例えば、構築中状態では、ユーザーのワークステーション上にあるシック(thick)クライアント・アプリケーションが請求をキャプチャーしているかも知れません。その請求はサブミットされると、請求処理システムに渡されます。そして請求が承認されると、その請求はまた別のシステムである、支払いシステムに渡されます。この受け渡しに関して、もし実装が実際にシステムからシステムにデータを渡すのであれば、システム間(ここでの例ではsubmitClaim() と approveClaim() との間)での遷移に責任を持つオペレーションに対して、特に注意する必要があります。こうしたオペレーションでは、2つのシステムの更新が要求されるため、いずれかのシステムが利用できなくなると影響を受けます。こうした方法の実装では、非同期キュー機構を使った方が有利な場合が多いのです。

ビジネス・オブジェクトの状態はビジネス上の区分と技術上の区分の両方を反映している場合が多いものです。そのため、オリジナルのExpenseClaimServiceを、各状態に適用可能なサービスに分割することは極めて妥当なことです。そうすると、リスト10 に示すようなサービスができます。

リスト10.状態によってサービスを分割する
ClaimEntryService {  
createClaim(String userId);  
ClaimItemDetails[] getClaimItems(int );. 
ClaimErrors[] validateClaim(int claimId);
void removeClaimItem(int claimId, int itemId);
int addClaimItem(int claimId, ClaimItemDetails details)
int submitClaim(int claimId);
}

ClaimApprovalService {
int approveClaimItem(int claimId, int itemId, String comment);
void approveClaim(claimId)
void returnClaim(claimId)
ClaimItemDetails[] getClaimItems(int );. 
ClaimErrors[] validateClaim(int claimId);
}

ClaimPaymentService {
void  payClaim(int claimId);
}

こうすると、各サービスが一層理解しやすくなります。さらに、インターフェースをこのように分割することによって、サービス(あるいは「一連の」サービスかも知れません)の開発やデプロイ、維持管理、利用も楽になります。こうしたサービスは、別々なコンシューマーの関心対象となることができ、別々の開発チームによって開発することができ、別々にデプロイでき、従ってリリース・サイクルを分離することができます。別の言い方をすると、オブジェクト・ライフサイクルに注目することによって、適切な粒度を持つサービスを確立できるのです。


サービス・オペレーション設計の原則

サービスに関する全体的な設計原則は説明したので、今度は個々のサービス・オペレーションの設計に移ります。

オペレーションは、ビジネス・アクションを表現すべきである

先ほど、サービスやオペレーションの名前として、(オペレーション名に動詞を使って)ビジネス領域を示す名前を付けるべきであるという一般的な原則を述べました。オペレーションを考える際には、この原則を一歩進めます。つまりオペレーションを定義する際には、単なる汎用的な名前ではなく、具体的なビジネス上の意味を持つように定義するのです。例えば、汎用的なUpdateCustomerDetailsオペレーションよりも、ChangeCustomerAddressやRecordCustomerMarriage、AddAlternativeCustomerContactNumberといったオペレーションを作成した方が有利なのです。この方法には、次のような利点があります。

  • オペレーションを、具体的なビジネス・シナリオに対応させることができます。こうしたシナリオには、単にデータベース内のレコードを更新すること以上のことが含まれるかも知れません。例えば、住所の変更や婚姻の有無の変更などによって、正式なドキュメンテーションを作成する必要が出てくるかも知れません。そしてシステムは、そのドキュメントの詳細(あるいはスキャンしたコピー)を記録する必要があるかも知れません。そうしたビジネス・シナリオを、具体性に欠けるオペレーション(UpdateCustomerDetailsなど)を使って実装することは、ずっと困難なものです。
  • 個々のオペレーション・インターフェースが、単純に、理解しやすいものになるため、使いやすさが向上します。
  • 各オペレーションに対する更新単位が明確に定義できます(ここでの例では、住所や婚姻の有無、電話番号など)。並行性要求の高いシステムを実装する際には、オペレーションへのアクセス要求に基づいて、粒度の細かなロック戦略に改訂することができます。それによって、リソースに対する競合を減少させることができます。

オペレーションは、粒度の粗いパラメーターを持つべきである

オペレーション・パラメーターを考える場合にも、粒度の問題が出てきます。CreateNewCustomerというオペレーションに対する2つのインターフェース(リスト11リスト12)を比べてみてください。

リスト11.粒度の細かな複数パラメーターを持つCreateNewCustomerオペレーション・インターフェース
int CreateNewCustomer(String familyName, 
String givenName, 
String initials, 
int age
String address1
String address2
String postcode
// ... 
)
リスト12.粒度の粗い単一パラメーターを持つCreateNewCustomerオペレーション・インターフェース
int CreateNewCustomer( CustomerDetails newDetails)

リスト11 は、数多くの粒度の細かなパラメーターを持つオペレーションを示しています。リスト12 のオペレーションのパラメーターは、単一で粒度が粗く、構造化型です。私達は2つの理由から、粒度の粗いパラメーターを推奨します。第1の理由は、柔軟なオペレーションを作成でき、既存のコンシューマーを妨害することなく新しいバージョンのオペレーションを提供できるためです。第2の理由は、似たような型を持つ大量のパラメーターを持ったオペレーションは、3GLコードから呼び出された場合に転置(transposition)エラーを起こしやすいためです。これと対照的にデータが構造化型で置かれている場合には、明示的メソッド(etGivenName() やsetInitials() など)が使われ、この方法の方がエラーを起こしにくいのです。

オペレーションは、並行性を考慮して設計すべきである

Entity Beans(Entity Enterprise Java Beans)がサポートするような、伝統的でトランザクショナルなプログラミング・モデルを利用すると、データベースの更新を、リスト13 に示すようなスタイルのデータベース・ロックを使って行うことができます。

リスト13.トランザクショナル・プログラミング・モデル
Begin Transaction

Retrieve data from database - locking record

Modify values

Update database record with modified values

Commit Transaction - unlocking record

データベース・ロックが、ライン2のretrievalからライン5のcommitまで保持されていることに注意してください。こうすると、わずかに待ち時間を我慢するだけで、正しい並行振る舞いを保証することができます。データベース更新機能を提供するサービスを設計したい場合には、リスト8 のライン2とライン4の検索と書き込みオペレーションに対応するオペレーションを提供することができます。しかし、高度に分離された、場合によっては非同期のSOAインフラでの連続呼び出しの間では、データベース・ロックは保持しないように強く推奨します。それよりもむしろ、並行性に関する制御を適切なアプリケーション・ロジックに権限委譲することによって、楽観的ロック戦略を使うように推奨します。

楽観的ロック戦略での更新リクエストは「レコードXYZへの更新が、バージョンVとして用意されています。もし私が読み取った後に誰もレコードを修正していなかった場合に限り、変更を行ってください。」のように言うことができます。

リスト12に示す同じモデルに対して、データベース・トリガーとリビジョン・カウンターを使う楽観的ロック実装を表現してみましょう。この実装には、次のようなステップが必要です。

  • 楽観的ロックが必要なテーブルに対して整数カラムを追加します。このカラムはリビジョン・カウンターを持っています。
  • データベースにトリガーを追加します。これによって、テーブル中のレコードに対してどんな更新を行っても、リビジョン・カウンターが増加されます。
  • すべての検索オペレーションは、リビジョン・カウンターを含むデータ・アイテムを返します。
  • すべての更新オペレーションは、検索の結果得られるリビジョン・カウンターを含んでいる必要があります。
  • 更新オペレーション実装はデータベース・レコードに対して、「リビジョン・カウンターが等しい場所で、レコードを更新する」というように適切な更新を行う必要があります。もし、干渉するような何らかの修正がレコードに加えられると、この更新は失敗します(干渉更新が発生すると、更新トリガーが起動され、リビジョン・カウンターが変更されてしまうため)。
  • 他のコンシューマーによる干渉更新によって更新が失敗すると、そのコンシューマーには特別なエラーがレポートされます。

この実装では、リビジョン・カウンターが更新される場合、コンシューマーは正確なリビジョン・カウンターを提供することが要求されることに注意してください。この、正確さに関する責任は、データベースやプロバイダー、コンシューマー全体に当てはまります。また、この実装が真に楽観的であることにも注意してください。つまり競合の可能性が低い場合に、うまく動作するのです。更新の競合が起こりやすい場合には、リトライによるパフォーマンス・オーバーヘッドが非常に大きくなる可能性があります。楽観的ロック戦略は他にもあり、適切な並行性を実現するためには詳細な設計作業が必要です。

並行更新を管理することは比較的複雑なことから、これに関連した1つの推奨事項を挙げましょう。つまり、可能であればステートレス・セマンティックを使うことです。例えば、「残高をX増加する」という1つのオペレーションは、これと等価な「レコードを検索する」と「レコードに書き込む」という一対のオペレーション(コンシューマーが検索と書き込みの間に値を増加します)よりもずっと容易に、適切な並行性振る舞いを実装することができます。


まとめ

この記事の主な目的は、SOAのサービス設計がいかに重要かを強調することでした。ここで設計の原則として解説した項目のリストは、それほど大きなものではありません。各企業にとって適切な原則を注意深く選択するために、またサービスを構築する人達の指針としても、ここで挙げたようなSOA設計原則リストを使うことができるはずです。

参考文献

学ぶために

  • Olaf ZimmermanとPal Kroqdal、Clive Geeによる共著、「Elements of Service-Oriented Analysis and Design」(developerWorks, 2004年6月)は、この記事の基礎になっています。
  • Maarten Mullenderによる記事、Dealing with Concurrency: Designing Interaction Between Services and Their Agentsは、SOAでの並行性問題をさらに深く理解するために好適です。
  • Performance Analysis for Java Web Sites(JoinesとWillenborg、Hyghの共著、Addison Wesley刊、ISBN 0-201-84454-0)は、JavaベースのWebサイトで最高パフォーマンスを得るための指針を提供しています。Webサイトをシステムとして扱い、様々なコンポーネント(ネットワークやJava™ 仮想マシン、バックエンド・システム)がどのように関係するか、そしてそれらが全体的なパフォーマンスに影響しうることを解説しています。
  • developerWorksのSOA and web servicesゾーンでは、豊富な記事と、初心者から中級者、そして上級者まで幅広い方々に役立つ、Webサービス・アプリケーションの構築方法に関するチュートリアルを提供しています。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=SOA and web services
ArticleID=243776
ArticleTitle=SOAの実現: サービス設計の原則
publish-date=02172006