レベル: 中級 Dan Jemiolo (danjemiolo@us.ibm.com), Advisory Software Engineer, IBM
2007年 12月 18日 WS-* のユーザーと REST のユーザーとの間では、さまざまな領域の問題についてどちらの技術がより適しているかという議論が続いています。そのなかで WS-* のユーザーがよく主張することは、より複雑なエンタープライズ・レベルの問題は RESTful な手法では解決できないという意見です。そのような意見が本当かどうかを試すため、この記事では REST ユーザーの間ではあまり取り上げられない問題領域 (つまりシステム管理) のための RESTful なソリューションを作成してみます。以前、私は developerWorks のチュートリアルで HTTP サーバー製品を管理するための Web サービス・インターフェースの作成方法を紹介しました。そのチュートリアルのなかでは、WSDL の概念と WS-* 標準を使って Apache Muse および Apache Axis から管理インターフェースとソフトウェアを定義し、管理アプリケーションを作成しました。今回の記事ではこのアプリケーションのインターフェースと関数をProject Zero、そして REST の設計原則に基づいて作り直し、REST がこのエンタープライズ・プロジェクトに有効な選択肢であるかどうかを判断します。
始める前に
この記事では、読者が Project Zero をダウンロード済みであること、そして 初心者向けチュートリアルの手順を完了しているか、あるいは単純なアプリケーションを自分自身で作成した経験があることを前提とします。また、Apache Muse と WS-* 仕様を使用して HTTP サーバーの管理インターフェースを作成する方法を紹介している developerWorks のチュートリアル、「Apache Muse を使用して HTTP サーバー用の WSDM インターフェースを作成する」を十分に理解していることも必要です。今回の記事を読むのに Muse や WS-* 技術の専門的知識は必要ありません。上述のチュートリアルで解決しようとした問題、そしてコードが実現していた機能を理解していれば十分です。
はじめに
この記事は WS-* 技術と REST 指向の技術の長所と短所を列挙して比較するものでも、どちらが「勝者」かを決めるものでもありません。この記事が目的としているのは、REST と Web 2.0 開発の手法がシステム管理プロジェクトにとって生産的な代替手段となるのか否かを明らかにし、そして願わくば、開発者にさらに多くの選択肢を提供することです。WS-* のユーザーと REST のユーザーの間では、さまざまな領域の問題にどちらの技術がより適しているかという議論が続いていますが、WS-* のユーザーはよく、複雑なエンタープライズ・レベルの問題は RESTful な手法では解決できないと主張しているからです。
 |
Project Zero コミュニティー
Project Zero の Web サイトをざっと見て、Project Zero が最近の Web アプリケーションに強力な (しかも極めて単純な) 開発および実行プラットフォームをどのように提供するかを確かめてください。コミュニティーではプロジェクト開発についての議論、開発者の支援を活発に行っています。あなたの意見もぜひお聞かせください。 |
|
復習: 前回の HTTP サーバー向け WSDM インターフェース
HTTP サーバーを管理するために作成した当初の Web サービス・インターフェースでは、WSDL 1.1 文書を使って一連のプロパティーとオペレーションを公開しました。これらのプロパティーは、WS-RP (WS-ResourceProperties) 仕様が定義するオペレーションによってアクセス可能な名前と値のペアです。実際のプロパティーのなかには WS-* 仕様 (WSDM (WS-DistributedManagement) など) で定義されたものもあれば、使用する HTTP サーバー・リソースに合わせてカスタマイズしたものもあります。以下に、前回使用したプロパティーをすべてリストアップします。
-
ResourceId (WSDM)
-
ManageabilityCapability (WSDM)
-
Description (WSDM)
-
Caption (WSDM)
-
Version (WSDM)
-
OperationalStatus (WSDM)
-
Name (カスタム)
-
Port (カスタム)
-
ThreadsPerChild (カスタム)
インターフェースに含まれるオペレーションは WS-RP からのもので、1 つまたは複数のプロパティー値の読み取り/書き込みオペレーションが中心となっています。さらに、HTTP サーバーを起動、停止するための Start と Stop という 2 つのカスタム・オペレーションもあります。WS-RP オペレーションはすべて Apache Muse フレームワークで提供する一方、カスタム・オペレーションは Java™ コードで実装し、Apache Muse 拡張ポイントを使用して組み込んでいます。以下は、使用したオペレーションすべてのリストです。
-
GetResourceProperty (WS-RP)
-
GetResourcePropertyDocument (WS-RP)
-
GetMultipleResourceProperties (WS-RP)
-
Start (カスタム)
-
Stop (カスタム)
覚えているかもしれませんが、WS-RP に基づく「書き込み」オペレーションは一切組み込んでいません。これは、直接変更する必要があるプロパティーは 1 つもなかったためです。一部には可変のプロパティーもありますが、副次作用として変更されるに過ぎません (例えば、OperationalStatus プロパティーはサーバーが起動または停止すると変更されることになります)。
REST への WS-ResourceProperties のマッピング
RESTful なプログラミングが依存するのは、ネットワーク・トランスポート・レベル (通常は HTTP) のルールと規約、そしてよく知られたいくつかのデータ・フォーマット (通常は XML または JSON) です。リソース・プロパティー文書に含まれる情報を RESTful な方法で公開するために、このプロジェクトでは HTTP GET メソッドを使用してリソース・プロパティーと同様のフィールド (名前と値のペア) を持つ JSON オブジェクトを取得することにします。つまり、リソース・プロパティー文書を JSON オブジェクトに置き換え、WS-RP メソッドの代わりに HTTP メソッドを使用するということです。HTTP サーバー・リソースを JSON で表現すると、例えばリスト 1 のようになります。
リスト 1. HTTP サーバーの JSON 表現
{
"name": "server1.ibm.com",
"port": 8080,
"threads": 250,
"admin": "admin@us.ibm.com"
}
|
お気付きかもしれませんが、この JSON 表現には WSDM プロパティーがありません。WS-* の概念のなかには RESTful な設計では対応するものがないものや、アプリケーションの他の部分に存在するものがあります。例えば WSDM ResourceId プロパティーや暗黙的なリソース・パターンは必要とされません。そこで、エンドポイント参照と ResourceId の値を使ってリソースを定義する代わりに、URI パターンを使用してすべてのリソースに固有の URI を持たせることにします。HTTP をリソース・インターフェースの一部として再利用することにより、いくつかのプロパティーとオペレーションを不要にするわけです (WS-* のユーザーは、この方法では HTTP に縛られてしまうことになると指摘するかもしれませんが、その良し悪しについての論議はこの記事の目的ではありません)。さらに記述的な URI を使用して、前回 WSDL 文書に記述的なプロパティーの一部を組み込んだのとは対照的に、サービスを人間が読めるように文書化します。要するに、HTTP 指向のソリューションを前提として、JSON 表現を必要な限り完全にするということです。
HTTP サーバー・リソースを簡単に見つけて操作できるように、/httpd/{serverName} という URI パターンを使用してリソースを区別します。/httpd URI がアプリケーションで管理されるすべての HTTP サーバーの集合を指すようにし、/httpd/{serverName} URI が個別の HTTP サーバーを指すようにします。集合の URI に送信されたリクエストは (可能であれば) 集合全体と照合して評価される一方、個々のサーバー URI に送信されたリクエストはその URI が表すサーバーに限定されます。表 1 に、この HTTP サーバー・リソースを記述する REST API を示します。
表 1. HTTP サーバー・リソースの REST API
| メソッド | URI | 形式 | 結果 |
|---|
| GET | /httpd | text/json | このアプリケーションが管理するサーバーを表す名前のリスト | | GET | /httpd/{serverName} | text/json | 特定サーバーの構成データが含まれる JSON オブジェクト |
インターフェースを拡張してユーザーがこれらのプロパティーを直接変更できるようにするとしたら、WS-RP の SetResourceProperties ではなく HTTP の PUT を使用することになります。PUT リクエストの URI パターンは GET の場合と同じですが、この 2 つの大きな違いは、PUT リクエストは JSON データを送信し、GET リクエストは JSON データを受信するという点です。
REST へのカスタム・オペレーションのマッピング
カスタム・オペレーションである Start と Stop は、設計部門でより大きな課題となります。私たちが扱う HTTP メソッドは 4 つ (GET、PUT、POST、DELETE) しかありません。また、オペレーション名を PUT または POST リクエストに組み込むのは、REST の世界では好ましくない形だと考えられているためです。この問題に対処するには、サーバーを起動、停止するという行為についての考え方を変え、サーバーの起動と停止は HTTP サーバー・システムに対するアクションではなく、HTTPサーバー・プロセスの作成または破棄として捉える必要があります。これは、それほどの拡大解釈でもありません。オペレーティング・システムはまさに、監視可能かつ (最終的には) 終了可能なプロセスを作成することによって要求の開始、停止を処理するからです。このプロジェクトでは、HTTP サーバー・プロセス・リソースをその独自の URI セットと HTTP 体系を使って作成します。この新しいリソース・タイプのための REST API を表 2 に示します。
表 2. HTTP プロセス・リソースの REST API
| メソッド | URI | 形式 | 結果 |
|---|
| GET | /httpd/{serverName}/process | なし | サーバーが実行中の場合は 200 OK、そうでない場合は 404 Not Found | | POST | /httpd/{serverName}/process | なし | サーバーを起動して 201 Created を返します。 | | DELETE | /httpd/{serverName}/process | なし | サーバーを停止して 204 No Content を返します。 |
ご覧のように、HTTP プロセス・リソースには JSON 表現がありません。つまり、これは単なる URI だということです。この URI に HTTP POST を送信するとプロセスが作成されます (「サーバーの起動」)。一方、HTTP DELETE はプロセスを破棄し (「サーバーの停止」)、HTTP GET はサーバーのステータスを提供します (これと同様の WSDM の OperationalStatus プロパティーはもう必要ありません)。このようにインターフェースを 2 つのリソース・タイプに分割することによって、RPC や WS-* スタイルの手法を用いることなく、同じ機能を実現できました。次のセクションでは、この設計決定を実際の実動コードに変換する方法について説明します。
Zero による HTTP サーバー管理の実装
以前のチュートリアルで説明した Muse ベースの実装を理解していればなおさらのこと、上記のセクションで作成した RESTful なインターフェースを実装するためのコードは極めて簡単に作成できます。このプロジェクトでは、Java クラスの代わりに Groovy スクリプト、WSDL ファイルの代わりに RESTdoc コメント、そして XML の代わりに JSON を使用します。
Groovy による Apache HTTP サーバーへのアクセス
コードの作成に取り掛かる前に、Apache HTTP サーバー (httpd) がインストール済みで、正常に動作していることを確認してください。以前のチュートリアルを最後まで終えていないために httpd がまだインストールされていない場合は、「参考文献」セクションに Apache Web サイトへのリンクを記載しているので、このサイトからサーバーとそのインストール手順をダウンロードしてください。サーバーは必ず、サービス (またはデーモン) としてインストールされている必要があります。サービスの有無がわからない場合は、コマンドラインに httpd -k install と入力すると、既存のサービスがなければ新規に作成されます。httpd がサービスとしてインストールされていれば、コードから非同期にサーバーを起動、停止することができます。
httpd がインストールされて準備が整ったら、次に必要な作業はコードを保持するための Zero プロジェクトを作成することです。リスト 2 のコマンドラインを使用して zero.httpd という名前のプロジェクトを作成してください。このプロジェクトにはデフォルトで、Zero Core の依存関係と RESTdoc ツールが含まれているようにします。これ以外に、この記事で作成するコードに必要なものはありません。完成済みのプロジェクトを先に確認したいという場合は、「ダウンロード」セクションからダウンロードすることができます。
リスト 2. サンプル・プロジェクトの作成
コードを作成する前に、まだやらなければならないことがあります。コードが httpd システムを検出する方法を決めることです。コードがシステムを検出できなければ、システムのファイルを読み取り、それに対してコマンドを実行することはできません。以前のチュートリアルでは muse.xml ファイルを使用して、httpd システム・パスが含まれる初期化パラメーターを作成しました。こうすることによって、Java ソース・ファイルにシステム・データをハードコーディングすることなく、httpd ファイルへのパスを作成することができます。Zero でも同じような手法を取りますが、今回は zero.config ファイルを使ってサーバー名とシステム・ディレクトリーとのマップを作成します。サーバー名は任意の英数字からなる ID にすることができるので、HTTP サーバー・リソースの具体的な URI にこのサーバー名が使われます。リスト 3 にサンプルの zero.config ファイルを示します。このファイルを元に作業を始めても構いません。
リスト 3. HTTP サーバー・システムの構成
[/config/httpd/servers[]]
server1=/httpd/server1
buildServer=/dev/builds/httpd
intranetSite=/public/prod/apache
[/config/http]
port=9080
|
リスト 3 では、サーバー名を太字で強調してあります。Groovy コードに含まれるこの名前と値のペアには、Zero のグローバル・コンテキストを使用してアクセスできるようになります。わかりやすいようにテストでは一貫してこの server1 を使用しますが、server1 のシステム・パスの値は皆さんのローカルの httpd システムで使用される値に変更してください。他の 2 つのサーバー・エントリーは必要ありません。これらのエントリーはサンプル・プロジェクトの構成フォーマットを示すために記載しているだけです。
zero.config ファイルを保存して閉じる前に、リスト 3 に示されている /config/http の port の指定と同じエントリーを追加してください。このエントリーは Zero のデフォルト・ポート 8080 を値 9080で上書きします。httpd のデフォルト・ポートも同じく 8080 なので、このエントリーを追加して Zero アプリケーションと httpd プロセスが競合しないようにする必要があります。
次に、httpd.groovy、process.groovy、process.bnd という 3 つのファイルを作成し、プロジェクトの /app/resources ディレクトリーに追加します。最初に挙げた 2 つのファイルは、前のセクションで説明した 2 つの RESTful なリソースを表す Groovy スクリプトです。3 つ目のファイルは、リソースが許可する RESTful な URI パターンを構成するために使用されます。
まずは一番簡単なタスク、process.bnd ファイルを作成するところから始めましょう。このファイルに追加しなければならないのは、リスト 4 に示す 1 行だけです。この行は Zero ランタイムに対して、先ほど REST API の表に記載した /httpd/{id}/process URI パターンを許可するように指示します。この行がない場合、Zero の /httpd/{id} と /process/{id} へのリクエストは許可されますが、/httpd/{id}/process へのリクエストは決して許可されません。
リスト 4. BND ファイルの作成
このプロジェクトの httpd システムを表す httpd.groovy ファイルには 2 つのメソッド、onList() と onRetrieve() が必要です。前者はこの Zero アプリケーションが管理する httpd システムのリストを返し、後者は個別の httpd システムの JSON 表現を返します。現在、このプロジェクトのインターフェースではリモートからの httpd システムへの変更をサポートしていないため、この 2 つ以外に必要となる RESTful なメソッド (onCreate() など) はありません。
リスト 5 は、onList() および onRetrieve() のコードです。onRetrieve() の実装では readConfig() というメソッドを使用して httpd.conf ファイルの解析を行います。このメソッドによる解析コードは以前のチュートリアルからコピーしたもので、より Groovy に似た構文を使うように多少変更してあります。readConfig() の実装については、完成済みのサンプル・プロジェクトをダウンロードして、そのなかに含まれる /app/resources/httpd.groovy を参照してください。
リスト 5. httpd.groovy の作成
def onList()
{
request.view = "JSON";
request.json.output = config.httpd.servers[0].keySet();
render();
}
def onRetrieve()
{
def serverConfig = readConfig();
if (serverConfig.isEmpty())
{
request.status = 404;
return;
}
request.view = "JSON";
request.json.output = [
name: serverConfig.ServerName,
port: serverConfig.Listen,
threads: serverConfig.ThreadsPerChild,
admin: serverConfig.ServerAdmin
];
render();
}
|
リスト 5 では、JSON レスポンス・データの作成時に公開する httpd 構成プロパティーを強調表示してあります。WS-* バージョンの場合と同じく、プロパティーにはより一般的な名前を付け (インターフェースが Apache 固有にならないようにするため)、セキュリティーの観点から慎重を期する事項は、ここでは無視していることに注意してください。もう 1 つの注意点として、サーバーが存在しない場合には単純な HTTP ステータス・コード (404) のみでそのことを示しています。つまり、障害タイプや追加のメッセージは送信されません。onRetrieve() メソッドは概念の点では WS-RP の GetResourcePropertyDocument と同様ですが、結果を照合するためのスキーマはなく、「障害」はアプリケーション層ではなくトランスポート層 (HTTP) で定義されます。
httpd システム・リソースの定義が済んだので、今後は httpd プロセス・リソースに手をつけます。リスト 6 に process.groovy の実装を示します。このリソース・タイプはサーバーの起動と停止、そして httpd プロセスの有無の確認に使用されるだけなので、実装は極めて単純です。一般的な REST 設計原則に多少手を加え、ここでは HTTP POST で httpd プロセスを作成し (サーバーの起動)、HTTP DELETE で httpd プロセスを破棄し (サーバーの停止)、HTTP GET でサーバーが実行中かどうかを判断しています。この 3 つのオペレーションはそれぞれ onCreate()、onDeleteCollection()、onList() メソッドに対応します。
リスト 6. process.groovy の作成
def onCreate()
{
Runtime.getRuntime().exec("${getServerHome()}/bin/httpd -k start");
request.status = 201;
}
def onDeleteCollection()
{
Runtime.getRuntime().exec("${getServerHome()}/bin/httpd -k stop");
request.status = 204;
}
def onList()
{
def pidFile = new File(getServerHome(), "logs/httpd.pid");
if (!pidFile.exists())
request.status = 404;
}
|
onCreate() と onDeleteCollection() の両メソッドは、以前のチュートリアルでサーバーの起動と停止に使ったコードと同じものを使用します。具体的には、JDK の Runtime.exec() API を使用して httpd 実行ファイルをロードし、適切なコマンドを実行するということです。これは、このプロジェクトの WS-* インターフェースに含まれるカスタム・オペレーション、Start および Stop と同様です。onList() メソッドは、httpd が起動時には毎回プロセス ID (PID) ファイルを作成するという事実を利用し、そのファイルの有無をテストすることによってサーバーが実行中かどうかを判断します。PID ファイルが存在しない場合は、通常の 200 OK の代わりに 404 Not Found を返します。この process.groovy ファイルについても、サンプル・プロジェクトをダウンロードすると完成版を入手できます。
これで、httpd のための RESTful な API の実装は完成しましたが、これを簡単にテストする方法がまだありません。そこで、次のセクションでは Groovy メソッドの RESTful な文書を作成し、テスト用のコードを作成せずに RESTdoc ツールを使って API をテストすることにします。実際、そのままブラウザー上で上記コードの httpd に対するオペレーションを検証することができます。
RESTdoc による REST API の公開
前のセクションではコードをずいぶん作成しましたが、文書化はしていません。Zero の RESTful なリソース・スクリプトに関して言えば、適切に文書化しておくと、コードの動作を覚えておく上で役立つだけでなく、クライアント・サイドのプログラマーがクライアント・コードを作成してテストする上でも役に立ちます。Groovy および PHP のメソッドには JavaDoc のようなコメントを作成し、Zero の RESTdoc ツールに作成させた REST 表に対してページをテストすることが可能です (RESTdoc に関する詳細は「参考文献」を参照)。そこで、これから RESTdoc コメントを Groovy スクリプトに追加して httpd インターフェースをテストしてみます。
リスト 7 に、httpd.groovy ファイルの場合の RESTdoc コメントを示します (簡潔にするため、メソッド実装は省いてあります)。サンプル・プロジェクトをダウンロードすると、この httpd.groovy の 2 つのスクリプトに関する RESTdoc コメントを確認することができます。httpd.groovy のコメントは RESTdoc コメントで使われる可能性のあるすべてのタグを示しているため、非常にわかりやすいものとなっています。
リスト 7. httpd.groovy の文書化
/**
*
* @success 200 Returns a list of server names that can be used to build server URIs.
* @format text/json
* @example
* [
* "server1",
* "server2",
* ...
* ]
*
*/
def onList()
{
...
}
/**
*
* @success 200 Returns the configuration data for the given server name.
* @error 404 If there is no server associated with the given name.
* @format text/json
* @example
* {
* "name": "server1.ibm.com",
* "port": 8080,
* "threads": 250,
* "admin": "admin@us.ibm.com"
* }
*
*/
def onRetrieve()
{
...
}
|
RESTdoc コメントを配置したら、Zero アプリケーションを起動して (zero run と入力)、ブラウザーで http://localhost:9080/resources/docs までナビゲートしてください。索引ページが表示され、httpd および process を含めたアプリケーションの RESTful なリソース・タイプがすべてリストアップされるはずです。httpd リンクをクリックすると、図 1 のようなページが表示されます。このページに表示されるのはリソース・タイプを操作するために使用可能なすべての URI パターン、そしてリクエストとレスポンスを処理するコードに必要な情報です。
図 1. RESTdoc ページのスクリーンショット
RESTdoc による REST API のテスト
RESTful な API をテストするには、これらの API を記述するページを使用します。httpd の RESTdoc ページで、一番上に表示されている URI パターン、/httpd をクリックしてください。すると、図 2 に示すダイアログが表示されます。この URI パターンには変数がないので、Send ボタンをクリックするだけでサーバー名のリストが含まれるレスポンスが表示されます。これらのサーバー名は、zero.config ファイルに記述したものです。図 3 に、このレスポンスを示します。
図 2. RESTdoc リクエスト・ページのスクリーンショット
図 3. RESTdoc レスポンス・ページのスクリーンショット
次に、上から 2 番目に表示されている URI パターン、/httpd/{httpdId} をクリックし、httpdId 変数の値として server1 と入力します。このツールでは値を入力して URI パターンを完成させるまでは Send ボタンをクリックできないことに注意してください。Send をクリックすると、httpd システムの JSON 表現が含まれるレスポンスが表示されます。
RESTdoc インターフェースでの最後のテストは、以前のチュートリアルからコピーした「起動」と「停止」機能を対象としたものです。このプロジェクトの Zero ベースのインターフェースでサーバーを起動または停止するには、/httpd/{httpdId}/process に HTTP POST、あるいは HTTP DELETE を送信する必要があります。起動と停止の機能をテストする方法は以下のとおりです。
- RESTdoc の索引から
process リソースを選択します。
- HTTP POST URI パターンをクリックし、
httpdId 変数の値として server1 と入力します。その上で、Send をクリックします。
- 別のブラウザー・ウィンドウ (またはタブ) で http://localhost:8080 にアクセスします。デフォルトの httpd ウェルカム・メッセージが表示されたら、テストは成功です。
-
RESTdoc ページで HTTP DELETE URI パターンをクリックします。
httpdId 変数の値として server1 と入力し、Send をクリックします。
- http://localhost:8080 ページを更新します。サーバーは停止しているので、接続タイムアウトになるはずです。
- RESTdoc ページで HTTP GET URI パターンをクリックします。
httpdId 変数の値として server1 と入力し、Send をクリックします。
- サーバーが停止し、その成果物 (PID ファイル) が削除されたことを確認できるステータス・コード、404 が表示されるはずです。
このセクションでは、コードや文書を追加で作成することなく、RESTful な API をテストする方法を説明しました。RESTdoc インターフェースにはテストを自動化する手段は用意されていませんが、アプリケーション開発の初期段階や、サービスについて学びたいと思っているクライアント・プログラマーにとっては理想的なインターフェースです。
まとめ
この記事で Zero をベースに作成した httpd の RESTful なインターフェースは、Apache Muse ベースの WS-* バージョンと同じく機能的に完成されたものとなりました。Groovy スクリプトと RESTdoc コメントの組み合わせは、Java クラスと WSDL を組み合わせた場合と同じ機能、振る舞いを実現し、HTTP が単独で処理するには「複雑過ぎる」と考えられているタスクを REST が処理できることを実証しています。REST ソリューションと WS-* ソリューションにはそれぞれに長所と短所があり、どちらが適しているかはプロジェクトによって異なるでしょう。この記事では結局のところ、どちらが優れているかを指摘するのではなく、両方とも有効な選択肢であることを説明するにとどめました。
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| Sample application | wa-pz-httprest.zip | 8KB | HTTP |
|---|
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | Dan Jemiolo は、ノースカロライナ州リサーチ・トライアングル・パークにある IBM Autonomic Computing チームの Advisory Software Engineer です。彼は Apache Muse 2.0 の設計および開発を指揮し、現在も引き続きこのプロジェクトに取り組んでいます。また、Dan は WS-RF TC に WS-ResourceMetadata 仕様のエディターとして参加しており、Web サービス標準の採用を促進する IBM の戦略に携わっています。彼はわずか 2 年前に、Rensselaer Polytechnic Institute で Computer Science を専攻し理学修士の学位を取得した後、IBM に入社しました。 |
記事の評価
|