Javaの例外クラス名の付け方を解説する記事のアイキャッチ。Java道場風の学習空間で若手エンジニアが業務例外とシステム例外の命名を考えるイラスト。

Java

Javaの例外クラス名はどう付ける?業務例外とシステム例外の命名ルール

AIによる要約

例外クラス名は、どの処理で落ちたかではなく、何が成立しなかったかを表すと使いやすくなります。業務ルール違反にはInsufficientStockExceptionのような具体名を、外部APIやDBなど技術的な失敗にはExternalApiTimeoutExceptionのような原因が分かる名前を付けます。BusinessExceptionSystemExceptionだけで済ませず、呼び出し側が判断できる粒度を残すことが重要です。

新人SE
新人SE
例外クラスを作ることになりました。OrderExceptionやBusinessExceptionではダメですか?
ポンコツSE
ポンコツSE
コンパイルは通りますが、名前だけでは何が起きたか分かりません。ログ、レビュー、呼び出し側の分岐まで考えて命名しましょう。

業務システムでは、在庫不足、二重申請、締め日超過、外部APIのタイムアウトなど、多くの失敗を扱います。新人のうちは「とりあえずRuntimeExceptionを継承したクラスを作る」ところまでで終わりがちですが、曖昧な名前は後から調査コストを増やします。

この記事では、SIer・SESのJava案件で使いやすい例外名の決め方を、注文処理の実例で整理します。難しい例外設計ではなく、まずレビューで説明できる基準を持つことが目的です。

この記事のポイント

  • 例外名は「処理名」より「成立しなかった条件」を表す
  • 業務例外と技術的な例外を同じクラスへ詰め込まない
  • メッセージには調査に必要な識別子を残し、機密情報は入れない
  • 細かすぎる継承階層より、呼び出し側が判断できる粒度を優先する

例外クラス名で伝えるべきこと

良い例外名は、catchする側やログを読む人へ「何が失敗したか」を伝えます。次の名前を見比べてください。

// 何が起きたか分かりにくい
throw new OrderException();
throw new BusinessException();
throw new SystemException();

// 成立しなかった条件が分かる
throw new InsufficientStockException(productId, requestedQuantity);
throw new OrderAlreadyCanceledException(orderId);
throw new ExternalApiTimeoutException("payment-api", timeoutMillis);

OrderExceptionは注文に関する失敗だとしか分かりません。在庫不足なのか、注文が見つからないのか、状態遷移が不正なのかを、結局メッセージやスタックトレースから探すことになります。

「どのServiceで発生したか」ではなく、「何ができなかったか」を例外名にします。

業務例外とシステム例外を分けて考える

現場では例外を大きく、業務ルール上の失敗と、技術的な失敗に分けると整理しやすくなります。厳密な定義はプロジェクトごとに違いますが、まず次の感覚を持っておけば十分です。

種類画面・APIでの扱い
業務例外在庫不足、申請済み、取消期限超過利用者が理解できるメッセージへ変換しやすい
システム例外DB接続失敗、外部APIタイムアウト、設定不足共通エラーにし、運用ログへ詳細を残すことが多い

業務例外の命名例

  • InsufficientStockException: 在庫が必要数に足りない
  • OrderAlreadyCanceledException: 取消済み注文を再度取り消した
  • ApprovalDeadlineExceededException: 承認期限を過ぎている
  • DuplicateApplicationException: 同じ申請がすでに存在する

業務例外は、画面で利用者へ返すメッセージと近いことがあります。ただし、例外名を日本語メッセージの代わりにせず、表示文言はメッセージコードなどで別管理する案件も多くあります。

システム例外の命名例

  • ExternalApiTimeoutException: 外部APIが規定時間内に応答しない
  • FileImportException: ファイル取込処理が完了できない
  • ConfigurationNotFoundException: 必須設定が見つからない
  • DataAccessException: Springが提供するDBアクセス例外の共通体系

ライブラリやフレームワークが適切な例外をすでに提供しているなら、自作例外で包む必要があるかを先に確認します。すべてをSystemExceptionへ変換すると、元の例外型から得られた情報を失うことがあります。

例外名を決める4つの手順

1. 失敗した業務条件を文章にする

最初から英語名を考えず、「要求数に対して在庫が足りない」「取消済み注文をもう一度取り消した」のように日本語で状況を書きます。文章が曖昧なら、例外名も曖昧になります。

2. 名詞または状態をExceptionの前へ置く

NotFoundAlreadyCanceledInsufficientStockDeadlineExceededなど、失敗の状態を短く表します。プロジェクト内ですでに使われている語彙があれば揃えます。

3. 呼び出し側が別対応するか確認する

在庫不足と注文不存在で、返すHTTPステータスや画面メッセージが違うなら、別クラスにする意味があります。どちらも同じ共通エラーにしかならないのに、似た例外を20個作る必要はありません。

4. 既存コードとの一貫性を確認する

NotFoundExceptionNotExistExceptionが混在すると検索しにくくなります。新しい命名規則を独断で増やさず、近い機能の例外、コーディング規約、ControllerAdviceを確認します。

注文処理の実装例

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final StockRepository stockRepository;

    public void cancel(long orderId) {
        Order order = orderRepository.findById(orderId)
                .orElseThrow(() -> new OrderNotFoundException(orderId));

        if (order.isCanceled()) {
            throw new OrderAlreadyCanceledException(orderId);
        }

        order.cancel();
    }
}

注文不存在と取消済みでは、発生条件も利用者へ伝える内容も違います。例外型を分けると、呼び出し側で個別に変換できます。

public class OrderNotFoundException extends RuntimeException {
    private final long orderId;

    public OrderNotFoundException(long orderId) {
        super("Order not found. orderId=" + orderId);
        this.orderId = orderId;
    }

    public long getOrderId() {
        return orderId;
    }
}

IDをフィールドとして持たせれば、文字列を解析せずにエラーレスポンスやログへ利用できます。ただし、必要な情報まで何でも例外へ詰め込むのではなく、呼び出し側が使う値へ絞ります。

メッセージには調査用の情報を残す

Failedだけのメッセージでは、運用担当者が再現条件を探せません。注文ID、商品ID、外部システム名など、事象を絞れる情報を含めます。

throw new InsufficientStockException(
        "Insufficient stock. productId=" + productId
                + ", requested=" + requestedQuantity
                + ", available=" + availableQuantity);

パスワード、アクセストークン、クレジットカード番号、個人情報を例外メッセージへ出してはいけません。ログへ出力される前提で、識別に必要な最小限の情報にします。

checked例外とRuntimeExceptionは名前だけで決めない

Exceptionを直接継承するchecked例外は、呼び出し側へcatchまたはthrowsを強制します。RuntimeExceptionはその強制がありません。業務例外だから必ずchecked、システム例外だから必ずRuntimeExceptionという決まりではありません。

Springの業務アプリケーションではRuntimeException系へ統一する設計もよくあります。トランザクションのロールバック方針にも関係するため、新人が単独で決めず、既存プロジェクトの規約と共通例外処理を確認してください。

継承階層は浅く保つ

共通処理のためにBusinessExceptionを親にすること自体は問題ではありません。ただし、ApplicationExceptionBusinessExceptionOrderExceptionOrderStateExceptionと階層を深くすると、どの型でcatchすべきか分かりにくくなります。

public abstract class BusinessException extends RuntimeException {
    private final String messageCode;

    protected BusinessException(String messageCode, String message) {
        super(message);
        this.messageCode = messageCode;
    }

    public String getMessageCode() {
        return messageCode;
    }
}

public class InsufficientStockException extends BusinessException {
    public InsufficientStockException(long productId) {
        super("order.stock.insufficient",
                "Insufficient stock. productId=" + productId);
    }
}

共通のメッセージコードやログ方針が必要なら、親クラスを1段設ける程度から始めます。継承は分類のために使い、名前の曖昧さを隠すために使わないことが大切です。

ControllerAdviceで利用者向けの応答へ変換する

@RestControllerAdvice
public class ApiExceptionHandler {

    @ExceptionHandler(OrderNotFoundException.class)
    public ResponseEntity<ApiError> handleOrderNotFound(
            OrderNotFoundException ex) {
        ApiError error = new ApiError("ORDER_NOT_FOUND", "注文が見つかりません");
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(InsufficientStockException.class)
    public ResponseEntity<ApiError> handleInsufficientStock(
            InsufficientStockException ex) {
        ApiError error = new ApiError("INSUFFICIENT_STOCK", "在庫が不足しています");
        return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
    }
}

例外メッセージをそのまま画面へ返すと、内部情報が漏れる可能性があります。例外は調査情報を保持し、ControllerAdviceでは利用者向けの安全な文言へ変換します。

現場レビューでよくある指摘

// レビューコメント例
OrderExceptionだけでは発生条件が分かりません。
在庫不足と注文不存在で呼び出し側の対応が違うため、例外型を分けてください。

// レビューコメント例
元例外を捨てているため、原因追跡が難しくなっています。
ラップする場合はcauseをコンストラクタへ渡してください。

// レビューコメント例
例外メッセージにメールアドレスが含まれています。
ログへ出るため、顧客IDなど必要最小限の識別子へ変更してください。

例外をラップする場合は、原因となった例外を捨てないことも重要です。

try {
    paymentClient.authorize(request);
} catch (SocketTimeoutException ex) {
    throw new ExternalApiTimeoutException(
            "Payment API timed out. orderId=" + orderId, ex);
}

提出前のセルフチェック

  • 例外名だけで、成立しなかった条件を説明できるか
  • 業務ルール違反と技術的な失敗を混ぜていないか
  • 呼び出し側で別対応しない例外を細分化しすぎていないか
  • 元例外をcauseとして保持しているか
  • メッセージに調査用IDがあり、機密情報がないか
  • 既存の例外体系と単語が揃っているか

読みやすい例外処理を学ぶ参考書

例外名だけでなく、意図が伝わる名前や短い処理単位を身につけると、エラー処理全体も読みやすくなります。次の書籍は、レビューで説明できるコードを書く基礎固めに向いています。

書籍「リーダブルコード」の表紙
書影:O'Reilly Japanより引用
PR 新人・若手エンジニア向け

リーダブルコード

著 / 角 征典 訳

コードレビューの「読みづらい」を減らす、最初の一冊。

命名、コメント、条件分岐、変数、メソッド分割を短い実例から学べます。Java専用書ではありませんが、現場で使える読みやすさの判断基準が身につきます。

  • レビュー指摘の理由と直し方を整理したい
  • 動くだけでなく、読みやすいコードを書きたい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

まとめ

Javaの例外名は、処理を実行した場所ではなく、成立しなかった条件を表すと読みやすくなります。OrderExceptionのような大きな名前へ集約せず、在庫不足、状態不正、外部APIのタイムアウトなど、呼び出し側が判断できる粒度にします。

一方で、例外クラスを細かく増やすこと自体が目的ではありません。利用者への応答、ログ、リトライ、トランザクションなど、後続処理が変わるかを基準にし、プロジェクトの既存ルールへ揃えましょう。

-Java
-, , ,