16
12

More than 3 years have passed since last update.

<Java>エラーハンドリング設計の鉄板4箇条と+α

Last updated at Posted at 2021-02-10

はじめに

自分自身、結構適当と言うか、
エラーハンドリング(例外処理)とログ出力(メッセージ)の設計を、
なんとな〜くやってたので、しっかりと調べてみやした。

本当だったら、
どこでどんなエラーが起こって、どうやったら解決できるか
っていう、
エンジニアにヒントと希望を与えてくれるものなのに、

タスクが盛り盛りで、作ることだけに集中してしまうあまりに、
運用と管理保守のことを軽視してしまっておりました。

なんとも、Viciousスパイラル(悪循環)で、
後々自分が苦しまないように、しっかりと準備したいと思います。

(※もろもろ間違っておりましたらごめんなさい。勉強し直します。)

1. エラー分類

大まかに分けるとエラーの種類は2つある。

正常系エラー

例えば、トランザクション開始前のバリデーションチェックなど、
想定内として、入力エラー等を正常処理としてエラーハンドリングするケース

正常系エラーは、例外は使わずに、ステータスコードを使うべし

なぜなら、例外処理は対応コストがかかるため、例外をthrowしない。
また、入力エラーは複数の発生原因が考えられるから、「例外は1つ返す」の原則から離れてしまう
加えて、UIが関連してくるので、ステータスコードを使った方がいい。(例外としては扱わない)
例:「ステータスは、0:未完了/1:完了で入力して下さい。」など

異常系エラー(例外)

例えば、他の機能とシステム連携中に、フォーマットチェックにひっかかったなど、
想定外に、トランザクションの続行が不可能等、そういった異常系の例外が発生したケース
開発者が任意で例外を生成しthrowするケースもあります。

以下、異常系エラーハンドリングについて触れて参る。

2. 例外クラスを定義する

ユーザー定義例外を使うことで、例外処理を統一した形式で実行できるのでオススメ

[eclipse](https://www.eclipse.org/downloads/)とか、
いわゆる統合開発環境を使うと、
継承元の親クラス(super)の初期化や、定義したクラスのコンストラクタ生成なんかを自動でやってくれるので便利。

今回は、非チェック例外であるRuntimeExceptionを継承したい。
普通RuntimeException系の例外は
「これ以上処理できないような致命的な異常が発生した」という状態なので、
JVMや、フレームワーク等に処理方法を任せるのがセオリーな姿。

ちなみに、Exceptionを継承すると、チェック例外となり、必ずtry~catch~構文が必要になり、必ずcatch節で処理しなければいけない模様。


public class MyRuntimeException extends RuntimeException {
    private static final long serialVersionUID = 1L;//シリアライズ可能なクラスとしてのマーク
    protected String msgCode;
    protected int level;

    public MyRuntimeException() {
        super();
    }
    public MyRuntimeException(String message, Throwable cause, boolean enableSuppression,
            boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
    public MyRuntimeException(String message, Throwable cause) {
        super(message, cause);
    }
    public MyRuntimeException(String message) {
        super(message);
    }
    public MyRuntimeException(Throwable cause) {
        super(cause);
    }
    //これを throw newで使いたい
    public MyRuntimeException(String message, String msgCode, int level) {
        super(message);
        this.msgCode = msgCode;
        this.level = level;
    }
    public String getMsgCode() {
        return msgCode;
    }
    public void setMsgCode(String msgCode) {
        this.msgCode = msgCode;
    }
    public int getLevel() {
        return level;
    }
    public void setLevel(int level) {
        this.level = level;
    }    
}

定義のポイント

  • Java APIで定義されている例外、つまり、デフォルトで提供されている標準機能としては、「メッセージ」「スタックトレース」の属性しか持たない
  • 運用監視には「メッセージコード」「ログレベル」も必要なので、ユーザー定義クラスを新規作成。
    • 属性を追加したい場合は、ユーザー定義例外のサブクラスを作るとよき◎
  • RuntimeExceptionを継承したクラスは、メソッドのthrowsを記述する必要がなくなり、ソースがスッキリする
public void someMethod() throws RuntimeException {
}
//↓つまりこんな感じで省略可能ってことか
public void someMethod() {
}

3. 例外処理を基底クラス(親)にまとめる

例外処理の構文try~catchは、処理の実行を呼び出す「根っこ」の部分のみに実装するとよいとのことなので、
早速以下、基底クラスのイメージです。

//基底クラスのざっくりイメージ
public class SuperBusinessService {

  public static void someCommonMethod() {
    try {
      System.out.println("処理を始めますよ。");

      //例外がスローされる可能性がある汎用処理
      //e.g. DB接続や、共通として必要データのロードなどの汎用処理など。
      if(A == "error"){
        generateMyException1();
      } else {
        generateMyException2();
      }
    } catch(MyRuntimeException e) {
      //例外1をキャッチしたときの処理;
      System.out.println(e.getMsgCode());//独自エラーコードを表示
      System.out.println(e.getLevel()); //独自エラーメッセージを表示
      e.printStackTrace();//スタックトレース(処理の呼出履歴のことで、エラー発生までに、どのメソッドをどこで呼び出したかを表示)

    // ※catch節は、必要に応じて、例外の数だけ指定可能
    // } catch(例外2の型 変数) {
    //  //例外2をキャッチしたときの処理;
    }
    finally {
      //必ず実行される処理;
      System.out.println("処理が終わりましたよ。");
    }
  }

  public static void generateMyException1() {
    throw new MyRuntimeException("エラーを強制的に発生させました。", "E0001", 1);//e.g. 1: error (logLevel)
  }
  public static void generateMyException2() {
    throw new MyRuntimeException("エラーを強制的に発生させました。", "E0002", 2);//e.g. 2: warn (logLevel)
  }

}

派生クラス(サブ、子)のイメージ。
基底クラスを継承し、汎用処理などのメソッドがそのまま使えるようになり、
子クラスで、try~catchthrowなどしなくても良くなった。
こうやって処理をまとめていけるってことか。

//派生クラスのざっくりイメージ
public class SubBusinessService extends SuperBusinessService {
  //内容
}

定義のポイント

  • 業務ロジックや汎用処理で発生する例外を、まとめて基底クラスで処理するように設計。
  • まとめることで、各業務ロジック内で、個別に例外処理を実装する必要はなくなる
  • 例外処理が基底クラス(親)にまとまることで、ロギング(ログ出力)全集中できる
  • catch句には業務処理を書くべきではない。あくまで、例外の発生と伝えてくれる熱いメッセージ。
  • 例外発生したときの対応コストは想像以上に高いので、正常処理系を例外に組み込むべきではない
  • 例外は一つの発生原因を返すように、簡素化すべき。

4. ログ出力のポイント

上記の例でも少し、取り入れたように、
ロギングも基底クラスにまとまるので、
業務ロジック内で開始メッセージなどをロギングする必要がない。

メッセージの種類と、ログ出力の箇所をまとめた表。

種類  箇所
業務ロジックの開始メッセージ try句の先頭
業務ロジック内で発生した例外のメッセージ catch句
業務ロジックの終了メッセージ finally句

実際のコードに置き換えると、いかのように。
※underlyingとは「基礎となる」「元となる」「根本的な」みたいな意味です。

public class UnderlyingSuperClass {
  try {
    //成功処理
    System.out.println("ロジックの開始");
  } catch (Exception e) {
    //例外処理
    e.printStackTrace();
  } finally {
    //いずれの場合でも最後にする処理
    System.out.println("ロジックの終了");
  }
}

ログに出力すべき内容

マルチスレッド・複数ユーザーという特徴をもつWebアプリケーションでは、
スレッド番号とユーザーIDは必ず出力するのがポイント。

これがないと、ユーザーごとの処理をログで追うことができないから。

以下、ロギング項目をまとめました。
基本的に、1つのログファイルに出力していきます。
※スタックトレースは標準エラー出力で対応

項目 説明
時刻 「yyyy/MM/dd hh:MM:ss.SSS」形式で、西暦からミリ秒まで出力
※実行時間を知りたい場合、ミリ秒は必須。
スレッド番号 スレッド固有の番号。マルチスレッドアプリケーションで必須
ユーザーID システム利用者番号。複数ユーザーがいるアプリケーションで必須
ログレベル 実装・運用フェーズでログの出力量を制限するために設定するもの。
Log4jの場合:fatal, error, warn, info, debug, traceを指定
メッセージコード ログ一行毎の分類コード、もしくはエラーコード。
※例:E0001など
任意メッセージ エラーメッセージなど、日本語の説明。
※エンジニアに希望を与える
スタックトレース 例外の発生箇所までの呼出階層。

Exceptionクラスの種類一覧

Javaでよく使われるExceptionクラスの一覧です。

例外クラス 説明
java.lang.IllegalArgumentException 不適切な引数 引数の指定エラー
java.lang.IllegalStateException 不正な状態 引数の指定エラー
java.lang.NullPointerException Nullアクセス 値がNullのケース
java.lang.IndexOutOfBoundsException 範囲外 配列のindex番号が無い
java.lang.ArithmeticException 不正な算術計算 ゼロでの割り算
java.lang.NumberFormatException 不正な数値型への変換 変換元の文字列が数値じゃない
java.io.FileNotFoundException ファイルが開けない ファイルが存在しない
java.io.IOException 不正な入出力 readLine()を呼び出す前に標準入力(System.in)を閉じてしまうとか

HTTPエラーコード種類一覧

500番台(サーバーエラー)

原因:サーバーがリクエスト処理で失敗したときに返すコード
エラーハンドリングとも関連が出てくるので覚えておくとよき。

番号 項目 説明
500 Internal Server Error サーバ内部でエラーが発生した場合に返す。
サーバサイドのプログラムの文法エラーや、誤った設定があった場合などに返す。
501 Not Implemented WebDAVが実装されてないサーバに対して、固有のメソッド(例:MOVEやCOPY)を使用したときなどに返す。
502 Bad Gateway ゲートウェイ・プロキシサーバで不正な要求を受け取って、拒否した場合に返す。
503 Service Unavailable 過負荷やメンテナンス中で、サービスが一時的に使用不可であるときに返す。アクセス集中でサーバ処理が追いつかない場合など。
504 Gateway Timeout ゲートウェイ・プロキシサーバが制限時間内に処理できなかった場合に返す
505 HTTP Version Not Supported リクエストがサポートされてないHTTPバージョンであるときに返す。
506 Variant Also Negotiates Transparent Content Negotiation in HTTPで定義されている拡張ステータスコード。ネゴシエートを試みてエラーになった場合に返す。
507 Insufficient Storage WebDAVの拡張ステータスコード。リクエスト処理のために必要なストレージ容量が不足したときに返す。
508 Loop Detected サーバ内でリダイレクトループに入ったときに返す。
※開発中にはたまに見かける。
509 Bandwidth Limit Exceeded サーバの帯域幅(転送量)を使い切ったときに返す。
510 Not Extended An HTTP Extension Frameworkで定義されている拡張ステータスコード。拡張不可のときに返す。

400番台(クライアントエラー)

ちなみに、HTTPのエラーコードなんかもここに載せておきます。利便性と自己啓発のため。
サーバーサイドの例外処理なので、関連しないものもありますが。。

原因:クライアントからのリクエストが成功しなかったときに返すコード

番号 項目 説明
400 Bad Request 存在しないメソッドを使用したり、クライアントのリクエスト異常のときに返す。
401 Unauthorized Basic認証やDigest認証などで使われるやつ
402 Payment Required 支払いが必要であるとき使われるやつ。
※将来のために予約されているやーつ。現在は未実装らしい。
403 Forbidden リソースへのアクセス拒否。
例:サーバ構築時に権限設定をミスったときとか
404 Not Found リソースが見つからなかったときのやつ
405 Method Not Allowed 無許可のメソッドを使用しようとした時のやつ。
特に、POSTメソッドの使用が許されていないURIにPOSTでアクセスしたケースなど
406 Not Acceptable Accept関連のヘッダに受理できない内容が含まれている場合に返されるやつ。Accept-Language、Accept-Charsetなどでファイル形式で、ブラウザが言語許可していないときのやつ。
407 Proxy Authentication Required プロキシ認証が必要な場合に返されるやつ。
408 Request Timeout リクエストが時間内に完了しない場合に返されるやつ
409 Conflict リクエストが、いま現在のリソースと矛盾していて完了できないケース
410 Gone 「リソースは恒久的に移動・消滅した。どこに行ったかもわからない。」みたいな特殊ケース。
404 Not Foundと似ているけど、Goneは二度と復活しないケースに使われるらしい
411 Length Required Content-Lengthヘッダがなくて、サーバがアクセス拒否したときのやつ
412 Precondition Failed 前提条件が誤りだった場合に返されるやつ。
413 Request Entity Too Large リクエストエンティティがサーバの許容範囲を超えてしまっていた場合に返すやつ。
例:サーバ制限のサイズより大きいファイルをアップロードなど。
414 Request-URI Too Long URIが長過ぎて、サーバが処理を拒否ったときのやつ。
例:動画データなどをGETで送ったようなケース。
415 Unsupported Media Type サーバで指定メディアタイプがサポートされてないときのやつ。
416 Requested Range Not Satisfiable 実際のリソースサイズを超えるデータを要求したときのやつ。
例:リソースサイズが1024Byteしかないのに、1025Byteを取得しようとした
417 Expectation Failed サーバが拡張ステータスコードを扱えない場合に返す。
418 I’m a teapot HTCPCP/1.0の拡張ステータスコード。ティーポットにコーヒーを淹れるリクエストが来て、拒否された場合に返す。
422 Unprocessable Entity WebDAVの拡張ステータスコード。
423 Locked WebDAVの拡張ステータスコード。リクエストしたリソースがロックされている場合に返すやつ。
424 Failed Dependency WebDAVの拡張ステータスコード。依存関係でエラーの場合に返すやつ
426 Upgrade Required Upgrading to TLS Within HTTP/1.1の拡張ステータスコード。アップグレード必須なときに返すやつ

とまあ、知らんものも多いし、どれかは今後一切使わないだろうな〜

その他HTTPステータスコード

ここで、詳しくは触れませんが、ざーーーーっくりと、書きますw

100番台: Informational
200番台: Success
300番台: Redirection

ほお。なるへそ。

まとめるの疲れたお

まとめ

例外処理とロギングの設計は、
美しさと言うよりかは、
現実的な設計、つまり汎用的かつシンプルなできるだけ手間のかからない設計がやっぱり大事。
ここでどんな例外が起きたのか、シンプルに原因だけをわかりやすいメッセージとコードで伝える。

システムが「ああ」と言ったらエンジニアが「こう」と叫べるように、

阿吽の呼吸が大事ですね。

一番やっちゃいけないのは、

  • ロジックの中にtry ~ catchを散りばめる
  • 例外処理が統一されていない、各々のやり方でやってしまっている。
  • ログに例外を出力してない。なんの例外なんだ・・・状態
  • 誤ったフォーマットで出力する
  • 一度の例外で複数回スタックトレースを出力する。グチャッててわけわかめ。
  • すべてのクラスに開始・終了メッセージのログ出力がなされてしまい、大量に出力されたログに翻弄される。かつ、パフォーマンスが非常に悪い。
  • ログにユーザーIDがないことで、誰の操作でエラーが発生したのかわからない。犯人探しができない。

一番っていってるのに、いっぱい書いちゃいましたねw

例外処理とログ出力の出来次第で、
運用開始後の障害の収束スピードと安定性が変わっていく。。

つまり、サービスの信用と信頼に繋がっていく。

以上、
ありがとうございました!!

16
12
2

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
16
12