よくあるアンチパターン
よくあるアンチパターン
Section titled “よくあるアンチパターン”Railsでよくあるアンチパターンと、実際に事故った構造を詳しく解説します。
A. リソースの「垂れ流し」
Section titled “A. リソースの「垂れ流し」”実際に事故った構造
Section titled “実際に事故った構造”# ❌ アンチパターン: 例外を握りつぶしてファイルやDB接続を閉じないclass FileService def process_file(file_path) file = File.open(file_path, 'r') # ファイル処理... rescue => e # 問題: 例外を握りつぶしてファイルを閉じない Rails.logger.error("File processing failed: #{e.message}") # ファイルが閉じられず、ファイル記述子がリーク end # 問題: ensureブロックがないため、正常終了時もファイルが閉じられないendなぜ事故るか:
- ファイル記述子の枯渇: ファイルが閉じられず、OSのファイル記述子が枯渇する
- 接続リーク: DB接続が閉じられず、接続プールが枯渇する
- 数時間後の停止: リソースリークは数時間後にシステム全体を停止させる
設計レビューでの指摘文例:
【指摘】リソースが適切に解放されていません。【問題】例外時にファイルやDB接続が閉じられず、リソースリークが発生します。【影響】ファイル記述子・接続プールの枯渇、数時間後のシステム停止【推奨】ensureブロックまたは適切なリソース管理で確実にリソースを解放するB. 無防備な待機
Section titled “B. 無防備な待機”実際に事故った構造
Section titled “実際に事故った構造”# ❌ アンチパターン: 外部API呼び出しにタイムアウトを設定しないclass OrderService def create_order(order_data) Order.transaction do # 1. 注文を作成(データベースに保存) order = Order.create!(order_data)
# 2. トランザクション内で外部APIを呼ぶ(問題) # 問題: タイムアウトが設定されていない # 問題: サーキットブレーカーがない result = PaymentService.new.charge_payment(order.id, order_data[:amount])
raise PaymentError, 'Payment failed' unless result.success?
# 3. 決済結果を保存 order.update!(payment_status: 'COMPLETED')
order end endendなぜ事故るか:
- トランザクションの長時間保持: 外部APIの応答を待つ間、データベースのロックが保持される
- 外部障害の影響: 外部APIの障害がデータベーストランザクションに影響する
- ロールバックの困難: 外部APIが成功した後にトランザクションが失敗した場合、外部APIのロールバックが困難
- タイムアウトのリスク: 外部APIの応答が遅い場合、トランザクションがタイムアウトする
- スレッドプールの飽和: 遅延が連鎖してスレッドプールが飽和し、全エンドポイントが応答不能に
実際に事故った構造
Section titled “実際に事故った構造”# ❌ アンチパターン: トランザクション内で外部APIを呼ぶclass OrderService def create_order(order_data) Order.transaction do # 1. 注文を作成(データベースに保存) order = Order.create!(order_data)
# 2. トランザクション内で外部APIを呼ぶ(問題) result = PaymentService.new.charge_payment(order.id, order_data[:amount])
unless result.success? raise PaymentError, "Payment failed" end
# 3. 決済結果を保存 order.update!(payment_status: 'COMPLETED')
order end endendなぜ事故るか:
- トランザクションの長時間保持: 外部APIの応答を待つ間、データベースのロックが保持される
- 外部障害の影響: 外部APIの障害がデータベーストランザクションに影響する
- ロールバックの困難: 外部APIが成功した後にトランザクションが失敗した場合、外部APIのロールバックが困難
設計レビューでの指摘文例:
【指摘】トランザクション内で外部APIを呼んでいます。【問題】外部APIの応答を待つ間、データベースのロックが保持されます。【影響】パフォーマンスの低下、デッドロックの発生、タイムアウトのリスク【推奨】after_commitコールバックまたはActive Jobを使用するC. 非冪等な再試行
Section titled “C. 非冪等な再試行”実際に事故った構造
Section titled “実際に事故った構造”# ❌ アンチパターン: 再送時にデータが二重登録されるclass OrderService def create_order(order_data) # 問題: Idempotency Keyがない # 問題: 再実行時に注文が二重作成される Order.create!(order_data) endend
# クライアント側でリトライdef create_order_with_retry(order_data) 3.times do |i| begin OrderService.new.create_order(order_data) return # 成功 rescue => e raise e if i == 2 # 最終リトライ失敗 sleep(1 * (i + 1)) # リトライ end endendなぜ事故るか:
- 二重登録: ネットワークエラーでクライアントが再送すると、注文が2つ作成される
- データの不整合: 同じ注文が複数存在し、在庫や決済に影響する
- ビジネスロジックの破綻: 重複データにより、ビジネスロジックが正しく動作しない
設計レビューでの指摘文例:
【指摘】非冪等な再試行が実装されています。【問題】再送時にデータが二重登録され、データの不整合が発生します。【影響】データの不整合、ビジネスロジックの破綻【推奨】Idempotency Keyを使用して冪等性を保証するD. N+1クエリ問題
Section titled “D. N+1クエリ問題”実際に事故った構造
Section titled “実際に事故った構造”# ❌ アンチパターン: N+1クエリ問題class OrdersController < ApplicationController def index # 問題: 注文ごとにユーザーを取得(N+1クエリ) @orders = Order.all # 1回のクエリ: Order.all # N回のクエリ: @orders.each { |order| order.user } endendなぜ事故るか:
- クエリ数の増加: 注文が100件ある場合、101回のクエリが実行される
- パフォーマンスの低下: データベースへのアクセス回数が増加し、パフォーマンスが低下する
- スケーラビリティの問題: データが増えると、パフォーマンスがさらに低下する
設計レビューでの指摘文例:
【指摘】N+1クエリ問題が発生しています。【問題】注文ごとにユーザーを取得するため、クエリ数が増加します。【影響】パフォーマンスの低下、データベースへの負荷増加【推奨】includesまたはeager_loadを使用して関連データを事前に読み込むよくあるアンチパターンのポイント:
- A. リソースの「垂れ流し」: 例外を握りつぶしてファイルやDB接続を閉じない → 数時間後にシステム停止
- B. 無防備な待機: 外部API呼び出しにタイムアウトを設定しない → スレッドプールの飽和、全エンドポイントの応答不能
- C. 非冪等な再試行: 再送時にデータが二重登録される → データの不整合、ビジネスロジックの破綻
- D. N+1クエリ問題: クエリ数の増加、パフォーマンスの低下
これらのアンチパターンを避けることで、安全で信頼性の高いRailsアプリケーションを構築できます。
重要な原則: 「正常に動く」よりも「異常時に安全に壊れる」ことを優先する。