はじめに (対象読者・この記事でわかること)

この記事は、JavaでWebアプリケーションを開発・運用しているが、突如としてHTTP 500エラーが返却されて原因が特定できずに困っているエンジニアを対象にしています。特に、ローカル環境では再現せず、本番環境のみで発生する「謎の500エラー」に四苦八苦している方に最適です。

この記事を読むことで、JavaアプリケーションでHTTP 500エラーが発生した際の効率的な調査手順が身につきます。具体的には、スタックトレースの読み方、ログレベルの変更方法、代表的な例外の切り分け方、そして本番環境で安全にデバッグ情報を取得する方法まで、実践的なノウハウを網羅的に学ぶことができます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Javaの基本的な文法と例外処理の仕組み - Spring BootもしくはJakarta EEの基礎知識 - Linuxコマンドとtail/grepなどの基本的なログ検索スキル

500エラーは「何もわからない」エラーではない

HTTP 500エラーは「Internal Server Error」と呼ばれ、サーバー側で何らかの予期しない例外が発生したことを示します。Javaアプリケーションの場合、主に以下の3パターンが考えられます。

  1. ランタイム例外がキャッチされていない - NullPointerException、IllegalArgumentException、IndexOutOfBoundsExceptionなど
  2. チェック例外を適切にハンドリングしていない - SQLException、IOExceptionなど
  3. カスタム例外が適切にマッピングされていない - ビジネスロジックで意図的に投げた例外が、フロントにまでそのまま伝わってしまう

これらを見極めるには、「例外の親譲さん探し」が有効です。サーバーのログファイル(通常はcatalina.outspring.log)に記録されている一番最初の例外を特定することで、原因の9割が判明します。

実践:500エラーを特定・解決するまでのステップ

以下、実際に発生した500エラーを題材に、原因特定から解決までの全手順を解説します。なお、サンプルコードはSpring Boot 3系で動作確認しています。

ステップ1:ログを「がっつり」取る

まず、本番環境でログレベルを一時的にDEBUGに変更します。ただし、パフォーマンスへの影響を最小化するため、該当パッケージのみに絞ります。

Yaml
# application-prod.yml logging: level: com.example.myapp.service: DEBUG org.springframework.web: DEBUG

次に、エラー発生時刻を起点にログを絞り込みます。

Bash
# エラー発生時刻 12:34:56 の前後30秒を抽出 grep -A 50 -B 50 "12:34:5[0-9]" /opt/tomcat/logs/catalina.out \ | grep -E "(ERROR|Exception|StackOverflowError)" -A 10

このコマンドにより、以下のようなスタックトレースが得られます。

java.lang.NullPointerException: Cannot invoke "com.example.myapp.entity.User.getEmail()" because "user" is null
    at com.example.myapp.service.ReportService.generateDaily(ReportService.java:47)
    at com.example.myapp.controller.ReportController.postDaily(ReportController.java:33)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    ...

ステップ2:原因コードを特定する

上記の例では、ReportService.java:47usernullであることが原因です。該当箇所を確認すると、

Java
// ReportService.java:47 String email = user.getEmail(); // userがnull

ここでなぜusernullになるのか、バックトレースをさかのぼって調査します。今回の場合、直前のログに以下の記載がありました。

DEBUG 2025-06-02 12:34:56,011 o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor : Read "application/json;charset=UTF-8" to [com.example.myapp.dto.ReportRequest@7a8b9c0d]

つまり、フロントエンドから送信されたJSONにuserIdが含まれておらず、DBからの取得結果がnullだったことが判明します。これは、フロントエンドのバリデーション不足とAPI仕様の齟齬が原因でした。

ステップ3:即座に復旧させる

本番環境では「原因究明より復旧最優先」が鉄則です。今回のケースでは、該当メソッドにnullチェックを追加し、エラーを回避する応急処置を実施します。

Java
// 応急処置版 public Report generateDaily(Long userId) { User user = userRepository.findById(userId).orElse(null); if (user == null) { log.warn("user not found for id: {}", userId); return Report.empty(); // 空レポート返却 } String email = user.getEmail(); ... }

この応急処置を適用後、即座にデプロイを実施し、500エラーが解消することを確認します。

ステップ4:本格修正を行う

応急処置後、以下の本格修正を実施します。

  1. フロントエンドにuserIdのバリデーション追加
  2. バックエンドに@NotNullバリデーション追加
  3. 存在チェックで404を返却するように修正
Java
// 本格修正版 public Report generateDaily(@NotNull Long userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new ResourceNotFoundException("user", userId)); String email = user.getEmail(); ... }

ハマった点やエラー解決

この調査で「ハマったポイント」が2つありました。

  1. ローカルでは再現しない ローカルでは常にuserId=1のテストデータが入っていたため、エラーが発生しませんでした。本番データをローカルにインポートするスクリプトを作成し、再現環境を構築しました。

  2. ログが出ていない デフォルトのログレベルがINFOで、デバッグ情報が出ていませんでした。本番環境にログレベルを動的に変更する仕組み(Spring Boot Actuatorのloggersエンドポイント)を導入しました。

解決策

最終的に以下の対策を組み合わせることで、再発を防止しています。

  • 単体テストに「userIdが存在しないケース」を追加
  • 統合テストで「存在しないリソースへのアクセス」をカバー
  • 本番環境の監視にPrometheus + Grafanaで「500発生率」をアラート設定
  • デプロイ前に本番データのスナップショットを取得し、ステージング環境で検証

まとめ

本記事では、Javaアプリケーションで発生したHTTP 500エラーの調査・解決手順を実践的に解説しました。

  • 例外の「親譲さん」を探すことで原因の9割が判明
  • 本番環境では即座に復旧できる応急処置を最優先
  • ログレベルの動的変更とログ検索スキルが必須

この記事を通して、500エラーの「謎」が「論理的に解決できる問題」に変わったことを実感していただければ幸いです。 今後は、同様の観点で「OutOfMemoryError」や「StackOverflowError」といったJVMレベルのエラー調査方法についても記事にする予定です。

参考資料