Java 例外処理の設計で迷いやすいポイント

Javaの例外処理設計で業務例外、RuntimeException、ログ出力を整理する図

例外処理の設計は「どこでcatchするか」だけでは決まらない

Java 例外処理は、実務で経験を積むほど迷いやすいテーマです。新人の頃はtry-catchの書き方を覚えれば十分に見えますが、業務アプリでは、例外を握りつぶしてよいのか、RuntimeExceptionで投げてよいのか、ログをどこで出すのかといった判断が増えます。

また、例外処理はコードの見た目以上に、保守性、障害調査、APIの使いやすさに影響します。Service層で全部catchしてしまうとControllerAdviceで共通処理できず、逆に何でもRuntimeExceptionで投げるだけでは業務上のエラーとシステム障害の区別がつきにくくなります。

そこでこの記事では、Javaの例外処理設計で迷いやすいポイントを、実務目線で整理します。checked exceptionとRuntimeExceptionの違い、業務例外の作り方、ログ出力、Spring BootでのControllerAdvice、コードレビューで見られやすい観点を取り上げます。

この記事でわかること

  • Javaの例外処理設計で最初に分けるべき観点
  • checked exceptionとRuntimeExceptionの使い分け
  • 業務例外を設計するときの考え方
  • ログをどこで出すべきか
  • Spring BootでControllerAdviceを使う理由

Java 例外処理でまず分けたい3種類のエラー

まず、Javaの例外処理設計では、すべてのエラーを同じように扱わないことが重要です。実務では、少なくとも次の3つに分けると整理しやすくなります。

種類設計上の扱い
業務ルール違反在庫不足、申込期限切れ、権限なし業務例外として扱い、ユーザーに意味のあるメッセージを返す
外部要因の失敗外部API失敗、ファイル読み込み失敗、DB接続失敗復旧可能性やリトライ可否を考え、ログと通知を設計する
実装上の不具合NullPointerException、想定外の分岐、配列範囲外原則として握りつぶさず、原因を調査できる形で残す

ユーザーが申込期限を過ぎた商品に申し込もうとした場合、それはシステム障害ではなく業務ルール上のエラーです。そのため、ログにERRORとして大量に出すより、適切なレスポンスと画面表示につなげる方が自然です。

一方で、DB接続エラーや想定外のNullPointerExceptionは、ユーザーの操作だけでは解決できません。このようなエラーには調査できるログ、通知、再発防止の観点が必要であり、例外処理で大事なのは、そのエラーを誰がどう扱うべきかを決めることです。

checked exceptionとRuntimeExceptionの使い分け

Javaでは、例外は大きくchecked exceptionとunchecked exceptionに分けられます。OracleのJava APIでも、ExceptionRuntimeExceptionは別のクラスです。

checked exceptionは、呼び出し側にcatchまたはthrowsを強制します。ファイル入出力や一部の外部リソース操作では、呼び出し側に失敗の扱いを考えさせる意味があります。

ただし、業務アプリのService層でchecked exceptionを多用すると、throwsが広がりやすくなります。Controllerや別のServiceまで例外宣言が伝播し、処理すべき場所も分かりにくくなります。

Spring Bootの業務アプリでは、業務例外を独自のRuntimeExceptionとして定義する設計がよく使われます。ControllerAdviceでまとめてレスポンスに変換しやすい一方で、RuntimeExceptionなら何でもよいという意味ではなく、業務例外、外部連携エラー、想定外エラーはクラスやエラーコードで意味を分けることが大切です。

ユーザーに返す意味から業務例外を設計する

業務例外を設計するときは、開発者の都合ではなく、ユーザーやAPI利用者に返す意味から考えると整理しやすいです。

ECサイトで在庫が足りない場合を考えます。このケースで単にRuntimeExceptionを投げるだけでは、画面側は「なぜ購入できないのか」を判断しにくくなります。

throw new RuntimeException("在庫が不足しています");

まずは動きますが、実務ではこれだけだと弱く、エラー種別、エラーコード、表示メッセージ、ログに出す情報を分けられないままです。

次のように業務例外として定義しておくと、ControllerAdvice側で共通処理しやすくなります。

public class BusinessException extends RuntimeException {
    private final String errorCode;

    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() {
        return errorCode;
    }
}

そして、Service層では業務ルール違反が起きたときに、意味のあるエラーコードを付けて投げます。

if (stockQuantity < orderQuantity) {
    throw new BusinessException("STOCK_SHORTAGE", "在庫が不足しています");
}

この形なら、画面表示、APIレスポンス、ログ、テストで同じ意味を扱いやすくなります。業務例外は「例外クラスを作ること」だけではなく、業務上どんな失敗を利用者へ伝えるかを設計するためのものです。

例外をcatchする場所は責務で決める

Javaの例外処理でよくある迷いが、「どこでcatchするか」です。結論としては、その層が責任を持てる例外だけcatchします。

catchする例避けたいこと
Repository基本的にはフレームワークに任せるDB例外を握りつぶして空リストを返す
Service業務判断として別の例外へ変換するすべてcatchしてRuntimeExceptionに包む
Controller個別の入力やレスポンス変換が必要な場合各Controllerに同じtry-catchを書く
ControllerAdvice共通のエラーレスポンスへ変換する原因調査に必要な情報まで消す

Service層で次のように何でもcatchしてしまうと、障害調査が難しくなります。

try {
    orderRepository.save(order);
} catch (Exception e) {
    return false;
}

このコードでは、DB接続エラーなのか、制約違反なのか、実装バグなのかが呼び出し側から分かりません。さらに、ログも残らないため、あとから原因を追えません。

Service層では、業務的な意味に変換する責務もあります。外部APIのレスポンスを見て「審査に失敗した」という業務例外へ変換する場合など、業務判断を持つ場面では例外変換が自然です。

ControllerAdviceでエラーレスポンス変換を共通化する

Spring BootでREST APIを作る場合でも、例外をAPIレスポンスに変換する処理はControllerAdviceに寄せると管理しやすくなります。公式ドキュメントでも、Controller AdviceはController横断の処理に使える仕組みとして説明されています。

業務例外を400 Bad Requestとして返す場合は、次のように共通化できます。

@RestControllerAdvice
public class ApiExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(
            BusinessException e) {

        ErrorResponse response = new ErrorResponse(
                e.getErrorCode(),
                e.getMessage()
        );

        return ResponseEntity.badRequest().body(response);
    }
}

この形にすると、Controllerごとに同じtry-catchを書かなくて済みます。加えて、エラーレスポンスの形式を統一しやすくなります。

ただし、ControllerAdviceにすべての業務判断を書くのは避けます。この仕組みの役割は例外をレスポンスに変換することであり、在庫不足かどうか、申込可能かどうかといった判断はService層で行う方が自然です。

ログは「同じ例外で何度も出さない」ことが重要

例外処理設計では、ログの出し方も重要です。Service層とControllerAdviceの両方でログを出すと、同じ例外が何度もERRORログに出ます。

ログが多ければ調査しやすいわけではありません。同じスタックトレースが何度も出ると本当に見るべき箇所が埋もれるため、例外を最終的に処理する場所でログを出す方が整理しやすいです。

例外の種類ログレベルの考え方補足
業務ルール違反INFOまたはWARNユーザー操作として起こり得るならERRORにしない
外部API失敗WARNまたはERRORリトライ可否や影響範囲で判断する
DB接続エラーERROR障害として調査できる情報を残す
NullPointerExceptionなど想定外ERRORスタックトレースを残し、再発防止につなげる

なお、ログには個人情報や機密情報をそのまま出さないようにします。リクエストボディ全体をログに出す設計は調査には便利に見えますが、セキュリティ上のリスクになるため、必要なID、処理名、エラーコード、相関IDなどを選んで残すことが大切です。

Java 例外処理でレビュー指摘されやすいNG例

ここまでの内容を踏まえると、Javaの例外処理では次のようなコードがレビューで指摘されやすいです。

NG例起きる問題改善の方向性
catchして何もしない障害の原因が追えない処理できないなら上位へ投げる
catchしてfalseやnullを返す失敗理由が消える意味のある例外や結果型で表現する
Exceptionを広くcatchする想定外エラーまで業務エラー扱いになる処理したい例外だけcatchする
RuntimeExceptionをそのまま乱用する業務エラーと障害の区別がつかない業務例外やエラーコードを設計する
各Controllerで同じtry-catchを書くレスポンス形式がばらつくControllerAdviceで共通化する
複数箇所で同じ例外ログを出すログが重複して調査しづらい最終処理地点でログを出す

特に、catchして何もしないコードは危険です。ローカルでは問題なく見えても本番で障害が起きたときに原因を追えず、例外処理の目的である失敗時の振る舞いの明確化から外れてしまいます。

よくある質問

Javaの業務例外はchecked exceptionにするべきですか?

必ずchecked exceptionにする必要はありません。Spring Bootの業務アプリでは、ControllerAdviceで共通的にレスポンス変換しやすくするため、独自のRuntimeExceptionとして業務例外を定義する設計もよく使われますが、見るべき点はcheckedかuncheckedかではなく、業務エラーとして区別できる設計になっているかです。

Exceptionをcatchしてはいけませんか?

絶対にいけないわけではありません。ただし、Exceptionを広くcatchすると、想定していない不具合まで通常の業務エラーとして扱ってしまう危険があるため、処理したい例外が明確なら、その例外型をcatchする方が安全です。

ログはService層とControllerAdviceの両方で出すべきですか?

基本的には、同じ例外のスタックトレースを複数箇所で出すのは避けます。Service層で業務的な補足情報を残す場合はありますが、重複したログは障害調査で見づらくなるため、最終的なERRORログはControllerAdviceなどの共通処理に寄せると整理しやすいです。

NullPointerExceptionはcatchして処理するべきですか?

原則として、NullPointerExceptionは業務例外としてcatchするより、実装上の問題として原因を直すべきです。nullが業務上あり得る値なら、Optional、事前チェック、バリデーションなどで明示的に扱います。

まとめ:Javaの例外処理設計は失敗時の責務を決めること

最後に整理すると、Javaの例外処理設計で重要なのは、単にtry-catchを書くことではありません。業務ルール違反、外部要因の失敗、実装上の不具合を分けたうえで、それぞれをどの層で扱うかを決めます。

また、業務例外はユーザーやAPI利用者に返す意味から設計します。RuntimeExceptionを使う場合でも、エラーコード、メッセージ、ログ、レスポンス形式をあらかじめ整理しておくと、チーム開発で扱いやすくなります。

設計ポイント意識すること
例外の分類業務エラー、外部要因、実装不具合を分ける
catchする場所その層が責任を持てる例外だけ処理する
業務例外ユーザーに返す意味とエラーコードを設計する
ログ同じ例外を複数箇所で出さない
Spring BootControllerAdviceでレスポンス変換を共通化する

つまり、Javaの例外処理は「失敗したときにどう動くべきか」を決める設計です。ここを丁寧に考えられるバックエンドエンジニアは、保守しやすい業務アプリを作りやすくなります。

JavaやSpring Bootの実務では、例外処理、API設計、ログ設計、SQL設計のような判断が大切です。こうした細かい判断が開発現場での信頼につながるため、今の経験を活かしながら、より設計寄りの案件やバックエンド開発に関わりたい方は、まず現在地を整理してみるのも一つの方法です。

IaC INP PM PMO PMP UX Webディレクター インフラエンジニア キャリアチェンジ フロントエンドエンジニア