この記事は クラウドワークス Advent Calendar 2022の9日目の記事です。
はじめに
クラウドワークスの@nisyuuです。エンジニアとしてクラウドテックというハイクラスなエンジニア・デザイナーを中心とした人材と企業をマッチングするエージェントサービスを開発しています。
アドベントカレンダーへの投稿は初めてなため、温かい目で読んでいただければ幸いです。
本記事のタイトルは「いかにして課題をみつけるか」というタイトルですが、簡単に説明するとシステム開発における問題に対する課題の立て方をテーマにしています。
エンジニアであれば誰もがバグに遭遇するはずです。システムの不具合に何度も遭遇してきたエンジニアであれば、容易にバグの調査に取り掛かることができるでしょう。しかし、バグは頻繁に起こり得るものであるにもかかわらず、アプローチの方法はさほど論じられていないように思います。確かに、バグを特定できるか否かは経験にもよるかもしれません。バグは特定しやすいものからそうでないものまで幅広くあり、特定の仕方も問題の種類に応じて柔軟に対応する必要があります。ただし、特定するための根本的な考え方はどの問題にも通用するのではないかと考えています。
そこで今回は システム開発における問題と課題について認識 し、不具合の原因を特定するまでに至るプロセスと根本的な考え方 を提示していきます。
本記事はエンジニアリング領域に触れながら話を進めますが、根本的な考え方はエンジニアリング以外の様々な問題にも適応できます。そのため、エンジニア以外の方にも有用な記事になっているのではないかと考えております。もしあなたがエンジニア以外の方であれば、技術的な部分は飛ばして読んでいただいても構いません。
問題と課題
問題と課題は一見すると同じような言葉に見えますが、問題 は解決を必要とする題のことで 課題 は問題を解決するために果たすべきことを意味します。
図1は問題が発生したときの問題の見え方と課題の見え方を表現した図です。問題が起きた時点で、問題は既に確認が可能であるのに対して課題はまだ見えておらず、これから発見すべきであることを表しています。問題を解決するための課題はいくつも存在しているかもしれませんが、最適な課題は限られていることがほとんどです。最適な課題は見極めながら見つけていかなければならないため、安易な推測で課題を立ててしまうことは避けましょう。
開発現場ではシステム不具合による問題が付きものです。問題を解決するために必要な課題は、自ら発見していかなければなりません。画面から分かる挙動やシステムのログなどから問題の原因を推測し、効果的な課題を立てていくことで問題を解決していくことができます。
心構え
起きた問題を解決できるかは経験にもよるかもしれませんが、何よりも労力と根気が必要です。大抵の場合、予想していた問題は予め対策されているため未然に防げていることが多いのですが、全ての問題を予想し対策することは大変難しいことです。そのため、予想できなかった未知の問題に取り組むには問題を理解するところから始め、原因を推測し課題を立て、解決にあたっていかなければなりません。問題を解決するまでに至る道のりは短いものもあれば長いものまであります。例え長い道のりであっても一つ一つ丁寧に取り組んでいくことで、効果的な課題が見つけられるでしょう。
問題解決の中で行き詰まることがあれば、エンジニア向けのQ&Aサイトに質問を投稿してみることや他のメンバーにアドバイスを求めることで解決への糸口が見つかるかもしれません。他の人にサポートを求めることも問題解決の一つの術であることを覚えておきましょう。
問題を理解する
課題を発見するためには、問題を理解する必要があります。 起きている事象 と 事象による弊害 、正解は何か を認識しておかなければなりません。問題を理解せずに課題に取り組むことは、目的がないまま行動することと同じです。そのため、問題が起きた際は問題を正しく捉えることから始めましょう。問題を正しく捉えることで最適な課題を探し出すことができ、問題を解決へと導くことができるでしょう。
起きている事象を確認する
問題が起きたときは何が起きているのかを認識しましょう。ボタンが押せなくなっているのか、ボタンは押せるが値が保存されないのか、ボタンを押した後の遷移先が間違っているのか、といったように丁寧に確認してください。
環境の問題で再現が難しい場合もありますが、できるだけ再現方法を明確にしておきましょう。
事象による弊害を認識する
問題になっているということは目的達成への弊害が起きているということです。目的達成への弊害が起きていないのであれば、そもそも問題でない可能性があります。
開発をしていると使われなくなった機能のバグに遭遇するかもしれません。使われていない機能は現時点で必要がない可能性があるため、バグを修正するのではなく機能自体の見直しをすることが賢明です。
正解を定義する
問題を解決したいのであれば正解がなければなりません。開発における正解は要件によって変わります。正解が分からないのであれば、まずは要件を見直し正解を定義しましょう。
ただし、要件に関わらない技術的そのものの問題は、対応すべきことがある程度定まっていることもあるため、正解を定義する必要性は低いでしょう。
原因を推測すること
問題を正しく捉えることができて初めて課題を見つけることができます。問題を効率よく解決するためには、最適な課題を探し出すことが先決です。また、課題は1つだけでなくいくつも存在する可能性があります。
最適な課題を探し出すには、 仮説 を立てて課題を発見していくことが効果的です。バグの特定に慣れていないエンジニアは、インターネットにありふれている解決策を手当たり次第に実践してしまいがちです。エラーログなどを検索し、似たバグに関する情報を探すことも大切ですが、まずはシステムを理解しログや実行結果などから仮説を立てていくことで、課題発見の確実性を上げることができるでしょう。また、過去に発生した類似のエラーを探し原因を探っていくことも有効的です。
仮説を立てる
問題が引き起こされるパターンを考えることで、原因を特定するまでの道のりを短くすることができます。ただし、仮説を立てるためには システム構成やソースコードへの理解 、ログを読み解く力 、デバッグをする力 が求められます。
システム構成やソースコードへの理解
問題の原因が発生している箇所を特定するためには、システム構成やソースコードで実装された処理への理解が不可欠です。例えば、Dockerの設定ミスが原因で環境が正常に立ち上がらない問題があるとします。もしDockerが使われていることを知らなければマウントができていないのか、それともポートの設定が間違っているのかなど、原因の推測がしづらくなってしまいます。
また、ソースコードのロジックに不備があった場合、ソースコードの処理を把握しておかなければちょっとした不備にすら気付きづらくなるでしょう。
理解しておく対象のシステムは、自身のローカル環境だけでなくステージング環境と本番環境についても当てはまります。CDNや外部連携などはステージングや本番環境でしか使えない場合がよくあります。ローカル環境との差分を知らない状態だと、万が一差分のある部分で障害が発生すると原因の特定に時間がかかってしまいます。システム構成はいつでもチェックできるはずなので、今取り組んでいるプロジェクトの構成を把握していない場合はすぐにチェックしてください。
ログを読む
何か問題が起きると、ほとんどのエンジニアはログを真っ先に探しにいくと思います。これは正しい判断です。多くのシステムは予期せぬエラーが発生した場合に、発生箇所と処理の誤りがどのようなものかをログに吐き出してくれます。
以下のコードは、Rubyのコードと出力されたエラーです。
irb(main):001:0> countries = {Japan: {capital: 'Tokyo'}, Germany: {capital: 'Berlin'}}
=> {:Japan=>{:capital=>"Tokyo"}, :Germany=>{:capital=>"Berlin"}}
irb(main):002:0> countries[:Spain][:capital]
(irb):2:in `<main>': undefined method `[]' for nil:NilClass (NoMethodError)
(irb):2:in `<main>': undefined method `[]' for nil:NilClass (NoMethodError)
がエラーログにあたり、エラーが発生している箇所とどのような誤りがあるのか出力されています。実際にエラーログを読んでいくと、undefined
とあるため何かが定義されていないのだなと分かります。次にfor nil:NilClass
とあるので、nil
に対して何か操作を使用としてエラーが出ていることが分かります。ここでエラーが出ているコードを見てみましょう。countries
にはSpain
シンボルがないためcountries[:Spain]
はnil
となります。countries[:Spain][:capital]
は、nil
であるにもかかわらずcapital
シンボルを指定しているためエラーになっているのではないかと推測できます。エラーの内容と推測したコードの挙動は合致しているため、おそらく合っていると考えられるでしょう。
上記の例ではエラーをそのまま読み解くことができましたが、時にはエラーの内容が上手く読み解けないこともあります。そのような場合は、エラーログをそのまま検索してみることでヒントを探してみてください。
デバッグをする
常にログからエラーの原因を特定することができるとは限りません。そのようなときは、変数やメソッドなどの実行結果を実際に出力することが有効的です。
ほとんどのWebフレームワークにはデバッグを簡単にできるツールがあります。Ruby on Railsであればpry-rails
というgemを使うことで処理を途中で止め、コンソール上で止めたところまでに実行された処理を確認することができます。JavaScriptであれば、console.log
などを使うことでブラウザの開発者ツールに実行結果を出力させることができます。
類似のエラーログを探す
エラー監視ができるようなモニタリングツールを導入しているプロジェクトでは、エラーが発生した際にアラートを通知させることや、エラーログを蓄積することができます。
そのため、過去に対応したエラーと同じようなエラーが発生した場合は、蓄積されたログを探すことで解決のヒントになるかもしれません。
分解と結合
問題を引き起こしている原因が1つのときもあれば、2つ以上のときもあります。複数の原因が重なることで複雑な問題に見えることもありますが、 1つ1つの原因は簡潔 であることがほとんどです。複数の原因があると推測できれば、原因を最少単位に分解し、分解後に原因を結合させて同じ問題が引き起こされるかを考察しましょう。
課題を立てること
問題の原因が推測できれば、次に原因を解消するための課題を立てます。課題は明確で簡潔にしたものをGitHubのイシューなどへ作成しましょう。プロジェクトによっては他のタスク管理ツールでタスクを起票しているかもしれませんが、本記事ではイシューを例にして話を進めます。GitHubのイシューを使ったことがない方は、こちらのリンク先を参考にしてみてください。
修正対象が2つよりも多くなるのであれば、1つのイシューに課題をまとめてしまうのではなく分割することで、課題に取り組みやすくなります。ただし、原因が複数であっても解決が容易、もしくは分割して修正することが困難であれば1つのイシューにまとめても良いでしょう。
課題を立てる際には設計も考慮する必要があります。設計をしないまま課題を立ててしまうと、課題の確実性が低くなる他、見積もりが判断しにくくなります。実装方針と必要な対応を予め計画しておくことで初めて、正しい課題が立てられます。
イシューを分ける
GitHubなどのイシューは、プルリクエストを紐づけることができます。つまり、イシューが含んでいる対応範囲が大きすぎるとプルリクエストに含まれるソースコードの差分も多くなる可能性があります。差分が多くなるとレビューへの負担にもなることから、なるべく 1つのイシューが担う対応は小さくする ことが望ましいです。
設計と課題
既存のソースコードに変更を加えることは、新しく機能を実装するよりも難しいことがほとんどです。最近実装したソースコードであれば、影響範囲を思い出すことは簡単かもしれません。しかし、遠い昔に実装されたソースコードを紐解き影響範囲を洗い出すことは労力が必要になります。運が悪いと、既にいなくなったメンバーにしか分からないような実装があるかもしれません。迂闊にソースコードへ変更してしまうことはリスクが伴うため、ある程度設計を意識して変更を入れる必要があります。そのため、DRY原則や直行性、結合度などを考慮して実装方針を立てることでできるだけリスクを抑えた課題を立てられます。
設計に関しては以下の書籍が参考になるのでぜひご覧ください。
設計の参考になるおすすめ図書
達人プログラマー(第2版)
ソフトウェアアーキテクチャの基礎
最適な課題が立てられない
全ての問題に対して最適な課題が立てられるとは限りません。時間や予算などの都合、時には根本的な原因が推測しづらく課題が上手く立てられない場合もありえます。 最適な課題が立てられないときは、応急的な課題を立てることも検討 しましょう。もしくは、 機能要件を見直すことで問題を回避することができるかもしれません 。
応急的な課題を立てた場合、応急対応のままにしておくことは賢明ではありません。根本的な対策があるのであればイシューを作成しておき、いつでも取り掛かることができるようにしておきましょう。対策がまだないのであれば、調査イシューなどを作っておき、問題が忘れ去られないようにしなければなれません。
振り返ってみること
問題の理解 、 原因の推測 、 課題の作成 ができれば、本当に解決できるようになったか振り返ってみましょう。振り返るときは自分1人でなく、 同じ開発チームのメンバーに説明をしながら振り返ってみましょう 。他のメンバーに共有することで、自分だけでは気づかなかったことに対し意見をもらえるかもしれません。また、経験がまだ浅いメンバーに対しても共有することで課題を見つけるまでの考え方、手順を教えられる場にもなります。
振り返ってみて問題を解決できないことが分かったのであれば、再び問題の理解、原因の推測、課題の作成、振り返りを実践してみてください。
問題が解決できそうであれば、残すは課題を実施するだけです
おわりに
本記事をご覧いただき、ありがとうございました。
ここで最後となってしまいますが、問題解決のための課題をいかにしてみつけるかを考察しながら提示してきました。
本記事を読んでいただいている皆さんは、これからも多くの問題にあたるはずです。時にはエンジニアリング領域に関わらない問題が立ちはだかることもあるでしょう。問題解決の根本的な考え方は、様々な問題に対しても利用できます。以下に本記事で提示してきた課題発見のプロセスをまとめました。問題に遭遇したときはぜひ参考にしてみてください。
いかにして課題をみつけるか
- 問題を理解すること
- 起きている事象を確認し再現できるのであれば再現させる
- 事象による弊害を捉え問題の本質を認識する
- 正解となる結果を知っておく
- 原因を推測すること
- ログやデバッグの結果から仮説を立てる
- 過去に発生した類似のエラーログを探す
- 原因を最少単位に分解し再び結合して同じ問題が起きるか考察する
- 課題を立てること
- 課題に着手しやすいようにイシューを分ける
- 設計を考慮しリスクを抑えた課題にする
- 最適な課題へ取り組むことが難しければ応急的な課題を立てる
- ふり返ってみること
- 問題、原因、課題を他のメンバーに共有し見直す
- 問題を解決できないことが分かれば再び問題の理解、原因の推測、課題の作成、振り返りをする
- 問題を解決できることが分かれば課題を実施する