【Effective Java】抽象概念に適した例外をスローする

Effective Javaの独自解釈です。
第3版の項目73について、自分なりにコード書いたりして解釈してみました。

# 概要
- 下位レイヤーの例外はそのまま出力させるのではなく、上位レイヤーの概念に応じた例外に翻訳することで以下のメリットが得られる。
 -  ログ調査しやすくなる
 -  さらに上位のレイヤーでの例外ハンドリングがしやすくなる
- 実装で例外発生を確実に防げるなら、翻訳は考えず例外発生そのものを防ぐ。

# 説明
以下の環境で実装したコードで説明する。

- Java 11
- Doma 2.19.2
- Spring Boot 2.3.3
- MySQL 5.7

## DB挿入時における例外出力の例

DBに`USER`テーブルがあるとし、レコードを新規追加することを考える。
`USER`テーブルには主キーとして`id`カラムを持っている。

Java側の関連クラスは以下の通り。（importなど略）

**Entity**

```java
public class User {

    private String id;

    private String name;
}
```

**Dto**

```java
public class CreateUser {

    private String id;

    private String name;
}

```

**Daoインタフェース**

```java
public interface UserDao {

    @Insert
    int insert(User user);
}
```

### 下位レイヤーの例外をそのまま出力させた場合

既に`USER`テーブルに存在する`id`を持つ`createUser`を引数に入れ、以下のサービス層のメソッドでinsertを試み例外を発生させる。

```java
public User createUser(CreateUser createUser) {

    User user = new User();
    user.setId(createUser.getId());
    user.setName(createUser.getName());

    userDao.insert(user);

    return user;
}
```

このとき、標準エラーには以下が出力される。

```
〜略〜
nested exception is org.springframework.dao.DuplicateKeyException: [DOMA2004] 一意制約違反により更新処理が失敗しました。
SQLファイルパス=[null]。
ログ用SQL=[]。
詳しい原因は次のものです。{2}; nested exception is org.seasar.doma.jdbc.UniqueConstraintException: [DOMA2004] 一意制約違反により更新処理が失敗しました。
SQLファイルパス=[null]。
ログ用SQL=[]。
詳しい原因は次のものです。{2}] with root cause

java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'A01' for key 'PRIMARY'
〜略〜
```

Daoレイヤーから出力された`DuplicateKeyException`がそのまま`createUser`メソッドを突き抜けてスローされている。

弊害としては以下の通り。

- スタックトレースをたどれば`createuser`メソッドで主キー重複例外が出てるから、`USER`のID重複エラーだなと一応わかるが、ひと目ではわかりにくい。
- 上位レイヤーのコントローラ層で、Entityに応じたエラーハンドリング処理をしたい場合、`DuplicateKeyException`が投げられると、どのEntityに対しての例外なのか判別するために、ややこしい文字列解析などする必要が出てくる。

### 上位レイヤーの例外に置き換えた場合
`createUser`メソッドの主な関心事はDBの制約ではなく、`User`そのものである。ということで`User`に着目した独自例外を作ってやり、それに翻訳する処理を施す。

**独自例外**

```java
public class UserDuplicatedIdException extends Exception {

    public UserDuplicatedIdException(String message, Throwable cause) {
        super(message, cause);
    }
}
```

Primary KeyというDB寄りの言葉は使わず、あくまで`User`の`id`という`User`寄りの言葉で例外クラスを作る。
`createUser`メソッドは以下のように、`DuplicateKeyException`から`UserDuplicatedIdException`への翻訳処理を実装する。

```java
public User createUser(CreateUser createUser) throws UserDuplicatedIdException {

    User user = new User();
    user.setId(createUser.getId());
    user.setName(createUser.getName());

    try {
        userDao.insert(user);
    } catch (DuplicateKeyException e) {
        throw new UserDuplicatedIdException("Can't create already existing id user.", e);
    }

    return user;
    }
```

先程の例と同じように`id`重複の`USER`テーブルレコードを挿入しようとすると、以下の標準エラー出力が出力される。

```
〜略〜
nested exception is com.demo.domain.exceptions.UserDuplicatedIdException: Can't create already existing id user.] with root cause

java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'A01' for key 'PRIMARY'
〜略〜
```
`createUser`からは、このレイヤーの関心事である`User`に関する例外`UserDuplicatedIdException`が出力される。

利点は先程とは逆で以下の通り。

- 例外クラスを見ただけでどのEntityで何が起こったかがひと目で分かる。
- コントローラ層では、catchした例外クラスに応じて楽に分岐処理を実装できる。

また、障害やデバッグ時にログ調査をする際も、目的の例外クラスでgrepをかけられるので捗る。

### 例外翻訳は乱用しない

先述の例で、`USER`の`id`を`Null`で登録しようとすると、以下の標準エラーが出力される。

```
〜略〜
nested exception is org.springframework.dao.DataIntegrityViolationException: [DOMA2009] SQLの実行に失敗しました。
SQLファイルパス=[null]。
ログ用SQL=[]。
原因は次のものです。java.sql.SQLIntegrityConstraintViolationException: Column 'id' cannot be null。
根本原因は次のものです。java.sql.SQLIntegrityConstraintViolationException: Column 'id' cannot be null; SQL [insert into USER (id, name) values (?, ?)]; Column 'id' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'id' cannot be null] with root cause

java.sql.SQLIntegrityConstraintViolationException: Column 'id' cannot be null
〜略〜
```

なるほど、この場合は`DataIntegrityViolationException`を別の例外に翻訳すればいいんだなと思うかもしれないが、それはNGである。

insert時のDBの中身はクライアント側で制御できないが、insertしようとするEntityは制御可能である。
コントローラ層でリクエストの`id`指定がない場合はバリデーションで弾くなり、代替値を入れるなりすれば、この例外は100%発生しないので、翻訳処理も必要なくなる。