L-02: スタックトレース・例外チェーン
例外が発生したとき、スタックトレースを正しくログに記録することは障害調査の基本です。e.printStackTrace() を使ってはいけない理由と、層ごとに例外をラップする「例外チェーン」の設計方法を学びましょう。
いつ使うか
- DB アクセス層・サービス層・コントローラー層など、アーキテクチャ層ごとに例外をラップしたいとき
- 障害調査のために根本原因(Root Cause)をログに残したいとき
- 低レベルの例外(
SQLExceptionなど)を上位層に公開せず、アプリ固有の例外に変換したいとき - ユーザー向けのエラーメッセージと、開発者向けの詳細ログを分けて管理したいとき
例外チェーンの仕組み
new ServiceException("msg", cause) のように コンストラクタに原因例外を渡すと、getCause() で辿れる「例外チェーン」が形成されます。
ServiceException: ユーザー表示名の取得に失敗しました
↓ getCause()
DataAccessException: ユーザー取得失敗: id=200
↓ getCause()
RuntimeException: DB接続タイムアウト(根本原因)
サンプルコード
Java 8 では例外クラスを static inner class として定義するのが一般的です。DataAccessException → ServiceException と層ごとにラップすることで、呼び出し元はどの層で何が起きたかを型で判断できます。原因例外(cause)を必ずコンストラクタに渡してください。
よくあるミス・注意点
⚠️ e.printStackTrace() を本番コードで使ってはいけない理由
e.printStackTrace() には3つの問題があります。 ①標準エラー出力(System.err)にしか出力されないため、 ログファイルには記録されません。 ②ログレベル(SEVERE / WARNING / INFO)が制御できないため、重要度が分かりません。 ③ログフォーマットが統一されず、ログ収集ツールでの検索・集計が困難になります。 必ず logger.log(Level.SEVERE, "メッセージ", e) を使ってください。
⚠️ 例外をラップするとき cause を必ず渡す
throw new ServiceException("msg") のように cause を渡さないと、元の例外のスタックトレースが完全に失われます。 根本原因が分からなくなるため、必ずthrow new ServiceException("msg", e) と書いてください。
⚠️ getMessage() だけ取るのは最悪のパターン
catch (Exception e) { throw new RuntimeException(e.getMessage()); } は最悪のパターンです。getMessage() だけを取ると 元の例外クラス・スタックトレースが完全に失われ、障害調査が非常に困難になります。 必ず throw new RuntimeException(e) またはthrow new RuntimeException("msg", e) と書いてください。
⚠️ ユーザー向けメッセージと開発者向けメッセージを分けて設計する
例外のメッセージには2つの読者がいます。 デバッグを行う開発者向けには「どの ID で何が起きたか」などの技術的詳細を含め、 エンドユーザー向けには「しばらく時間を置いてから再度お試しください」など分かりやすい日本語を返すよう、 層を分けて管理しましょう。
テストする観点
- 例外チェーンの深さが期待通りであること(境界値: 深さ1・深さ2・深さ3)
getCause()を辿ると根本原因に到達できること- 不正な ID(0 以下・100 超)を渡したとき、それぞれ異なる根本原因例外が返ること
- 正常な ID を渡したとき、例外が発生しないこと(境界値: id=1, id=100)
logger.log()に例外を渡したとき、LogRecord.getThrown()に例外が設定されること- cause を渡さずにラップした場合、
getCause()が null を返すこと(誤実装の検出)