想定読者
- プロジェクトに入ったばかりの新卒エンジニア
対応手順
手順1:現象の言語化
手順1のゴール:誰が読んでも再現できる状態にする
不具合には他者から報告された意図しない挙動、開発中に直面した不具合など様々なパターンがあります。どのような場合でも、以下を明確にする必要があります。
- 前提条件
- 誰が、どの環境で、いつ、どのようなデータの時に発生するか?
- 操作手順
- 誰でも再現できる手順となっているか?
- 期待される結果
- 仕様書・テストに記載があるか?
- 実際の結果
- 手元で再現できる事象か?
- 不具合再現率は100% か?
実際のプロジェクトにおいては、影響が軽微な不具合など対応を後回しにすることがあります。
その時、見つけた不具合は将来対応する項目として記録しておきます。1
このチケットは起票した人が対応するとは限りません。そのため誰でも再現できて、だれでも修正へと着手できるようにする必要があります。
手順2:予測する
手順2のゴール:原因の候補を並べ、検証順を決める
発生している現象を言語化したら、原因を複数予測します。
経験を重ねれば重ねるほど、より精度の高い予測をできるようになり、調査対象を絞ることができるようになります。
しかし、これはそもそも予測しない、と等価ではありません。
予測したうえで、原因の可能性から除外しているにすぎません。
予測の材料になるのは、例えば以下があります。
- エラーログの記載
- 手順1で言語化した現象
Webアプリケーションにおける原因分類としては、以下のようなものがあります。
- 入力内容(リクエスト内容)
- データ(DB)
- ビジネスロジック
- 外部依存
- 環境差 など
例えば、以下のような事象が当てはまります。
- 入力内容はあっているか?
- 該当するデータは存在するか?
- 分岐の考慮漏れはないか?
- 外部サービスの制限に引っかかっていないか?
- 本番データと検証データのデータ量が起因ではないか? など
手順3:予測に対して適切な調査を行う
手順3のゴール:候補をつぶして原因を特定する
手順2で建てた予測から確度が高そうなものを検証し、可能性をつぶしていきます。本フェーズではまだ修正は行いません。
本調査フェーズでのコードの変更は環境再現のための変更(設定値の変更など)とログの追加に留めます。
調査・修正を同時に行うことは「影響範囲の見誤り」、「複数原因があった場合の見落とし」など再度不具合が混入する原因となります。
検証の順序・確認ポイントとしては以下を押さえるとよいです。
- ログの確認
- 処理の始まりはどこだったか
- エラーの発生点はどこか
- 始まりからエラーまでの間にどの処理を通ったか
- 既存のテストコードの確認
- テストコードが存在するか
- テストは成功しているか
- 仕様通りのテスト内容となっているか
- print デバッグ(ログ出力による確認)
- 意図した値が格納されているか
- 通るべき箇所を通っているか
- 通るべきではない箇所を通っていないか
- デバッガの利用(print デバッグで条件を絞り切れない場合)
- 各メソッドでどのような型の値が返ってきているか
- 定義した変数がいつ、どのように変化しているか
手順4:発見した不具合原因を修正する
手順4のゴール:エラーの解消ではなく、仕様に沿った修正を行う
原因を特定したら修正を行います。この時、安易なエラー解消を目的とするのではなく、修正の方針をよく検討する必要があります。
手順1で言語化した正しい動作・正しい仕様を考慮した上で、適切な修正方針は何か? を考えなければ、不具合の再発や負債となるコードを作ることになります。
また、手順3で特定した原因が、他の箇所でも同様に存在している可能性を考慮する必要があります。
類似原因による不具合を発見した場合、合わせて修正を行うかはプロジェクトの方針によります。後で対応する場合は手順1 にあるようにチケットの起票のみを行います。
具体的な内容については、手順5で説明します。
実装方針の参考
例えば、下記のようにログインを実施するコードがあったとします。
public class User {
private String email;
private String passwordHash;
public User(String email, String passwordHash) {
this.email = email;
this.passwordHash = passwordHash;
}
public String getEmail() {
return email;
}
}
public class UserService {
public User authenticate(String email, String password) {
User user = userRepository.findByEmail(email);
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
throw new AuthenticationException("password mismatch");
}
return user;
}
}
public User login(LoginRequest request) {
return userService.authenticate(request.getEmail(), request.getPassword());
}
本コードでは、下記のような不具合が発生します。
- ログインフォームを空欄のまま送信
- request.getEmail() が null
- authenticate(null, password) が呼ばれる
-
userRepository.findByEmail内でエラーが発生する
エラー解消のみを目的とした修正をする場合は下記のような例があります
public User authenticate(String email, String password) {
+ if (email == null) {
+ return null;
+ }
User user = userRepository.findByEmail(email);
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
throw new AuthenticationException("password mismatch");
}
return user;
}
本修正でも、「ログインフォームのemailが空の時、ログインできない」という処理は成立する可能性があります。2
しかし、このような安易な修正には、以下のような視点が抜けています。
- 処理の性質上、
emailは必須入力項目となる。にもかかわらず、null が渡ってきている原因は何か? - 認証することが目的のメソッドであるにもかかわらず、email の存在チェックを本メソッド内のみで行うのは責任の持ち方として正しいか?
現在のコードが抱えている本質的な問題は、ログイン処理に必須となる email の空入力が許容されている点となります。そのため、実際に修正が必要なのはControllerと LoginRequest となります。
ここでは説明を単純化するため email の例に絞っています。password も必須項目ですが、手順5 で示すように対応するかどうかは必要に応じて判断します。
public class LoginRequest {
private String email;
private String password;
+ public void validate() {
+ if (email == null || email.isBlank()) {
+ throw new ValidationException("email is required");
+ }
+ }
public String getEmail() {
return email;
}
}
public User login(LoginRequest request) {
// 本来はエラーに対して適切なHTTPステータスコードの返却を行うが、
// 本記事では簡略化のため省略する。
+ request.validate();
return userService.authenticate(request.getEmail(), request.getPassword());
}
補足
実際の業務では、メソッド内に下記のような処理を加えることが多くあります。
特に集団開発において、作成者の意図しないメソッドの使い方をされることがあり、その事故を防ぐためにコード上で必須項目であることを示すために記載します。
本処理の追加は不具合の修正ではなく、メソッドの利用条件を設定するための安全装置です。
LoginRequest同様に、 email の例に絞っています。
+/**
+* メールアドレスとパスワードを用いてユーザー認証を行う。
+*
+* @param email ユーザーのメールアドレス(必須)
+* @param password パスワード(必須)
+* @return 認証済みのユーザー
+* @throws IllegalStateException メソッドの前提条件が満たされていない場合(呼び出し側の実装ミス)
+* @throws AuthenticationException 認証に失敗した場合
+*/
public User authenticate(String email, String password) {
+ // 本来は入力時に弾くべきだが、意図しないメソッドの利用を早期検知するために保険として書く。
+ if (email == null) {
+ throw new IllegalStateException("authenticate called with null email");
+ }
User user = userRepository.findByEmail(email);
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
throw new AuthenticationException("password mismatch");
}
return user;
}
補足:調査用に追加したログの扱いについて
手順3の調査フェーズでは、原因特定のために一時的なログを追加することがあります。
これらのログは、原因が修正できた時点で原則として削除しましょう。
調査用ログを残したままにすると、
- ログによるノイズが増え、重要な情報が埋もれる
- ログ出力量増加による性能劣化
- 本来想定していない情報漏えい
といった問題につながります。
なお、再発防止や監視の目的でログを残す場合は、「なぜこのログが必要なのか」を説明できる状態にした上で、恒久ログとして整理して追加します。
手順5:不具合は修正されたか・類似の不具合がないかの確認
手順5のゴール:直ったこと、壊してないこと、取りこぼしがないことを証明する
手順4で不具合を修正した後、完了報告をする前に4点確認するべきことがあります。
- 手順1 の再現手順を再度行い、期待される結果が得られるか?
- 不具合に対し、適切なテストコードを作成したか?
- 既存の機能で壊れた場所はないか?
- 同一原因・類似原因で不具合が発生していないか?
1. 手順1 の再現手順を再度行い、期待される結果が得られるか?
修正後は、手順1で整理した再現手順を最初から最後まで通しで実行し、期待される結果が得られるかを確認します。
期待通りに動作しない場合は原因が複数存在している可能性があるため、手順2に戻り、追加の予測・調査を行います。
補足:再現が難しい不具合の場合
本番環境に依存するなど、ローカルでの再現が難しい不具合も存在します。
その場合は、ログを追加した上でリリース後に経過観察を行い、
実際に不具合が解消されたかを確認することがあります。
2. 不具合に対し、適切なテストコードを作成したか?
一度発生した不具合は、将来の改修や仕様変更によって再度混入する可能性があります。
そのため、不具合の内容に対応したテストコードを作成します。
- 不具合が再現する条件をテストで表現できているか
- 修正後の挙動が期待通りであることを確認できているか
仕様自体を変更した場合は、テストコードも新しい仕様に合わせて修正します。
なぜテストを書くのか
テストコードは品質の保証手段であると同時に、将来の変更によって仕様から外れた挙動になった場合の早期検知手段です。
3. 既存の機能で壊れた場所はないか?
今回の修正が、既存機能に影響を与えていないかを確認します。
最低限、以下を実施します。
- 既存のテストコードがすべて成功するか
- 修正したコードが、他にどこから呼ばれているか
既存機能に影響が出ている場合は、修正方針を再検討します。
補足:集団開発での注意点
集団開発では、影響範囲が想定より広い場合や修正に時間がかかる場合、対応方針の判断はレビュワーや管理者が行います。
そのため、以下の情報を整理して共有します。
- 現在までに実施した内容
- 影響範囲
- すべて修正した場合の想定工数
影響が大きい場合、「今回は対応せず、後で対応する」と判断されることもあります。
4. 同一原因・類似原因で不具合が発生していないか?
3.では「今回の修正によって壊した箇所」を確認しました。
本項目では「修正した原因が、もともと他の箇所にも存在していなかったか」を確認します。
今回対応した不具合は、特定の1箇所だけで起きているとは限りません。
同じ原因が、別の機能や別の呼び出し経路でも発生していないかを確認します。
原因を「その画面特有の問題」として捉えるのではなく、入力・データ・ロジック・外部依存といった形に一般化し、同じ前提条件が成り立つ箇所を洗い出すことが重要です。
類似不具合の探し方例
-
原因を1文で言い換える
- 例:nullを許容してしまっている
- 例:存在しないIDでも処理が進む
-
同じ前提条件が成り立つ箇所を探す
- 同じRequest / DTOを使っている処理
- 同じService / Repositoryを呼んでいる処理
-
境界値・入力バリエーションを再確認する
- null / 空文字 / 空配列
- 存在しないID / 削除済みデータ
- 権限なし・未ログイン状態
どこまで対応するかの判断基準
類似不具合が見つかった場合、必ずしもすべてを同時に修正する必要はありません。
判断のため、以下の情報を整理して共有します。
- 発生箇所と内容
- 想定される影響範囲
- 修正した場合の想定工数
これにより、レビュワーや管理者が
「今直す / 後で対応する」を判断できます。
-
チケットを起票する、といったような言い方をします。チケット=ToDo のようなもので、本セクションで言語化したような内容を網羅して記載するものです。管理ツールとして、Jira、Backlog が有名です。 ↩
-
本メソッドを利用する場合 。自作の関数で確認する場合など、
nullとnullの比較でtrueを返す可能性があるため、一概に処理が成立するとは言えません。 ↩