よくあるアンチパターン
よくあるアンチパターン
Section titled “よくあるアンチパターン”Javaでよくあるアンチパターンと、実際に事故った構造を詳しく解説します。
A. リソースの「垂れ流し」
Section titled “A. リソースの「垂れ流し」”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: 例外を握りつぶしてファイルやDB接続を閉じない@Servicepublic class FileService { public void processFile(String filePath) { FileInputStream fis = null; try { fis = new FileInputStream(filePath); // ファイル処理... } catch (IOException e) { // 問題: 例外を握りつぶしてファイルを閉じない log.error("File processing failed", e); // ファイルが閉じられず、ファイル記述子がリーク } // 問題: finallyブロックがないため、正常終了時もファイルが閉じられない }}なぜ事故るか:
- ファイル記述子の枯渇: ファイルが閉じられず、OSのファイル記述子が枯渇する
- 接続リーク: DB接続が閉じられず、接続プールが枯渇する
- 数時間後の停止: リソースリークは数時間後にシステム全体を停止させる
設計レビューでの指摘文例:
【指摘】リソースが適切に解放されていません。【問題】例外時にファイルやDB接続が閉じられず、リソースリークが発生します。【影響】ファイル記述子・接続プールの枯渇、数時間後のシステム停止【推奨】try-with-resources文またはfinallyブロックで確実にリソースを解放するB. 無防備な待機
Section titled “B. 無防備な待機”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: 外部API呼び出しにタイムアウトを設定しない@Servicepublic class OrderService { @Autowired private OrderRepository orderRepository;
@Autowired private PaymentApiClient paymentApiClient;
@Transactional public Order createOrder(OrderData orderData) { // 1. 注文を作成(データベースに保存) Order order = orderRepository.save(new Order(orderData));
// 2. トランザクション内で外部APIを呼ぶ(問題) // 問題: タイムアウトが設定されていない // 問題: サーキットブレーカーがない PaymentResult result = paymentApiClient.chargePayment( order.getId(), orderData.getAmount() );
if (!result.isSuccess()) { throw new PaymentException("Payment failed"); }
// 3. 決済結果を保存 order.setPaymentStatus("COMPLETED"); return orderRepository.save(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 “実際に事故った構造”// ❌ アンチパターン: 再送時にデータが二重登録される@Servicepublic class OrderService { @Transactional public Order createOrder(OrderData orderData) { // 問題: Idempotency Keyがない // 問題: 再実行時に注文が二重作成される Order order = new Order(orderData); return orderRepository.save(order); }}
// クライアント側でリトライpublic void createOrderWithRetry(OrderData orderData) { for (int i = 0; i < 3; i++) { try { orderService.createOrder(orderData); return; // 成功 } catch (Exception e) { if (i == 2) throw e; // 最終リトライ失敗 Thread.sleep(1000 * (i + 1)); // リトライ } }}なぜ事故るか:
- 二重登録: ネットワークエラーでクライアントが再送すると、注文が2つ作成される
- データの不整合: 同じ注文が複数存在し、在庫や決済に影響する
- ビジネスロジックの破綻: 重複データにより、ビジネスロジックが正しく動作しない
設計レビューでの指摘文例:
【指摘】非冪等な再試行が実装されています。【問題】再送時にデータが二重登録され、データの不整合が発生します。【影響】データの不整合、ビジネスロジックの破綻【推奨】Idempotency Keyを使用して冪等性を保証するD. Serverless環境での長時間実行
Section titled “D. Serverless環境での長時間実行”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: Serverless環境での長時間実行@RestControllerpublic class ReportController { @Autowired private ReportService reportService;
@PostMapping("/reports/generate") public Report generateReport(@RequestBody ReportRequest request) { // 問題: 10分かかる処理(Vercelの最大実行時間300秒を超える) return reportService.generateLargeReport(request); }}なぜ事故るか:
- 実行時間の制限: Vercelは最大300秒、AWS Lambdaは最大15分
- タイムアウト: 処理が完了する前にタイムアウトする
- リソースの浪費: タイムアウトまでリソースを占有する
実際の事故例:
2024-01-01 10:00:00 - レポート生成開始2024-01-01 10:00:01 - データベースからデータを取得(100万件)2024-01-01 10:04:50 - データ処理中(4分50秒経過)2024-01-01 10:04:59 - Vercelのタイムアウト(299秒経過)2024-01-01 10:05:00 - 関数が強制終了→ 結果: レポートは生成されず、ユーザーはエラーを受信設計レビューでの指摘文例:
【指摘】Serverless環境で長時間実行される処理を実装しています。【問題】Vercelの最大実行時間(300秒)を超える可能性があります。【影響】タイムアウトエラー、ユーザー体験の悪化【推奨】非同期処理に変更し、ジョブキューを使用するE. チェック例外の無視
Section titled “E. チェック例外の無視”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: チェック例外の無視@Servicepublic class FileService { public void processFile(String filePath) { try { File file = new File(filePath); // チェック例外を無視 file.createNewFile(); // IOExceptionが発生する可能性がある } catch (Exception e) { // 空のcatchブロック(例外を無視) } }}なぜ事故るか:
- エラーの隠蔽: エラーが発生しても処理が続行される
- デバッグの困難: エラーの原因が分からない
- データの不整合: エラーが発生しても処理が続行され、データが不整合になる
実際の事故例:
2024-01-01 10:00:00 - ファイル処理開始2024-01-01 10:00:01 - ファイル作成を試みる2024-01-01 10:00:02 - IOException発生(ディスク容量不足)2024-01-01 10:00:03 - 例外が無視される(空のcatchブロック)2024-01-01 10:00:04 - 処理が続行される2024-01-01 10:00:05 - データベースに「ファイル処理完了」を記録→ 結果: ファイルは作成されていないが、データベースには「完了」と記録される(データ不整合)設計レビューでの指摘文例:
【指摘】チェック例外を無視しています。【問題】エラーが発生しても処理が続行され、データの不整合が発生します。【影響】データの不整合、デバッグの困難、ユーザーへの誤った情報の提供【推奨】適切なエラーハンドリングとロギングを実装するF. スレッドセーフティの欠如
Section titled “F. スレッドセーフティの欠如”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: スレッドセーフティの欠如@Servicepublic class CounterService { private int count = 0; // 問題: スレッドセーフではない
public void increment() { count++; // 問題: アトミックではない }
public int getCount() { return count; }}なぜ事故るか:
- 競合状態: 複数のスレッドが同時に
count++を実行すると、値が正しく増加しない - 可視性の問題: あるスレッドでの変更が他のスレッドに反映されない可能性がある
- データの不整合: カウントが正しくない値になる
実際の事故例:
スレッド1: count = 0 を読み取るスレッド2: count = 0 を読み取るスレッド1: count = 1 に書き込むスレッド2: count = 1 に書き込む(スレッド1の変更を上書き)→ 結果: 2回のincrement()呼び出しで、countは1しか増加しない(期待値は2)設計レビューでの指摘文例:
【指摘】スレッドセーフティが確保されていません。【問題】複数のスレッドが同時にアクセスすると、競合状態が発生します。【影響】データの不整合、予期しない動作【推奨】AtomicIntegerを使用するか、synchronizedキーワードを使用するG. リソースリーク(重複)
Section titled “G. リソースリーク(重複)”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: リソースリーク@Servicepublic class FileService { public void processFile(String filePath) { FileInputStream fis = new FileInputStream(filePath); // 問題: リソースがクローズされない // 処理... }}なぜ事故るか:
- リソースの枯渇: ファイルハンドルがクローズされず、リソースが枯渇する
- メモリリーク: リソースが解放されず、メモリがリークする
- パフォーマンスの低下: リソースが枯渇すると、新しいリクエストが処理できなくなる
実際の事故例:
2024-01-01 10:00:00 - ファイル処理開始(ファイルハンドル1を開く)2024-01-01 10:00:01 - ファイル処理完了(ファイルハンドル1がクローズされない)2024-01-01 10:00:02 - ファイル処理開始(ファイルハンドル2を開く)...2024-01-01 10:30:00 - ファイルハンドルが1000個開かれる(上限に達する)2024-01-01 10:30:01 - 新しいファイル処理が失敗(リソース不足)→ 結果: システムが応答しなくなる設計レビューでの指摘文例:
【指摘】リソースがクローズされていません。【問題】ファイルハンドルがクローズされず、リソースが枯渇します。【影響】リソースの枯渇、メモリリーク、パフォーマンスの低下【推奨】try-with-resources文を使用するよくあるアンチパターンのポイント:
- A. リソースの「垂れ流し」: 例外を握りつぶしてファイルやDB接続を閉じない → 数時間後にシステム停止
- B. 無防備な待機: 外部API呼び出しにタイムアウトを設定しない → スレッドプールの飽和、全エンドポイントの応答不能
- C. 非冪等な再試行: 再送時にデータが二重登録される → データの不整合、ビジネスロジックの破綻
- D. Serverless環境での長時間実行: 実行時間の制限、タイムアウト
- E. チェック例外の無視: エラーの隠蔽、データの不整合
- F. スレッドセーフティの欠如: 競合状態、データの不整合
- G. リソースリーク: リソースの枯渇、メモリリーク
これらのアンチパターンを避けることで、安全で信頼性の高いシステムを構築できます。
重要な原則: 「正常に動く」よりも「異常時に安全に壊れる」ことを優先する。