2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

例外処理の責務を整理するとアーキテクチャが綺麗になる話

Last updated at Posted at 2025-12-05

💡 はじめに

例外処理は責務分離保守性に直結する重要な設計ポイントです。
場当たり的に try-catch を書き散らすと、ログが多重に出たり、異常が握りつぶされたり、レイヤ間が密結合になってしまいます。

本記事では、Javaの例外処理を 「どこでcatchするべきか」 に焦点を当て、レイヤごとの責務を整理しながら アーキテクチャが綺麗になる例外設計を解説します!

⚠️ Javaの例外処理の問題は「catchする場所」にある

例外処理でよくある問題は、例外が「発生した場所」と「扱うべき場所」が一致しないことです。

例えば、DBアクセスで SQLException が発生した場合

SQLException
try {
    // DB処理
} catch (SQLException e) {
    e.printStackTrace();
    return null;
}

一見キャッチして対処したように見えますが、

  • 呼び出し元から見ると「成功なのか失敗なのか」判断不能
  • nullハンドリングがアプリ全体に波及
  • DB例外がInfrastructure層に閉じず、上位層の判定ロジックが複雑化

こういった形でシステムの保守性が徐々に崩れていきます。
例外は "意味があるレイヤ" で扱わないと、設計全体に悪影響を与えるのです。

🧭 理想的な例外伝播と責務の分離

では、どこでcatchし、どこではcatchしないのか?
レイヤアーキテクチャを例に考えます:

役割 例外の扱い方 catchすべき?
Presentation (Controller) UI/API境界 最終的に例外をレスポンスに変換 ◎ 最後でまとめてcatch
Application (Service/UseCase) ユースケース調整 必要なら業務例外へ変換 △ 必要時のみ
Domain 業務ルール 技術詳細は知らない △ 原則Throw/Domain例外
Infrastructure DB/外部API 技術例外が発生する場所 × 原則catchしない(翻訳のみ)

例外は、意味のある層でだけ扱う
技術的例外はInfrastructureで翻訳し、UI層でまとめて処理

これにより、

  • プレゼンテーション層は業務例外だけを知ればよい
  • ドメインは技術詳細から独立できる
  • 例外が一貫した抽象で伝播し可読性が向上

設計が綺麗に保たれやすくなります。

🔄 例外翻訳(Exception Translation)の考え方

下位層の例外を、上位が理解できる例外へ変換するテクニックが 例外翻訳 です。
Effective Java でも推奨されているアプローチで、Springの DataAccessExceptionなどが良い例です。

public User findById(UserId id) {
    try {
        return jdbcTemplate.queryForObject(...);
    } catch (SQLException e) {
        // DBの詳細は上位に漏らさない
        throw new RepositoryException("ユーザ取得に失敗", e);
    }
}

もしこれを翻訳せずに SQLException を直接投げると、ControllerやServiceがDB例外を知ってしまいます。
こうなると「NoSQLに移行した」「外部APIに変更した」という変更が上位層に波及し、柔軟性を失う原因になります。

翻訳により、

  • 上位層は抽象例外のみ扱えばよい
  • 実装変更がレイヤを越えて伝染しない
  • フレームワーク依存からドメインを守れる

という大きなメリットが得られます。

🧪 実装例

ここでは、例外がどの層でどのように扱われるかを、短いコードで追ってみます。

  • Infrastructure層で SQLException が発生
  • RepositoryException に翻訳して上位に投げる(ここで 例外連鎖 が発生)
  • Application層で必要に応じて BusinessException に変換
  • 最後はController層でHTTPレスポンスへ変換

ポイントは 例外翻訳 + 例外連鎖 を意識するとアーキテクチャが綺麗に保てることです。

Infrastructure
class UserRepositoryImpl implements UserRepository {

    @Override
    public User findById(long id) {
        try {
            return jdbcTemplate.queryForObject(...);
        } catch (SQLException e) {
            // 元例外を cause として保持したままラップ → 例外連鎖
            throw new RepositoryException("ユーザ取得に失敗", e);
        }
    }
}

例外連鎖とは、元の例外を内部に保持したまま別の例外としてスローすることです。
これにより 上位層はDB例外を意識せずに済むのに、原因調査は容易になります。

Application
class UserService {

    public UserDto getUser(long id) {
        try {
            return UserDto.from(userRepository.findById(id));
        } catch (RepositoryException e) {
            // ここでも cause を渡せば連鎖は維持される
            throw new BusinessException("ユーザ取得に失敗しました", e);
        }
    }
}

Presentation層で最終的にレスポンスへ変換する

Presentation
@RestControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    ResponseEntity<?> handleBusiness(BusinessException e) {
        return ResponseEntity.badRequest().body(e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    ResponseEntity<?> handleGeneral(Exception e) {
        return ResponseEntity.internalServerError()
            .body("サーバ内部でエラーが発生しました");
    }
}

🏁 まとめ

例外処理は、「どこでcatchするか」を意識するだけで設計の見通しと保守性が大きく変わります。

技術例外は Infrastructureで翻訳し(Exception Translation)、元例外は causeとして保持(例外連鎖)、最終的なハンドリングは Presentation層で対応
この流れを守ることで、レイヤ責務が明確になり、アーキテクチャが綺麗に保てます

例外は発生場所ではなく、意味のある場所で扱う、これがシンプルで強力な指針です。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?