Skip to content

よくあるアンチパターン

Node.jsでよくあるアンチパターンと、実際に事故った構造を詳しく解説します。

// ❌ アンチパターン: 例外を握りつぶしてファイルや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ブロックがないため、正常終了時もファイルが閉じられない
}

なぜ事故るか:

  1. ファイル記述子の枯渇: ファイルが閉じられず、OSのファイル記述子が枯渇する
  2. 接続リーク: DB接続が閉じられず、接続プールが枯渇する
  3. 数時間後の停止: リソースリークは数時間後にシステム全体を停止させる

設計レビューでの指摘文例:

【指摘】リソースが適切に解放されていません。
【問題】例外時にファイルやDB接続が閉じられず、リソースリークが発生します。
【影響】ファイル記述子・接続プールの枯渇、数時間後のシステム停止
【推奨】try-finallyブロックまたは適切なリソース管理で確実にリソースを解放する
// ❌ アンチパターン: 外部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;
});
}

なぜ事故るか:

  1. トランザクションの長時間保持: 外部APIの応答を待つ間、データベースのロックが保持される
  2. 外部障害の影響: 外部APIの障害がデータベーストランザクションに影響する
  3. ロールバックの困難: 外部APIが成功した後にトランザクションが失敗した場合、外部APIのロールバックが困難
  4. タイムアウトのリスク: 外部APIの応答が遅い場合、トランザクションがタイムアウトする
  5. イベントループのブロック: 遅延が連鎖してイベントループがブロックされ、全エンドポイントが応答不能に
// ❌ アンチパターン: トランザクション内で外部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;
});
}

なぜ事故るか:

  1. トランザクションの長時間保持: 外部APIの応答を待つ間、データベースのロックが保持される
  2. 外部障害の影響: 外部APIの障害がデータベーストランザクションに影響する
  3. ロールバックの困難: 外部APIが成功した後にトランザクションが失敗した場合、外部APIのロールバックが困難
  4. タイムアウトのリスク: 外部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を呼ぶ
// ❌ アンチパターン: 再送時にデータが二重登録される
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))); // リトライ
}
}
}

なぜ事故るか:

  1. 二重登録: ネットワークエラーでクライアントが再送すると、注文が2つ作成される
  2. データの不整合: 同じ注文が複数存在し、在庫や決済に影響する
  3. ビジネスロジックの破綻: 重複データにより、ビジネスロジックが正しく動作しない

設計レビューでの指摘文例:

【指摘】非冪等な再試行が実装されています。
【問題】再送時にデータが二重登録され、データの不整合が発生します。
【影響】データの不整合、ビジネスロジックの破綻
【推奨】Idempotency Keyを使用して冪等性を保証する
// ❌ アンチパターン: コールバック地獄
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);
});
});
});
});
}

なぜ事故るか:

  1. 可読性の低下: ネストが深くなり、コードが読みにくくなる
  2. エラーハンドリングの困難: エラーハンドリングが複雑になる
  3. デバッグの困難: エラーの原因を特定するのが困難

設計レビューでの指摘文例:

【指摘】コールバック地獄になっています。
【問題】ネストが深く、コードが読みにくくなります。
【影響】可読性の低下、エラーハンドリングの困難、デバッグの困難
【推奨】async/awaitを使用する
// ❌ アンチパターン: 未処理のPromise
app.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;
}

なぜ事故るか:

  1. エラーの隠蔽: エラーが発生しても処理されない
  2. デバッグの困難: エラーの原因が分からない
  3. データの不整合: エラーが発生しても処理が続行される

実際の事故例:

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を使用し、適切なエラーハンドリングを実装する
// ❌ アンチパターン: メモリリーク
const eventEmitter = new EventEmitter();
app.post('/orders', (req, res) => {
// 問題: イベントリスナーが削除されない
eventEmitter.on('order.created', (order) => {
sendEmail(order.userId);
});
createOrder(req.body);
res.json({ status: 'ok' });
});

なぜ事故るか:

  1. イベントリスナーの蓄積: リクエストごとにイベントリスナーが追加される
  2. メモリの枯渇: メモリが枯渇し、アプリケーションがクラッシュする
  3. パフォーマンスの低下: メモリが枯渇すると、パフォーマンスが低下する

実際の事故例:

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 - メモリ使用量: 2GB
2024-01-01 10:30:02 - OutOfMemoryError発生
2024-01-01 10:30:03 - アプリケーションがクラッシュ
→ 結果: システムが応答しなくなる

設計レビューでの指摘文例:

【指摘】イベントリスナーが削除されていません。
【問題】リクエストごとにイベントリスナーが追加され、メモリリークが発生します。
【影響】メモリの枯渇、アプリケーションのクラッシュ、パフォーマンスの低下
【推奨】イベントリスナーを適切に削除するか、一度だけ登録する
// ❌ アンチパターン: ブロッキング操作
const fs = require('fs');
app.get('/data', (req, res) => {
// 問題: 同期I/O操作(ブロッキング)
const data = fs.readFileSync('large-file.txt', 'utf8');
res.json({ data });
});

なぜ事故るか:

  1. イベントループのブロック: 同期I/O操作により、イベントループがブロックされる
  2. 他のリクエストの処理不可: イベントループがブロックされている間、他のリクエストが処理できない
  3. パフォーマンスの低下: アプリケーション全体のパフォーマンスが低下する

実際の事故例:

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アプリケーションを構築できます。

重要な原則: 「正常に動く」よりも「異常時に安全に壊れる」ことを優先する。