たなかこういちの開発ノート

システム開発に携わる筆者が、あれこれアウトプットするブログ

エラーケースのパターン分類〔実装編〕

(※〔概念編〕からの続きです。)
 
各Condition=終了ケースをどのよう実装できるか、具体的な(主にJavaによる)実装パターンをまとめました。
 
Condition 終了ケース Log4j
Log Level
Java例外
実装例
トランザクション HTTP Status Code
(Web API)

GREEN 成功終了/肯定的正常終了 INFO 例外スロー無し commitする 200
YELLOW 失敗終了/否定的正常終了
〔機能要件〕
WARN チェック例外
ApplicationException
(extends Exception)

roll backする
200
ORANGE 失敗終了/否定的正常終了
〔非機能要件〕
WARN 非チェック例外
OperationalException
(extends RuntimeException)

roll backする 200
RED 異常終了
〔外的要因〕
ERROR 非チェック例外
InfrastructureException
(extends RuntimeException)

roll backする 4XX/5XX
BLACK 異常終了
〔内的要因〕
FATAL 非チェック例外
RuntimeException
roll backする 4XX/5XX
 
Log4j Log Level
 
Log4jでは、TRACE, DEBUG, INFO, WARN, ERROR, FATALのログレベルが規定されています。各Condition/終了ケースに上記表のように各ログレベルを割り振りました。各Conditionで終了したとき、対応するレベルでログ出力をするように統一ルールを設けておくことはログの品質向上に寄与するでしょう。
 
Java例外
 
〔概念編〕の最後で述べた「処理を呼び出した側における、各Condition=終了ケースのハンドリング指針」の内容に基づいて、各Condition=終了ケースを非チェック例外のスローで表明するか、チェック例外のスローで表明するかの割り振りをしています。また、各終了ケースを表す固有の派生例外クラスの提案も行っています。
 
基本的に、ユースケース横断的な共通の機構でハンドリング可能なケースについては、非チェック例外のスローが適切だと考えます。対して、各ユースケース(の代替フロー)に対応した個別の判断ロジックを実装する必要がある場合は、チェック例外をスローし、呼び出した側で個々の例外をキャッチすることを強制することが適切と考えます。つまり、失敗終了〔機能要件〕のみチェック例外、それ以外は非チェック例外で実装するという割り振りとなります。
 
プログラムコードの“バグ”でNullPointerExceptionやNumberFormatExceptionが生じたら、まさにCondition BLACK=内的要因による異常終了、ということになります。
 
プログラムコードのロジックで、例えばトランザクションデータが参照するマスターデータを取得したら存在しなかったというデータ不整合を検知したとします。具体的にはそのようなチェックを行うif文を記述するでしょう。そのif文が真値となったとき、RuntimeExceptionまたはその派生クラスをプログラムコードのロジックで明示的にスローすることで、Condition BLACKに陥ったことを表明します。
 
Condition RED=外的要因による異常終了、の場合、即ちSQLExceptionやSocketExceptionなどを検知した場合ということになりますが、一般にその旨を表す非チェック例外(※ここではInfrastructureExceptionとした)にラップして再スローします。SQLException発生後の後始末などは、基本的にユースケース横断的にハンドリングすべき事柄で、個々のユースケースで個別にハンドリングしたい内容では無いでしょう。SpringやSeasar2などの現代のフレームワークの多くはこのような機構を装備しています。
 
ところで、ユニーク制約違反やその他のテーブル定義上の制約違反が発した場合、DBMS/JDBCはSQLExceptionとしてスローしますが、Condition RED=外的要因による異常終了として扱うべきではありません。Condition BLACK=内的要因による異常終了もしくはCondition YELLOW=機能要件に関わる失敗終了として扱うべき事柄です。この点を丁寧にハンドルする事は、ログ出力も行っている場合にそのログの品質を向上させるために重要です。
 
Condition ORANGE=非機能要件に関わる失敗終了の多くは、アプリケーションのプログラムコードで当該ケースに陥っているかの判断ロジックを個々に実装する必要があるでしょう。接続上限超過の場合には例えば"ConnectionTimeoutException"といった特別な例外が(係る資源Managerによって)スローされる場合もあります。プログラムコードのロジックで所定の例外(※ここではOperationalExceptionとした)を明示的にスローすることで、Condition ORANGEにあることを表明します。これらはユースケース横断的に適用される仕組みでハンドリングするのが妥当でしょう。
 
Condition YELLOW=機能要件に関わる失敗終了に対しては、呼び出した側が、それが担うユースケースの内容に応じて個別の固有のハンドリングロジックを実装する必要があります。私は『業務ロジックは静的型付け言語で実装すべき』という信念の持ち主です。その一環として、個別にハンドリングするひつようのある機能要件に関わる失敗終了のケースは、そのような各ケースの存在をロジック中で明確に表明し、かつハンドリングの漏れなどのエラーをコンパイル時に検知できるように、チェック例外にて実装すべきと考えています。
 
※Javaチェック例外に関する考察についての参考文献:
 
「Java言語のチェック例外は本当にGood Partなのか?」:
http://d.hatena.ne.jp/ryoasai/20110220/1298203734
 
トランザクション
 
異常終了の場合は議論無く一律roll backでよいでしょう。
 
各失敗終了にてcommitする場合もあり得るのでは?という議論があるかもしれません。本記事で規定している失敗終了は“警告”ではありません。明らかに処理完遂できなかったこと、即ち、"incomplete"ケースを表すことを意図しています。いわゆる“警告”すべき事象や状態が発生していても、処理としては完遂できている、即ち、"completed"ケースならば、終了のケースとしては成功終了、です。以上を踏まえて、成功終了の場合に限りcommit、そうではない失敗終了の場合は一律roll backが妥当、と考えます。
 
※なお、成功終了時に、“警告”すべき事象や状態が発生していたら、その旨についてINFOレベルのログ出力をしておくべき、と考えます。
 
HTTP Status Code (Web API)
 
処理がWeb APIとして実装される場合、各終了ケース=ConditionはHTTP上の電文で表現できる必要があります。ここで、成功終了の場合はHTTP Status Code 200、異常終了の場合は400番台または500番台とすることにはほぼ議論は無いでしょう。
 
失敗終了を200とするか4XX/5XXとするかについては多少議論がありそうです。私は、「HTTP Status Code 200として、失敗の内容は所定の書式でBodyに記述する」という方針を推奨します。理由は2点あります。一点目は、概念として失敗終了は“正常”のケースの一つです。(※そのように定義している。)ですから、成功終了が200、異常終了が4XX/5XXだというなら、失敗終了は同じ“正常”である成功終了と同じ200にすべきでしょう。というより、“異常”である4XX/5XXに含めるべきではありません。二点目は、Status Code 4XX/5XXは、HTTPによる通信自体のエラーによっても引き起こされます。HTTP通信エラーは“外的要因による異常終了”に分類されるべきです。失敗終了を400番台等で表現すると、失敗終了と異常終了との区別が難しくなります。
 
◆以上
 (※〔運用編〕へ続きます。)