歴史的背景
構造化プログラミング(C言語)の時代の例外処理
サブルーチン内でエラーが発生した場合はエラーコードを返却する形で実装するのが常套手段でした。
1の時はデッドロック、2の時は通信障害、3の時はシステムエラー
この方法の問題点
エラーコードの判定処理をアプリで確実に行う必要がある点
もし、判定処理を間違えたりすると障害発生時に原因特定が難しくなる。また、エラーコードの値を追加・削除する場合はプログラマが関係するサブルーチンを全て調べて書き換える必要が発生する。
エラーコードを判定する同じようなロジックがサブルーチン間で連鎖する点
呼び出す側のサブルーチンではエラーコードの判定処理を常に書かなくてはいけないので、プログラムが非常に冗長になります。
オブジェクト指向時代の例外処理
特別なエラーを返す可能性があることをメソッドで宣言します。
最上位の呼び出しもとのメソッドだけで、例外処理を下記、それより下位層のメソッドでは例外が発生する可能性があることだけを宣言すればよくなります。
重複したエラー処理を書かなくて良くなり、かつ必要な箇所でエラー処理を書き忘れた場合はコンパイラや実行環境が教えてくれます。
Javaで言えば、throw、throwsってやつですね。ただ、現実としてWebアプリとかを作る場合はフレームワークが例外処理機構を別途用意しているケースが多い(Spring MCVなど)のでそちらに倣う形になります。
例外を検知する側
RuntimeExceptionにラップして返す。
検査例外を非検査例外(RuntimeException)にラップして返します。とりあえずこれだけ覚えておけば最低限無難なコードになります。(e.printStackTrace();などで最低限出力してあるコードよりは)
1 2 3 4 5 6 7 |
try { ファイル処理など // 正常系 ... } catch (IOException e) { throw new RuntimeException(e); } |
なお、より適切な非検査例外にラップして返せるとなお良い。上の例で言えば、「UncheckedIOException」にラップして送出するのが適切です。
- IllegalArgumentException(与えられたパラメータが想定外の場合)
- IllegalStateException(呼び出された側の内部状態が想定外の場合)
アンチパターン
booleanで返す。
1 2 3 4 5 6 7 |
try { 処理 return true; } catch (IOException e) { log.error("xxxx", e); return false; } |
メリット
簡単に実装できる。
デメリット
呼び出し元に原因が通達されない。
自前でロギングしてから再送出する。
1 2 3 4 5 6 |
try { ファイル処理 } catch (IOException e) { log.warn("xxxx " + e, e); // 自前でログを出力するのはNG throw new UncheckedIOException(e); } |
これをすると、自前のログが出力された後、大域の例外ハンドラで再度同じようなログが出力されてしまう。これをすると運用時に二箇所別の原因でエラーが出ているものだと勘違いしてしまう。
非検査例外にラップし忘れる。
1 2 3 4 5 |
try { ファイル処理 } catch (IOException e) { throw new UncheckedIOException("オリジナルメッセージ"); } |
これをすると、元の例外の原因の情報が損なわれてしまう。必ずエラーオブジェクトを非検査例外にラップすること。
try-with-resources構文を使う。(Java7以降)
1 2 3 4 5 |
try (ファイル処理) { ファイル処理 } catch (IOException e) { throw new UncheckedIOException(e); } |
finallyでファイルの後処理を記述すると、その中でもtry-catchをしなければならず冗長な記述になってしまう。Javaの環境が新しめであれば、「try-with-resources」を使った方が良いです。
Optionalで包む
下位層が例外を送出することになることが設計上適切とは言い難い場合に例外を補足してOptionalという戻り値を返す場合があります。
1 2 3 4 5 6 7 8 |
public Optional<型> メソッド() { try { 処理 return Optional.of(型); } catch (Exception e) { return Optional.empty(); } } |
返り値のクラス(xxxResult)を実装する。
「成功した場合の戻り値となる値」と「失敗した場合の情報」を一つのクラスにして返す方法。かなり実装コストは高い。
Either型で返す。
関数型に近い概念。知識がある現場なら使っても良い。
リトライする。
外部API呼び出しとかのような処理で設計する場合もある。ただ、自前で実装することが難しいので外部ライブラリとかを使った方が良いケースが多い。
例外を受信する側
基本的な指針としては個々に記述することなく大域で例外クラスを処理するようにすると良いです。
アプリの最上位層(mainメソッドなど)でtry-catchを書く
シンプルでわかりやすいです。ただ、現実はあまり使われるケースは少ないです。
mainメソッドからスクラッチで開発するケースは少ないですし、Webアプリにしてもフレームワークにもっと良い大体手段があるためです。
Webフレームワークの例外処理機構を利用する。
普通のWebフレームワークなら例外クラスに応じた例外ハンドラを実行する機構を持ってます。Spring MVCなら例外ハンドラクラスや、例外メソッドに対してアノテーションで例外クラスを指定します。
DIコンテナのInterceptor機構を利用する。
DIコンテナの管理下にあるクラスの任意のメソッド呼び出しに対して前後処理を組み込めます。
アンチパターン
Exception Manager
分散型が主流の昨今だと実装は厳しい。リトライやロールバック実装が難しかったり、個別ケースごとに似たようなManagerがたくさんできる。
Exception Handler
これもAPIでいろんなサービスと交信し合うタイプのアーキテクチャだと厳しい。
リトライさせるか
リトライのユースケース
バッチ処理などでどうしても失敗できない処理の場合
バッチ処理はオンライン処理に比べたら同時処理数は一定のため過負荷などにはなりにくいです。
リトライの注意点
回数の多いリトライは過負荷を生む原因になるのでやったとしても数回程度にしましょう。
この記事へのコメントはありません。