例外機構を台無しにする巷の実装者たち

Javaには,異常系の処理を正常系の処理から明確に分離し,異常系の対処を実装者に強制することでコードの質を半強制的に高めるための「例外機構」が備わっている。例外は,チェックされるべき例外(Exceptionの直系)と,チェックしなくてもいい例外(RuntimeExceptionの系列)の2種類があり,「実装者に例外発生時の対処をさせるかどうか」でどちらを使うかを判断する。 多くの場合,業務的な例外はチェックされる例外,環境的な異常などの状況にはチェックされない例外が使われる。環境系の異常(ネットワーク異常,ファイルアクセス異常など)はアプリケーション内で共通的に処理されるだろうから,実装者にいちいち対処のコードを書かせる必要はない。それとは逆に,業務的に通常とは違う状況となった場合,その業務を使った局面によってその内容が変わる可能性があるので,業務それぞれで矛盾がない状態にしてあげるために実装者が個別に対処のコードを書く必要がある(もちろん割合の問題であり,業務的な観点でも対処を共通的に行えるものも存在するが)。 業務的な対処は,チェックしなければならない例外によって強制されるために,さほど問題にはならない。しかし,チェックしなくてもよい例外の扱いを間違えてしまうと,大変な結果を招いてしまう。 チェックされない例外の発生に対する対処は,データベースをロールバックしたり,作業用のファイルやキャッシュなどの後始末を行ったり,異常が検出されたことをログに出力して障害検出を可能にする,といった非常に重要な処理になる。これらは実装者が個別に対処することではなく,機構側が共通的に裏で勝手に処理することが望ましい。逆の言い方をすれば,実装者が個別にチェックされない例外を捕捉(catch)してはならない。独自に捕捉されて適切な処理が行われなかった場合,ロールバックを代表としたアプリケーションの根幹となる大事な処理が行われなくなってしまう。 機構を実装者に提供するアーキテクトは,チェックされない例外をうまく利用することで,共通的かつ重要な対処を実装社に対して隠す。チェックされる例外の場合は,対処のコードを書かなければコンパイルが通らない。それに対して,チェックされない例外はその対処を記述しなくてもコンパイラは怒らないので,実装者はそれに気がつくことなく,業務処理に専念できる。 このように例外をうまく使い分けることにより,異常系の処理は一括して機構側が握ることが可能になるはずである。しかし,現実はそんなに甘くない。「コンパイラに怒られることがない」=「実装者がチェックされない例外を捕捉することはない」という思い込みは,巷に溢れている多くのプログラマによって裏切られる。 コンパイラに怒られることがなくても,実際に動作させてみると,チェックされない例外が発生するパターンは往々にして存在する。特に実装作業の初期段階では,実装環境の整備に不足があったり,チェックされない例外の発生に対する共通的な対処が未実装だったりすることが多い。このような状況でチェックされない例外が仮に発生した場合,例えばWebアプリケーションであればHTTP500エラーがWebブラウザに返却され,アプリケーションの動作は失敗して実装者に例外のスタックトレースが提示される。 技術的な知識レベルが低く,さらにソフトウェア開発に対する意識も低い実装者は,自分がコーディングしたソフトウェアについて,「動いているように見せる」ように何とかしようとする。UIを伴うアプリケーションであれば,裏側で何が起きようとも,画面遷移が行われて表示情報がそれなりに表示されれば,正しく動いているように見せかけることは可能である。つまり,動作確認してチェックされない例外が発生してしまった場合,超短絡的に「例外が発生しても,それを捕捉して何もしないようにしてしまえば良い」と考える実装者がこの世に存在する。しかも,結構な高い割合で存在しているのが現実だ。 具体例を示してみよう。例えば,

class Logic {   void foo() {     …     if (…) {       throw new OptimisticLockingFailureException();     }     …   }

}

というような,ある状況によって楽観的排他制御に引っかかってチェックされない例外(OptimisticLockingFailureExceptionはRuntimeExceptionを継承)が発生するコードにおいて,それを利用する側は,

void process() {   Logic logic = …;   logic.foo(); }

だけでいいものを,わざわざ,

void process() {   Logic logic = …;   try {     logic.foo();   } catch(OptimisticLockingFailureException e) {

  } }

とされてしまう。こうなると,メソッド呼び出しの上位に存在する共通機構に例外が伝達されないので,一気に楽観的排他制御時の共通的な対処が行われなくなる。その結果,本来「他のユーザに更新された可能性があります」というエラー画面を出さなければならないのに,「正常に完了しました」とあたかもうまくいったようになってしまう。さらに,単独の人間が単に正常系の試験を行っただけではこの問題を発見するに至ることはなく,コードを見ていかないと発見することは困難である。つまり,運用時になって初めて表面化される潜在的な不具合となってしまう。 こんなコードを実装者が書く動機とすれば,動作確認時にたまたま発生した例外について,これが起き得る本質的な原因究明をすることなく,「よくわからない例外が発生したけど,まぁ『なかったこと』にして動作しているように見せかければそれでいいや」ということ以外は考えられないだろう。あまりにも無責任な対応だと言えるのだが,これで対価をもらえる今のソフトウェア業界は,はっきりいって天国だ。 アーキテクトや優秀な実装リーダーが,例外の体系をきれいにして実装者の負担を軽くしようと工夫しても,上記のようなことをやられてしまえば例外機構は台無しである。これを防ぐためには,

  • チェックされない例外について,具体的な例外クラスを示した上で,「捕捉してはいけません」という規約をとにかく徹底させる。

  • 「ぬぉ,例外のスタックトレースだ。これはまずいので捕捉してスタックトレースが表示されないようにしてしまおう」と思わせないために,実装者の実装作業の前に共通的な対処の機構を予め準備しておく(共通的に正しい対処が行われれば,それ以上何もしないだろうから)。

  • 単体テストをとにかく厳格に行う。チェックされない例外が発生したときのパターンについて,それが捕捉されずにそのまま上位にスローされることをちゃんと確認するよう徹底する。

  • とにかくコードレビューを徹底する。 という実践が必要となるだろう。 巷の実装者が書くコードについて,本当に驚かされることが少なくない。例外1つ取っても,やはり「共通的な機構や品質確保に対する事前の準備が重要」ということに尽きる。 それにしても,年々Javaプログラマの質は下がる一方ではないだろうか。EoDなどの推進も大事だが,プログラマの教育についても,企業はもっとお金をかけるべきである。そうでないと,近い将来「やっつけコードしか書けないプログラマ」しかソフトウェア業界にいなくなってしまうだろう。。。

このエントリーをはてなブックマークに追加

関連記事

2023年のRemap

Remapにファームウェアビルド機能を追加しました

Google I/O 2023でのウェブ関連のトピック

2022年を振り返って

現在のRemapと今後のRemapについて