Node.js と MongoDB を使用してモバイル・アプリを開発する: 第 2 回 ヒントと秘訣

代替スタックを使用する際の落とし穴を回避する

Systems of Engagement (人と関わりあうシステム) を開発するために、Java テクノロジーではなく Node.js (サーバー・サイドの JavaScript) を使用した場合の実装に関する詳細を学んでください。この記事では、Node.js と MongoDB を使用して RESTful なバックエンド・アプリケーションを開発した IBM Extreme Blue チームが、チームの思考プロセスと推奨事項を紹介します。チームの目標と課題についての説明は、この連載の第 1 回を読んでください。

Zach Cross, IBM Extreme Blue intern, IBM

Author photo - Zach CrossZach Cross は、ノースカロライナ大学 (UNC) チャペルヒル校でコンピューター・サイエンスの学士号を取得しました。現在は、UNC 大学院でコンピューター・サイエンスの学位を目指しているところです。彼の専門分野はソフトウェア・エンジニアリングおよびシステムです。特に、ネットワーク・プロトコルを得意としていて、分散システムと GPGPU プログラミングにも強い関心があります。余暇は、自転車、読書、料理を楽しんでいます。2013年 8月に IBM Extreme Blue インターンシップを終えました。



Aga Pochec, IBM Extreme Blue intern, IBM

Author photo - Aga PochecAga Pochec は、ワルシャワ経済大学で国際ビジネスの修士号を、そして高名なヨーロッパの CEMS (Community of European Management Schools and International Companies) プログラムで修士号を取得しました。現在は、ボストンのハーバード・ビジネス・スクールで経営管理学の修士課程を修了しつつあり、IBM で Extreme Blue インターンとして働いています。彼女はヨーロッパおよびアジア太平洋で 5 年間のビジネス・インテリジェンスおよびコンサルティングの経験を持つプロジェクト・マネージャーであり、マーケティング、ストラテジー、ビジネス開発を専門としています。旅行をこよなく愛し、時間があるときには新しい文化と料理を探索しています。2013年 8月に IBM Extreme Blue インターンシップを終えました。



Daniel Santiago, IBM Extreme Blue intern, IBM

Author photo - Daniel SantiagoDaniel Santiago は、現在プエルトリコ大学でコンピューター・エンジニアリングを学んでいる学生です。プログラミングに情熱を傾ける彼は、モバイル、バックエンド、Web 開発に関心を持っています。また、Node.js や MongoDB などの新しい技術にも好んで取り組んでいます。余暇は、料理、ランニング、そしてゲーム用コンピューターのアセンブルを楽しんでいます。2013年 8月に IBM Extreme Blue インターンシップを終えました。



Divit Singh, IBM Extreme Blue intern, IBM

Author photo - Divit SinghDivit Singh は、現在コンピューター・サイエンスの修士号を目指しているバージニア工科大学の大学院生です。PhoneGap および Sencha Touch でクロスプラットフォーム・アプリケーションの作成に取り組んでいる彼は、モバイル技術を専門としています。アプリケーションの開発を楽しんでいて、開発したアプリケーションを Google Play Store と GitHub で公開しています。また、Node.js などの新しい Web 技術にも好んで取り組んでいます。趣味は、スポーツ、音楽、テレビ・ゲームです。2013年 8月に IBM Extreme Blue インターンシップを終えました。



2013年 9月 19日

第 1 回で説明したように、ノースカロライナ州、リサーチ・トライアングル・パーク (RTP) にある IBM ラボの Extreme Blue チームは、IBM Passes のバックエンド全体を Node.js で開発するという課題に取り組みました。その結果、チームは同じ機能を提供する Java によるソリューションに比べ、開発に要する時間を 40% 短縮することに成功しました。また、パフォーマンス・テストによって、Node.js バックエンドはスケーラビリティーの点でもハードウェアの使用効率の点でも Java より優れていることが実証されました。

IBM Extreme Blue チームについて

Extreme Blue プログラムとは、ソフトウェア開発と MBA の学位を目指す有能な学生を対象とした IBM のインターンシップ・プログラムです。詳細については、ibm.com/extremeblue にアクセスしてください。

この記事では、私たちのチームが Node.js と MongoDB を使用して RESTful なバックエンド・アプリケーションを開発する過程で学んだ教訓を紹介します。上位レベルの設計および実装に関していくつかの決定をする際の思考プロセスに重点を置いて、以下のトピックを取り上げます。

  • NPM (Node Packaged Modules) の活用
  • 非同期プログラミングの概要
  • 大規模な Express.js アプリケーションのルーティング構成の構造化
  • アプリケーション・レベルのロギング
  • データの永続化およびモデル化に取り組む方法
  • Node.js ではサポートされていない暗号化機能をはじめとする Passbook 固有の問題の解決

さらに、Node.js と MongoDB での開発に関心がある方の学習プロセスを早めることを願って、以下の細かい「秘訣」も取り上げます。

  • 動的に型付けされる言語では、オブジェクトやそのプロパティーについて憶測を立てないこと
  • 暗黙的な制御フローを避けること
  • 依存関係の振る舞いを理解すること
Node.js、JavaScript、JSON、および RESTful な Web サービスの基本概念を理解している開発者の方であれば、この記事を最大限に活用できるはずです。

 

NPM

NPM (Node Packaged Modules) は、Node.js 用のパッケージ・マネージャーです。NPM を使用すると、(サード・パーティー製ライブラリーのインストールを含む) 依存関係の管理を簡単かつ迅速に行うことができます。NPM が依存関係を編成する手法では、アプリケーションとその依存関係が 1 つのディレクトリーに格納されるため、極めて多くの関連要素が含まれたアプリケーションになります。

インストール

サポートされているどのオペレーティング・システムでも、依存関係のインストールは、コマンドラインで「npm install [モジュール名]」と入力するだけの簡単なものです。例えば、Express.js Web フレームワーク・モジュールをインストールするには、単純に「npm install express」と入力して実行します。カレント作業ディレクトリーがプロジェクトのルートであるとすれば、このモジュールとその依存関係は node_modules という名前のフォルダーにインストールされます。

製品構成

NPM では依存関係がどのように成文化されるかというと、各モジュールの詳細は、プロジェクト・ルートにある package.json という単一のファイルに記述されます。このファイルには、プロジェクトとその依存関係を記述する JSON オブジェクトが 1 つだけあります。このファイルは、npm init コマンドによって生成することができます。インストール時に依存関係を追加するには、コマンドに --save パラメーターを追加します。例えば、「npm install async –save」を実行すると、最新バージョンの async モジュールが package.json に追加されます。さらに、「npm install mocha --save-dev」を実行すると、mocha が開発者用の依存関係として追加されます。

モジュールの依存関係が、開発者によって追加された依存関係とともに (バージョン管理情報も含めて) リストに列挙されていれば、インストールは「npm install」を実行するだけの簡単なもので、パラメーターを追加する必要はありません。この手法によって最終的には、アプリケーションを稼働させるのが極めて簡単になります。基本的には、ユーザーがアプリケーションのコード・ベースをダウンロードした後、カレント・ディレクトリーをそのプロジェクトのルート・ディレクトリーに変更して「npm install」を実行するだけで、アプリケーションは実行可能な状態になります。

モジュールの選択

NPM からモジュールを選択する際の選択肢は豊富で、同じ高度な機能を得るためのライブラリーが数多くあります。例えば、ユニット・テスト用モジュールが数多くあります。ただし、NPM レジストリーを見るとわかるように、いくつかのモジュールがその人気度で際立っています。このパッケージ・リポジトリーはコミュニティーが主導しているため、使用方法、更新、バグに関する情報は十分にあります。可能であれば、各カテゴリーの中で最も人気があり、非常に頼られていて、頻繁に更新されるモジュールを選択することをお勧めします。

NPM モジュールとしてのコマンドライン・ツール

アプリケーション内でライブラリーとして使用できる NPM モジュールに加え、コマンドライン・ツールが含まれる NPM モジュールもあります。そのようなモジュールの例としては、Mocha (ユニット・テスト用)、JSHint (スタイル・チェック用)、JSDoc (コード・ドキュメント生成用) が挙げられます。「npm install」を実行してモジュールをインストールすると、そのモジュールはローカル・プロジェクト・ディレクトリーにインストールされるだけで、その「バイナリー」は、ユーザーの PATH には追加されません。一方、グローバル (-g) オプションを使用すると、モジュールは PATH に追加されます。そのため、ユーザーは例えば CLI で「./node_modules/[モジュール名]/bin/[スクリプト名]」と入力するのではなく、「[スクリプト名]」と入力して実行することができます。この方法は便利ではあるものの、使用するのは控えることが望ましいです。それは、依存関係をローカルに維持しておくほうが、より軽量かつ多くの関連要素が含められたインストールになるからです。モジュールを除外して、そのモジュールがグローバルにエンド・ユーザーのマシンにインストールされていることを前提とするのは、バッド・プラクティスであり、移植性を制限することになります。

NPM での自動化

NPM では、依存関係管理を含む自動化は簡単です。プロジェクトの package.json ファイルには、最上位レベルに scripts キーを含めることができます。このキーは、コマンドのエイリアスと実行するリテラル・コマンドを表す、キー・バリュー・ペアのディクショナリーに相当します。基本的に、NPM ではより複雑な処理 (それが単一のコマンドであろうと、bash スクリプトであろうと) にエイリアスを割り当てて使用することができます。例えば、「npm run-script test」と入力すると mocha を実行する test コマンドを定義するには、以下の構成を使用します。

リスト 1. test コマンドを定義する
"scripts": {
     "test": "mocha -R spec"
}

モジュールのライセンス

NPM は、コミュニティー主導のパッケージ・マネージャーであり、コードは多くの開発者から寄与されています。エンタープライズ・アプリケーションを開発する際には、各モジュールで使用されているライセンスを必ず把握してください。特定のモジュールのライセンスを把握するだけでなく、そのモジュールの依存関係のライセンスにも注意しなければなりません。私たちが調べたところ、MIT License が最も一般的なライセンスであるようです。


非同期プログラミング

Java のような同期プログラミング言語を扱ってきた開発者が Node.js を学習していると、なかなか順調に進まなくなる可能性があります。これは、フロントエンドの JavaScript 開発と DOM 操作のエキスパートであっても起こり得ることです。習得に苦労する 1 つの側面は、モジュール、イベント・ループなどに関する規約に慣れることであり、もう 1 つの側面は非同期コードを作成することです。JavaScript 開発者にとって、後者は大した問題ではないかもしれませんが、非同期の Node.js コードがあまりにも複雑であるため、おそらく非同期の DOM 操作とイベント処理がささいなものに見えることでしょう。

Node.js による依存関係の処理方法とモジュールの使用方法については説明したので、ここからは非同期プログラミングについて詳しく見ていきましょう。

以下のコード・スニペットがあるとします。

リスト 2. test コマンドを定義する
1   console.log("Hello World!")
2   aFunction( aParam, function (aCallbackParam) {
3       console.log("Node is asynchronous");
4   });
5   console.log("Life is great");

この架空のコードから期待される出力は以下のとおりです。

Hello World!
Life is great
Node is asynchronous

Node.js では、単一の実行パスに沿って考えることはできません。特定の関数呼び出し (およびその関数が呼び出す非同期関数) の中で実行フローを認識する必要があります。この非同期実行を認識するのは簡単ですが、配慮しなければならない微妙な点がいくつかあります。例えば、アプリケーション全体での状態は、どのような種類のものでも、最小限にするか、読み取り専用で使用しなければなりません。そのような状態では、アトミック性が保証されずに、随時いくつもの非同期関数が作用する可能性があります。このことは、同期プリミティブを使用する他のプログラミング言語と大きくかけ離れているわけではありませんが、JavaScript/Node.js には同期プリミティブはありません。

リスト 2 の 2 行目では、何が行われているのでしょう? JavaScript では、関数を引数として渡すことができます。これにより、非同期実行が可能になります。引数としての関数は、一般に「コールバック関数」と呼ばれます。基本的にコールバック関数では、関数を呼び出すときに渡す引数は、その関数の完了時に呼び出されることが想定されます。このようにして非同期関数を呼び出すと、実行が待機状態になることなく、コードの次の行に直接進みます。JavaScript のこの特性を利用すれば、並行性の高い非同期 Node.js アプリケーションを作成することができます。コールバックを使用した関数を作成することに加え、モジュールで提供される同期関数よりも非同期関数を優先して使用することも必要です。例えば、ファイルシステムからの読み取りには、readFileSync の代わりに readFile のような関数を使用する必要があります。

非同期コードは、コールバックだけの問題ではありません。Node.js はイベント駆動型です。今度は、Node.js のコア・モジュールである http を使用して非同期 HTTP リクエストを送信した場合の動作を検討します。

リスト 3. 非同期 HTTP リクエスト
var req = http.request("http://www.ibm.com", function(res) {
  res.on('data', function (chunk) {
    console.log('BODY: ' + chunk);
  });
});

req.on('error', function(e) {
  console.log('problem with request: ' + e.message);
});

// write data to request body
req.write('data\n');
req.write('data\n');
req.end();

リスト 3 のコードは、理解しづらいかもしれません。最初の行では、ある URL に対して、レスポンスに作用するコールバックを指定した HTTP リクエストを送信しています。ただし、ここではオブジェクト全体ではなく、レスポンスに含まれるいくつかのチャンクを処理していることに注目してください。これを可能にする方法は、レスポンスの各チャンクをログに記録するコールバックを “data” イベントに登録することです。これに続き、レスポンスではなくリクエストに関連するイベント・ハンドラー、つまりエラー・ロガーを登録します。最も理解しにくいのは、最後の 3 行かもしれません。これらの行は、実際にリクエストのペイロードを書き込むためのものです。最後の req.end() の呼び出しによってリクエストの送信が実際に開始されます。これにより、イベント・ループで req.error や res.data などのイベントが発生する可能性が生まれます。

幸い、Node.js のコアとなるライブラリーとコミュニティー・ライブラリーの大半は、このイベント管理の大部分を抽象化しています。これらのライブラリーは、単純にコールバックを引数として取る関数を提供する傾向があり、非同期実行を保証しながらもエンド・ユーザーにはイベントを懸念させないようにしています。

この新しいスタックについて習得するのは、それほど困難なことではありません。いくつかの小さなプロジェクトに取り組むうちにコツを覚えてくるはずです。不適切な制御フローによる見つかりにくいレース・コンディションを突き止めるためのデバッグを 2、3 度繰り返すうちに、厄介なポイントについても十分に理解するようになってきます。最後になりますが、非同期コードの制御フローを管理するためのソリューションとしては、async ライブラリーが非常によく使われています。


大規模アプリケーションでの Express.js

Express.js は、Node.js において最も広く使用されている Web アプリケーション・フレームワークです。Express.js は根本的にミドルウェアの概念に基づいて作成されています。ミドルウェアとは、HTTP リクエストに適用される関数のことです。Express.js アプリケーションでは、リクエストをそのリクエスト (パス、メソッドなど) に基づく一連のミドルウェア (ミドルウェアのスタック) にマッピングします。Express.js を使用すれば、単純なモデル・ビュー・コントローラー (MVC) 形式の Web アプリケーションをわずかな時間で作成することができます。

Node.js で Express.js Web アプリケーション・フレームワークを使用するための手引書はたくさんありますが、その多くは Express.js の最も単純な機能と使用法しか反映していません。Express.js ベースのアプリケーションのサイズが大きくなってくると、構造化の問題が保守性に影響を及ぼす可能性が出てきます。大規模な Express.js アプリケーションの構造化に対する規範的な手法や最善の手法はありませんが、ここではチームの経験を基に、推奨事項と教訓を紹介したいと思います。

Express.js で「ルート」を定義するには、HTTP メソッドか、正規表現、またはリクエストのパス部分のストリングのいずれかと、適用するミドルウェア関数を指定します。例えば、images/[imageID] に対するすべての GET リクエストを、特定の画像オブジェクトを返す関数にルーティングするとしたら、以下のようなコード・スニペットを使用することになります。

リスト 4. GET リクエストのルーティング
var express = require("express"),
      app = express();
app.get("/images/:imageID", function (req, res, next) { … code to return image object ...});

Express.js アプリケーションの単純な構造化手法では、すべてのルートに対して上記のような行を使用することもできますが、私たちの場合、プロジェクトが複雑化してきたことから、アプリケーションの残りの部分のモジュール性に適合するようにルート構成をリファクタリングしました。具体的には、ルート構成をミドルウェア関数が定義されているモジュールに移し、これらのモジュールのそれぞれに Express.js のインスタンス (究極的にはミドルウェアの小さなスタック) を割り当て、メイン・アプリケーションからはプレフィックスを使用して各インスタンスへとルーティングするようにしました。例として、画像リソースに関連するルートの構成方法を参照してください。

リスト 5. ファイル: app.js
…
var imagesAPI = require("./routes/images.js");
…
app.use("/images*", imagesAPI);
app.use("/users*", usersAPI);
リスト 6. ファイル: images.js
…
function getImage(req, res, next) {…}
function postImage(req, res, next) {…}
function init(req, res, next) {
	…
	return next()
}

var api = express();
// Init middleware for API
api.all("/*", init);
// Path: /images/:imageID
api.get("/:imageID", getImage);
// Path: /images/
api.post("/", express.bodyParser(), postImage);

// Export the small Express app composed of the routes representing this API module
module.exports = api;

上記の例は、ミドルウェア (および複数のミドルウェアからなるミドルウェア) を使用して、アプリケーションの最上位レベルにモジュール方式で Express.js ルートを定義する方法を示しています。このモジュール方式は保持することが可能であるため、アプリケーションを拡張しやすくなります。この手法に従えば、API (例えば、「/v1/mobile/images」) のバージョン管理が特に容易になります。さらに、ミドルウェアのグループ化に対して init 関数を使用できることは、重複するコードを削減するのに役立ちます (例えば、リクエストに状態を追加するなど)。おそらく、この手法を改善または標準化する余地はあると思いますが、複雑な Express.js アプリケーションの最上位レベルにすべてのルートを残しておくよりも望ましいソリューションであることは明らかです。私たちが知っている限りでは、app.map では組み込みの本体パーサー (express.bodyParser) のような中間ミドルウェアを使用できないため、この手法は app.map を使用した手法とは若干異なります。

このような Express.js アプリケーションの構造化手法は、RESTful な Web サービス (IBM Passes) を構築するという事例には好都合でした。リソースは JSON でのみ提供されるため、私たちのアプリケーションにはテンプレート機能は組み込まれていません。そのため、この手法に関する正式な発言は一切できませんが、テンプレート機能がこの手法を何らかの形で複雑にするとは考えられません。テンプレート機能はミドルウェア・スタックでの追加ステップに過ぎないので、モジュール方式にも見事に適合できるはずです。概してこれは、Express.js アプリケーションを「動作するように構築する」ものから、「保持して拡張するように構築する」ソリューションへと変えられる単純な変更です。


アプリケーション・レベルのロギング

ロギングの 6 つのレベル

  • Fatal (60) ― アプリケーションが使用不可になりつつあるか、使用不可になっている。
  • Error (50) ― 特定の機能にとっては Fatal レベルであるが、アプリケーションは引き続き実行されている。
  • Warn (40) ― オペレーターによる調査が必要であることを通知する警告。
  • Info (30) ― アプリケーションの通常の機能を説明する情報。
  • Debug (20) ― 通常の機能に関する詳細な情報を追加。
  • Trace (10) ― 極めて詳細なアプリケーション・ロギング。

重要なロギング機能のために、私たちはよく使われている Bunyan というモジュールを使用しました。Bunyan は Node.js 対応の単純かつ高速な JSON ロギング・モジュールです。Bunyan を使用することにより、ログ・メッセージの詳細をそのレベルに応じてフィルタリングすることができます。しかも、構成に必要なのは、モジュールを必要なロギング・レベルとともに宣言することのみです。このように簡単に構成できることに加え、出力を読みやすいログへとインテリジェントにフォーマットの設定をすることができます。例えば、オブジェクトのプロパティーを含んだ構造化されていない行を出力するのではなく、適切にフォーマットが設定された JSON オブジェクトを出力することができます。また、Bunyan には、ログを読み込むための使いやすいコマンドライン・インターフェースも備わっています。これを、UNIX tail のようなコマンドライン・ツールと組み合わせると、リアルタイムでロギングするための強力なツールとなります。

ロギング・レベルの各数値は、優先度の値を表します。例えば、Warn レベルのすべてのログを表示するには、このレベルに対応する数値 (40) を指定するか、レベル名自体を指定します。すると、指定したレベルのログと、そのレベルより上のすべてのレベルのログを受け取ることになります。例えば、Debug レベルのログを表示するとしたら、Debug レベルに加え、Info レベル、Warn レベル、Error レベル、および Fatal レベルのログも表示されるというわけです。


データ・モデリング手法

プロジェクトの初期段階で、私たちはよく使われている MongoDB 対応オブジェクト・モデリング・ツールである Mongoose を使用することに決めました。この Node.js モジュールは、リレーショナル・データベースで使用するようなオブジェクト・リレーショナル・マッパーと似ていますが、MongoDB とともに動作するモジュールです。Mongoose は (ホスト・プログラミング言語のネイティブ構成体として都合よく表現されたスキーマによって) お馴染みの宣言型のデータ・モデリング手法を可能にします。さらに、オブジェクトのプロパティーに対して検証ロジックを定義することもできます。これには、作成、取得、更新、削除などのアクションの前後に実行されるロジックも含まれます。

リスト 7. Mongoose でのスキーマおよびデータ・モデルの使用例
var catSchema = mongoose.Schema({ name: String, required: true });
var Cat = mongoose.model('Cat', catSchema);
var cat1 = new Cat();
cat1.name = "Felix";
cat1.save(callback);

私たちの厳格に定義されたパス・オブジェクトにとっては、この手法が理に適っていました。パスの背後にある JSON は直接 JavaScript オブジェクトとして解釈できるため、Mongoose スキーマを使用して 1 対 1 でモデル化できると考えたからです。しかし残念ながら、Mongoose にはネストに関して限界があることがわかりました。パス・オブジェクト構造はいくつかのレベルでネストされていて、検証には複数の側面があります。これに対するソリューションとしては、概念上は Mongoose の検証機能が便利でしたが、ネストされたスキーマを定義することはできませんでした。つまり、パスのあらゆる「サブ・ドキュメント」にスキーマが必要となり、これらのスキーマを最上位レベルで参照する必要があることから、必然的にリレーショナル・データベースの手法に戻ってしまうためです。それでは、MongoDB を使用する意味がありません。また、パスに関する操作ごとのデータベース・クエリーの数も大幅に増えることになります。

そこで、私たちは Node.js のネイティブ MongoDB ドライバーを使用することにしました。私たちはスキーマに縛られていなかったため、データ・モデリングを繰り返す柔軟性がありました。基本的に、複合パス・オブジェクトをそのままの構造を維持してデータベースに挿入するという私たちの手法は、標準的な GIGO (Garbage In, Garbage Out) の考え方 (つまり、「入力が不適切であれば、得られる出力も不適切なものになる」という考え方) によるものです。Mongoose の便利な検証機能を実行するという選択肢はなくなったので、独自の検証ロジックを実装しなければなりませんでしたが、Mongoose で使用しているのと同じような、しかもネストもサポートする (したがって、再帰もサポートする) 汎用の宣言型スタイルのバリデーター・クラスを作成するのは簡単でした。これらのバリデーター・クラスを用意して、Apple Passbook の仕様に 1 対 1 で対応するように検証ロジック再作成しました。


アプリケーション固有の問題とそこから学んだ教訓

パスを表現するファイルについての Apple Passbook の仕様は、極めて詳細にわたります。仕様では、パスを作成するために使用する複数の JSON 文書と画像を含めた zip ファイルの生成についても言及しています。この zip ファイルには、すべてのファイルのチェックサムが含まれるマニフェスト・ファイルも含めます。さらに zip ファイルに対するもう 1 つの要件として、このマニフェスト (したがってそこに含まれるチェックサム) が改ざん、または破損していないことを検証するために、マニフェストの PKCS#7 署名を別個に含める必要もあります。

しかし残念なことに、Node.js のネイティブ暗号化モジュール (「crypto」) は PKCS#7 をサポートしていません。これは、Node.js がまだ成熟していないことによる欠点であり、この欠点のために私たちは代替手段を調査せざるを得ませんでしたが、結果的に一般公開されている非同期のモジュールを見つけることはできませんでした。PKCS#7 に関する仕様を読んで (純粋な JavaScript か、JavaScript バインディングを使用したネイティブ・コードで) Node.js モジュールを実装するという方法も考えられましたが、時間的制約から、それは最適なソリューションではないと判断しました。そこで、最も無難な方法として、Node.js の依存関係である OpenSSL を使用することにしました。Node.js の依存関係を使用すれば、移植性に悪影響を与えないと判断したからです。Node.js の子プロセス生成機能を利用して、コマンドライン・ツールと同じように単純に OpenSSL を使用しました。

この方法は、明らかに改善が必要です。コマンドライン・ツールを使用することは、信頼できないプラクティスである上に、パフォーマンスに与える影響は良いものではありませんでした。私たちのベンチマークでは、最終的なパスの生成時間は短縮されましたが、署名を行う実際のコード・パスが Node.js アプリケーション内のボトルネックとなることが明らかになりました。また、

イベント・ループをブロックしない分離したプロセスを生成することはできましたが、長時間実行される署名プロセスでは、同期コードを避けることはできませんでした。パスの生成で最も時間がかかった部分は、署名の計算です。暗号化には本質的に高い計算コストが伴うのは当然のことですが、それでも私たちは、このプロセスが深刻かつ不要なボトルネックであると考えています。本番アプリケーションには、適切な Node.js バインディングを使用したネイティブ実装が必要になってきます。

署名のボトルネックだけでなく、パス生成パイプラインの圧縮部分にも比較的時間がかかりました。圧縮も CPU バウンドなタスクであるため、多かれ少なかれ時間がかかることは避けられません。このことは、Node.js が CPU インテンシブなワークロードには最適でないことを、はっきりと再認識させてくれます。


Node.js 開発で陥りがちな落とし穴

暗黙的な制御フローを避けること

Node.js 開発で非同期関数を使用することの重要性は説明しましたが、その一方でコールバックを指定した非同期関数を使用すると、制御フローが複雑になります。非同期パターンを使用しても、単一の論理パスが意図されていた場合に、本質的に実行パスが複数になってしまう可能性があります。この問題は通常、明示的な制御フローなしで非同期関数またはそのコールバックを呼び出しているために発生します。そのような場合には、暗黙的な制御フロー (ブロック内の最終ステートメントなど) が問題の原因であると考えられます。ただし、次第に大きくなっていくコード・ベースでは、そのような暗黙的リターンだけにとどまらず、変更による意図せぬ結果として、他の命令が追加されている可能性もあります。

基本的に、暗黙的な制御フローは一般的なベスト・プラクティスではあるものの、Node.js アプリケーションでは診断しにくい副次的影響をもたらす可能性があります。制御フローは、リスト 2 とリスト 3 に示すように常に明示的にすることをお勧めします。

リスト 8. コード・ブロックの最終ステートメントにするコールバックと非同期関数で、return 文を使用すること
function (callback) {
          if (someCondition) {
return someAsyncFunction(param1, param2, function (err, results) {
	                     if (err) {
		                     log.error(err);
	                     }
	                     return callback(err);
                     });
          }
return callback();
}
リスト 9. 非同期メソッドの最後で単一のコールバックを使用して、コールバックを複数回呼び出さないようにすること
function (callback) {
	return someAsyncFunction(param, function (err, results) {
		if (err) {
			log.error(err);
                                // Calling callback in this branch is a Bad Idea
		}
		return callback(err, results);
           });
}

動的に型付けされる言語では、オブジェクトやそのプロパティーについて憶測を立てないこと

これは当たり前のことのように聞こえますが、実際には忘れがちです。静的に型付けされる言語を使い慣れている場合にはなおのこと、責任はコンパイラーから開発者に移ることを肝に銘じる必要があります。Web アプリケーション開発のコンテキストでは、GIGO (Garbage In, Garbage Out: 「入力が不適切であれば、得られる出力も不適切なものになること」) を前提としなければなりません。このように考えた上で、一般的に有効なソリューションとなるのは、前提 (または想定していること) が否定されるかどうかを確認し、前提が満たされない場合にはすぐに元に戻ることです。これは特に、JavaScript オブジェクトとそのプロパティーに言えることです。ネストされたプロパティーを操作するときにはさらに微妙な違いが出てくるため、参照されているすべてのプロパティー (またはキー) が存在することを確認する必要があります。

リスト 10. 前提が否定されるかどうかを確認する
function (someParam) {

          if (!assumptions about param) {
                     // Handle this unexpected value
          }

         // Proceed with assumptions tested
}
リスト 11. ネストされたプロパティーの微妙な違い
function (someParam) {

         if (!someParam ||
             !someParam.someProperty ||
             !someParam.someProperty.nestedProp) {
	         // Handle unexpected value
         }

        // Proceed with assumptions
}

依存関係の振る舞いを理解すること

この助言は言うまでもないかもしれませんが、非同期コードを使用することが重要な Node.js 開発のコンテキストでは重要なことです。ライブラリーが何を保証するかを理解せずにライブラリーを使用したり、その保証が不明瞭な場合にライブラリーの実際の実装を使用したりすると、アプリケーションにボトルネックを生じさせる可能性があります。

さらに、依存関係によって残される副次的影響を考慮してください。Express.js ベースの Node.js アプリケーションを作成するのに理想的な手法は、完全に機能するステートレスなミドルウェアを作成することです。ただし、Express.js の一部のコア・コンポーネントでさえも副次的影響を与えます。例えば、リクエスト本体を (JSON ストリングからネイティブ JavaScript オブジェクトに変換するなどして) 自動的に構文解析する bodyParser ミドルウェアには副次的影響があります。それは、一時ファイルをユーザーが削除しなければならないことです。私たちの場合は、アプリケーションの負荷テストを数週間行った後、大量の不可解な一時ファイルによってオペレーティング・システムが完全に停止しました。この問題は解決するために、bodyParser が残した一時ファイルを削除しました。


まとめ

この記事で目標としたのは、Node.js 開発手法を高圧的に指示することではなく、私たちのチームが直面したいくつかの問題と、それらの問題を解決する過程で学んだ教訓を紹介することです。ここで取り上げた手法には価値があると確信しています。また、この特定のアプリケーションに限らずに適用される基本原則的な観点も強調したいと思います。一般的な言葉で言うと、それは常に現状に挑戦することの重要性を信じているということです。絶えず評価を続けることが、最善の方法で問題に取り組んでいることを確信するための唯一の方法となります。


謝辞

私たちのメンターとして、インターンシップ期間全体にわたりその見識と経験で私たちを導いてくださった Joshua A. Alger 氏、Andy Dingsor 氏、Curtis M. Gearhart 氏、Christopher Hambridge 氏、Todd Kaplinger 氏に深く感謝いたします。私たちの成功は、IBM Extreme Blue の RTP Lab Manager である Ross Grady 氏の忍耐強さと励ましで絶え間なく改善を重ねることができたおかげです。また、Node.js の経験と貴重なフィードバックを提供してくださった Jeff Jagoda 氏にも感謝の意を表明いたします。

参考文献

学ぶために

製品や技術を入手するために

  • IBM 製品の評価版をダウンロードして、DB2、Lotus、Rational、Tivoli、および WebSphere のアプリケーション開発ツールやミドルウェア製品を体験してください。

議論するために

  • developerWorks コミュニティーに参加して、開発者によるブログ、フォーラム、グループ、Wiki を調べ、仲間やエキスパートとのつながりを持ってください。

コメント

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=Mobile development, Java technology
ArticleID=945551
ArticleTitle=Node.js と MongoDB を使用してモバイル・アプリを開発する: 第 2 回 ヒントと秘訣
publish-date=09192013