よくあるアンチパターン
よくあるアンチパターン
Section titled “よくあるアンチパターン”Node.jsでよくあるアンチパターンと、実際に事故った構造を詳しく解説します。
A. リソースの「垂れ流し」
Section titled “A. リソースの「垂れ流し」”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: 例外を握りつぶしてファイルやDB接続を閉じないimport * as fs from 'fs';
function processFile(filePath: string) { let fileHandle: fs.promises.FileHandle | null = null;
try { fileHandle = await fs.promises.open(filePath, 'r'); // ファイル処理... } catch (error) { // 問題: 例外を握りつぶしてファイルを閉じない console.error('File processing failed', error); // ファイルが閉じられず、ファイル記述子がリーク } // 問題: finallyブロックがないため、正常終了時もファイルが閉じられない}なぜ事故るか:
- ファイル記述子の枯渇: ファイルが閉じられず、OSのファイル記述子が枯渇する
- 接続リーク: DB接続が閉じられず、接続プールが枯渇する
- 数時間後の停止: リソースリークは数時間後にシステム全体を停止させる
設計レビューでの指摘文例:
【指摘】リソースが適切に解放されていません。【問題】例外時にファイルやDB接続が閉じられず、リソースリークが発生します。【影響】ファイル記述子・接続プールの枯渇、数時間後のシステム停止【推奨】try-finallyブロックまたは適切なリソース管理で確実にリソースを解放するB. 無防備な待機
Section titled “B. 無防備な待機”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: 外部API呼び出しにタイムアウトを設定しないasync function createOrder(orderData: OrderData): Promise<Order> { return await prisma.$transaction(async (tx) => { // 1. 注文を作成(データベースに保存) const order = await tx.order.create({ data: orderData });
// 2. トランザクション内で外部APIを呼ぶ(問題) // 問題: タイムアウトが設定されていない // 問題: サーキットブレーカーがない const response = await fetch('https://payment-api.example.com/charge', { method: 'POST', body: JSON.stringify({ orderId: order.id, amount: orderData.amount, }), });
if (!response.ok) { throw new Error('Payment failed'); }
// 3. 決済結果を保存 await tx.order.update({ where: { id: order.id }, data: { paymentStatus: 'COMPLETED' }, });
return order; });}なぜ事故るか:
- トランザクションの長時間保持: 外部APIの応答を待つ間、データベースのロックが保持される
- 外部障害の影響: 外部APIの障害がデータベーストランザクションに影響する
- ロールバックの困難: 外部APIが成功した後にトランザクションが失敗した場合、外部APIのロールバックが困難
- タイムアウトのリスク: 外部APIの応答が遅い場合、トランザクションがタイムアウトする
- イベントループのブロック: 遅延が連鎖してイベントループがブロックされ、全エンドポイントが応答不能に
実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: トランザクション内で外部APIを呼ぶasync function createOrder(orderData: OrderData): Promise<Order> { return await prisma.$transaction(async (tx) => { // 1. 注文を作成(データベースに保存) const order = await tx.order.create({ data: orderData });
// 2. トランザクション内で外部APIを呼ぶ(問題) const result = await fetch('https://payment-api.example.com/charge', { method: 'POST', body: JSON.stringify({ orderId: order.id, amount: orderData.amount, }), });
if (!result.ok) { throw new Error('Payment failed'); }
// 3. 決済結果を保存 await tx.order.update({ where: { id: order.id }, data: { paymentStatus: 'COMPLETED' }, });
return order; });}なぜ事故るか:
- トランザクションの長時間保持: 外部APIの応答を待つ間、データベースのロックが保持される
- 外部障害の影響: 外部APIの障害がデータベーストランザクションに影響する
- ロールバックの困難: 外部APIが成功した後にトランザクションが失敗した場合、外部APIのロールバックが困難
- タイムアウトのリスク: 外部APIの応答が遅い場合、トランザクションがタイムアウトする
実際の事故例:
2024-01-01 10:00:00 - 注文作成開始2024-01-01 10:00:01 - データベースに注文を保存(ロック開始)2024-01-01 10:00:02 - 外部決済APIを呼び出し(応答待ち)2024-01-01 10:00:30 - 外部決済APIがタイムアウト(28秒経過)2024-01-01 10:00:31 - トランザクションがタイムアウト2024-01-01 10:00:32 - ロールバック(注文は削除される)2024-01-01 10:00:33 - しかし、外部決済APIは成功していた→ 結果: 決済は完了しているが、注文は存在しない(データ不整合)設計レビューでの指摘文例:
【指摘】トランザクション内で外部APIを呼んでいます。【問題】外部APIの応答を待つ間、データベースのロックが保持され、 他のトランザクションがブロックされます。【影響】パフォーマンスの低下、デッドロックの発生、タイムアウトのリスク【推奨】Outboxパターンを使用し、トランザクション外で外部APIを呼ぶC. 非冪等な再試行
Section titled “C. 非冪等な再試行”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: 再送時にデータが二重登録されるasync function createOrder(orderData: OrderData): Promise<Order> { // 問題: Idempotency Keyがない // 問題: 再実行時に注文が二重作成される return await prisma.order.create({ data: orderData });}
// クライアント側でリトライasync function createOrderWithRetry(orderData: OrderData) { for (let i = 0; i < 3; i++) { try { await createOrder(orderData); return; // 成功 } catch (error) { if (i === 2) throw error; // 最終リトライ失敗 await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); // リトライ } }}なぜ事故るか:
- 二重登録: ネットワークエラーでクライアントが再送すると、注文が2つ作成される
- データの不整合: 同じ注文が複数存在し、在庫や決済に影響する
- ビジネスロジックの破綻: 重複データにより、ビジネスロジックが正しく動作しない
設計レビューでの指摘文例:
【指摘】非冪等な再試行が実装されています。【問題】再送時にデータが二重登録され、データの不整合が発生します。【影響】データの不整合、ビジネスロジックの破綻【推奨】Idempotency Keyを使用して冪等性を保証するD. コールバック地獄
Section titled “D. コールバック地獄”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: コールバック地獄function createOrder(orderData: OrderData, callback: (error: Error | null, order?: Order) => void) { db.query('INSERT INTO orders ...', (err, result) => { if (err) return callback(err);
db.query('UPDATE inventory ...', (err, result) => { if (err) return callback(err);
fetch('https://payment-api.example.com/charge', (err, result) => { if (err) return callback(err);
db.query('UPDATE orders SET payment_status = ...', (err, result) => { if (err) return callback(err);
callback(null, result); }); }); }); });}なぜ事故るか:
- 可読性の低下: ネストが深くなり、コードが読みにくくなる
- エラーハンドリングの困難: エラーハンドリングが複雑になる
- デバッグの困難: エラーの原因を特定するのが困難
設計レビューでの指摘文例:
【指摘】コールバック地獄になっています。【問題】ネストが深く、コードが読みにくくなります。【影響】可読性の低下、エラーハンドリングの困難、デバッグの困難【推奨】async/awaitを使用するE. 未処理のPromise
Section titled “E. 未処理のPromise”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: 未処理のPromiseapp.post('/orders', (req, res) => { // 問題: Promiseが処理されていない createOrder(req.body);
res.json({ status: 'ok' });});
async function createOrder(orderData: OrderData): Promise<Order> { // エラーが発生しても処理されない const order = await prisma.order.create({ data: orderData }); return order;}なぜ事故るか:
- エラーの隠蔽: エラーが発生しても処理されない
- デバッグの困難: エラーの原因が分からない
- データの不整合: エラーが発生しても処理が続行される
実際の事故例:
2024-01-01 10:00:00 - 注文作成リクエスト受信2024-01-01 10:00:01 - createOrder()を呼び出す(Promiseが処理されない)2024-01-01 10:00:02 - レスポンスを返す(status: 'ok')2024-01-01 10:00:03 - createOrder()内でエラー発生(データベース接続エラー)2024-01-01 10:00:04 - エラーが処理されない(未処理のPromise)→ 結果: ユーザーには「成功」と表示されるが、注文は作成されていない(データ不整合)設計レビューでの指摘文例:
【指摘】未処理のPromiseがあります。【問題】エラーが発生しても処理されず、エラーが隠蔽されます。【影響】エラーの隠蔽、デバッグの困難、データの不整合【推奨】async/awaitを使用し、適切なエラーハンドリングを実装するF. メモリリーク
Section titled “F. メモリリーク”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: メモリリークconst eventEmitter = new EventEmitter();
app.post('/orders', (req, res) => { // 問題: イベントリスナーが削除されない eventEmitter.on('order.created', (order) => { sendEmail(order.userId); });
createOrder(req.body); res.json({ status: 'ok' });});なぜ事故るか:
- イベントリスナーの蓄積: リクエストごとにイベントリスナーが追加される
- メモリの枯渇: メモリが枯渇し、アプリケーションがクラッシュする
- パフォーマンスの低下: メモリが枯渇すると、パフォーマンスが低下する
実際の事故例:
2024-01-01 10:00:00 - リクエスト1受信(イベントリスナー1を追加)2024-01-01 10:00:01 - リクエスト2受信(イベントリスナー2を追加)...2024-01-01 10:30:00 - リクエスト1000受信(イベントリスナー1000を追加)2024-01-01 10:30:01 - メモリ使用量: 2GB2024-01-01 10:30:02 - OutOfMemoryError発生2024-01-01 10:30:03 - アプリケーションがクラッシュ→ 結果: システムが応答しなくなる設計レビューでの指摘文例:
【指摘】イベントリスナーが削除されていません。【問題】リクエストごとにイベントリスナーが追加され、メモリリークが発生します。【影響】メモリの枯渇、アプリケーションのクラッシュ、パフォーマンスの低下【推奨】イベントリスナーを適切に削除するか、一度だけ登録するG. ブロッキング操作
Section titled “G. ブロッキング操作”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: ブロッキング操作const fs = require('fs');
app.get('/data', (req, res) => { // 問題: 同期I/O操作(ブロッキング) const data = fs.readFileSync('large-file.txt', 'utf8'); res.json({ data });});なぜ事故るか:
- イベントループのブロック: 同期I/O操作により、イベントループがブロックされる
- 他のリクエストの処理不可: イベントループがブロックされている間、他のリクエストが処理できない
- パフォーマンスの低下: アプリケーション全体のパフォーマンスが低下する
実際の事故例:
2024-01-01 10:00:00 - リクエスト1受信(large-file.txtを読み取り開始)2024-01-01 10:00:05 - ファイル読み取り中(イベントループがブロック)2024-01-01 10:00:06 - リクエスト2受信(処理待ち)2024-01-01 10:00:07 - リクエスト3受信(処理待ち)...2024-01-01 10:00:10 - ファイル読み取り完了(5秒経過)2024-01-01 10:00:11 - リクエスト2を処理開始→ 結果: 5秒間、他のリクエストが処理されない(パフォーマンスの低下)設計レビューでの指摘文例:
【指摘】ブロッキング操作を使用しています。【問題】同期I/O操作により、イベントループがブロックされます。【影響】イベントループのブロック、他のリクエストの処理不可、パフォーマンスの低下【推奨】非同期I/O操作(fs.promises.readFile)を使用するよくあるアンチパターンのポイント:
- A. リソースの「垂れ流し」: 例外を握りつぶしてファイルやDB接続を閉じない → 数時間後にシステム停止
- B. 無防備な待機: 外部API呼び出しにタイムアウトを設定しない → イベントループのブロック、全エンドポイントの応答不能
- C. 非冪等な再試行: 再送時にデータが二重登録される → データの不整合、ビジネスロジックの破綻
- D. コールバック地獄: 可読性の低下、エラーハンドリングの困難
- E. 未処理のPromise: エラーの隠蔽、データの不整合
- F. メモリリーク: イベントリスナーの蓄積、メモリの枯渇
- G. ブロッキング操作: イベントループのブロック、パフォーマンスの低下
これらのアンチパターンを避けることで、安全で信頼性の高いNode.jsアプリケーションを構築できます。
重要な原則: 「正常に動く」よりも「異常時に安全に壊れる」ことを優先する。