🐧

Javaのチェック例外を、境界と回復可能性で設計する

に公開

結論

チェック例外を使うかどうかは、「呼び出し側へ回復処理を要求する契約か」で決めます。公開メソッドで一律に禁止する、あるいは下位層の例外をそのまま公開する、という決め方はしません。

public Report loadReport(Path path) throws ReportFormatException {
    // 呼び出し側が別ファイルの選択や入力修正を行える
}

層をまたぐときは、その層の言葉へ例外を翻訳し、元の原因を保持します。

困る場面

Java では RuntimeExceptionError の派生クラスが非チェック例外で、それ以外の Throwable 派生クラスがチェック例外です。これは言語仕様上の分類です。

API 設計で迷うのは、失敗を型として強制的に処理させる価値があるかどうかです。たとえば、ファイル未検出は利用者に別ファイルを選ばせられる一方、プログラム内部の不変条件違反は通常その場で回復できません。

仕組み

チェック例外は、throws 宣言と catch-or-declare 規則によって呼び出し側へ伝播します。

catch する場所は、例外の種類ではなく、具体的な対応を取れる場所です。

選択の理由

条件 選択の候補
呼び出し側が通常の分岐として回復できる チェック例外、結果型
プログラムの契約違反 非チェック例外
値がないことが通常の結果 Optional
失敗理由を列挙し、合成したい 専用の結果型

チェック例外にすると呼び出し側の対応は可視化されますが、回復方法がないのに形式的な catch を増やすこともあります。効果と負担を API ごとに見ます。

たとえば、利用者が別のファイルを選び直せる ReportFormatException や、入力を修正して再実行できる ImportException には、チェック例外として対応を要求する余地があります。一方、内部状態が設計上あり得ない値になった場合や、呼び出し側に回復手段がない場合は、IllegalStateException などの非チェック例外で失敗を伝える方が自然です。どちらも名前だけで決めず、呼び出し側が実行可能な対応を持つかで判断します。

実装例

インフラ層の例外をアプリケーション層の意味へ変換する例です。

public Contract loadContract(ContractId id) {
    try {
        return repository.findById(id)
            .orElseThrow(() -> new ContractNotFoundException(id));
    } catch (DataAccessException e) {
        throw new ContractLoadException(id, e);
    }
}

複数件を処理し、失敗を最後にまとめる必要があるなら、原因を捨てません。

var failures = new ArrayList<Exception>();
for (Path path : paths) {
    try {
        importFile(path);
    } catch (ImportException e) {
        failures.add(e);
    }
}
if (!failures.isEmpty()) {
    var summary = new BatchImportException(failures.size());
    failures.forEach(summary::addSuppressed);
    throw summary;
}

運用とレビュー

  • catch した場所で、再試行、代替、通知、変換のどれを行うか明示する
  • throw new RuntimeException(e) だけを機械的に増やさない
  • ログ出力と再送出を両方行い、同じ失敗を重複記録しない
  • Spring MVC などの境界では、例外を HTTP 応答へ変換する責任を集約する

判断を変える条件

公開ライブラリへチェック例外を追加すると、利用者のソース互換性へ影響します。内部アプリケーションより慎重に設計します。逆に閉じたバッチ処理なら、結果を集約する専用型で運用しやすくなる場合があります。

確認方法

  • 呼び出し側に実行可能な回復手段があるか
  • 例外名が下位ライブラリではなく業務上の失敗を表すか
  • 原因例外が cause または suppressed として残るか
  • 正常系、失敗系、再試行時をテストできるか

チェック例外と非チェック例外の分類は、参照する Java SE 版の Java 言語仕様(JLS)11 章で確認できます。Java SE仕様一覧

まとめ

チェック例外は善悪ではなく、呼び出し側へ回復を要求する契約です。回復できる境界で捕捉し、層をまたぐときは意味を翻訳し、原因を失わないようにします。

Discussion