🪛

コア機能のリファクタリングで取り組んだこと

2023/12/07に公開

この記事は、Magic Moment Advent Calendar 2013 7日目の記事です。

Magic Moment@scent-y です。

今年、コア機能での障害に起因した中核データの不整合解消や顧客案内のため、エンジニアやCSのリソースが大きく割かれてしまうという課題に直面しました。根本対応のためタスクフォースに参画しコア機能のリファクタリングを行ったので、どのように取り組んだか紹介したいと思います。

Magic Moment Playbook のコア機能とは

顧客との合意項目を中心に営業オペレーションを定義し、営業活動の中でうまれた顧客との合意内容を記録する機能です。記録内容に応じてレポート機能の数値を更新したり、Salesforce や HubSpot に連携したり、
シーケンスのトリガーとして利用されたりします。
サービスの中核データにあたる顧客との合意内容の永続化を行う、非常に重要な機能になります。

コア機能で発生した障害と課題

コア機能では非同期処理を採用しており、処理中はUI上で操作できない状態になります。処理中のまま完了しない障害が発生し、顧客のオペレーションを止めてしまうと共に、一度の障害あたりに4-5名程度のエンジニアの数時間が消費されていました。
saga パターンを採用していて永続化対象のドメインオブジェクトをシーケンシャルにローカルトランザクションで永続化していく仕様になっていたのですが、補償トランザクションが整備できておらず、データ不整合が発生した際にエンジニアが都度対応しており、トイルが急増していました。また、オーケストレーションベースの saga を採用していて、オーケストレータサービスが単一障害点となってしまう課題がありました。
他に、単体テストの不足やエンジニアの認知負荷が高いという課題がありました。

タスクフォース初期フェーズの取り組み

メンバーに機能詳細を共有

全員が有識者の場合は不要ですが、リファクタリング対象へ詳しくないメンバー向けに機能詳細を共有し、全員が同じ認識を持つ状態にしました。

既存ロジックと現状の課題の洗い出し

扱うドメインオブジェクトと連携するサービス(弊社ではマイクロサービスを採用)が多い機能なので、把握漏れを防ぐためにコードを一行一行追いかけて、処理内容を書き出しました。他サービスとの連携でも何を行っているか洗い出すことで、全体像をまずは正確に把握することに努めました。

設計方針を決め、早い段階でフィードバックをもらい全体最適を目指す

基本方針としてはアプリケーションの複雑性を低下させ、不整合を起こしにくい設計にすることを目指しました。具体的にはsaga の利用を止めてシングルトランザクションに変更、補償トランザクションの廃止、連携している他サービスとのリトライ可能に、といった方針を決めました。
弊社ではProduct Development会というより良い設計のための建設的な議論を目的とした場があるのですが、そこで設計方針を共有し、早い段階でフィードバックをもらいました。その結果、性能テストの観点でのQA項目の追加や異常系フローの整理など、新たな気づきを得ることができました。また、不確実な要素に対する意見をもらうことで、プロジェクトの初期段階で潰すべきものに気づくことができました。

スコープを決め、扱わないものを明確にする

既存仕様の課題点に対してあれもこれもと色々な箇所を改善したくなりましたが、開発期間が限られているため、全てに対応は不可能です。リリーススケジュールを踏まえ、必須要件として対応するもの、nice-to-have とするもの、今回のリファクタリングでは扱わないものを明確にしました。

タスクフォース開発フェーズの取り組み

ファットになっているユースケース層の単純化

ユースケース層が肥大化しており複雑性と認知負荷が高かったり、ビジネスルールがユースケース層に含まれており責務が曖昧になっているという課題がありました。ビジネスルールはドメイン層に移し、ユースケース層から過多な責務を取り除くということを行いました。

単体テストの実装を必須に

リファクタリング対象のコードは単体テストがほぼ実装されておらず、デグレードを検知しづらいという課題がありました。コア機能かつ複雑な機能であるため単体テストの重要度は高いとみなし実装は必須とし、テストのケース名でどのようなビジネス要件を検証したいのか分かるように、ストーリーの伝わるテスト名を意識して実装しました。

before ... 単体テストが実装されていても正常系ということしか分からず、ケース名から何を検証しているか判断できない

{
    name: "success",
    args: args{ ... },
    wantErr:    false,
    want: &SavePlaybookResponse{ ... }
}

after ... 何を検証しているのか、非エンジニアでも把握できるケース名に

{
    name: "[正常]Playbook保存時に活動記録を行った場合、活動履歴が永続化されている",
    args: args{ ... },
    wantErr:    false,
    want: &SavePlaybookResponse{ ... }
}

また、検証が必要な項目を単体テストで実装することで、ローカルでマイクロサービス環境を立ち上げて手動で検証する工程を省くことができ、開発フェーズでの効率化に繋げることができました。

過去の歴史を紐解く

開発を進める中で、なぜこのような仕様になっているのか分からない、という場面に遭遇することが何度かありました。ブラックボックスとなっている箇所は経緯や背景を正確に把握した上でリファクタリングしないと意図しないデグレードにつながる恐れがあるので、把握してそうな人に訪ねたり、過去のプロジェクトの設計ドキュメントを参照したり、根気強く紐解いていくということを行いました。

想定外の挙動を発見した際、修正すべき既存バグに該当するものか、振る舞いの変更に該当するものなのか、明確に区別する

想定外の挙動に直面することが何度かありました。バグではないが改善したいものに該当する場合は振る舞いを変更することになり、対応すると影響範囲の調査や検証項目の増大が発生します。なので、振る舞いの変更に該当するものは基本的にスコープ外としました。既存バグに関してはトリアージを行いリスト化し、QA項目に反映できる状態にしました。

他プロジェクトでコア機能に関わる修正をした場合、変更履歴として情報を集約させる

コア機能は影響範囲が大きく、他プロジェクトでも同じ箇所に対して並行して改修される可能性がありました。周知しコア機能に関わる修正をした場合はNotion のデータベースへリリーススケジュール含め記述を依頼することで、統合が必要な箇所を把握し、リリース前のQA段階でコンフリクトが発覚し後手で対応するという自体を避けるようにしました。

スコープ外としたがいずれ対応したいものをリスト化

リソースの関係でスコープ外としたものはストックし、トリアージをした上で定期的に振り返りを行い、後々対応できるような状態にしました。

タスクフォースQAフェーズの取り組み

各スクラムチームにQA実施のヘルプを依頼

メンバーのみでQA項目を作成するとサービスによってはドメイン知識浅く、視野が狭くなる恐れや検証漏れが発生する恐れがありました。そこで、各スクラムチームにお願いしたい領域と想定工数を明記した上でQA実施のヘルプを依頼しました。結果、各サービスのドメイン知識を活かしたQA実現と不足していた知識を他スクラムチームが補完してくれる形になり、品質向上に大きく寄与しました。
QAフェーズの前あたりのタイミングで依頼することを決めましたが、今回のような大規模なリファクタリングの場合は初期の方針検討タイミングで依頼するか決定すると、もっとリソースの調整がしやすかったなと思います。また、有識者に探索的テストを依頼すると、よりQAの質を高めることができたなと思います。

QA項目の相互レビュー

メンバーで作成したQA項目を相互レビューすることで、ブラッシュアップを行いました。相互レビューは非同期で行ったのですが、デッドラインを超えてレビューが実施されることやケースの重複などが発生したので、その場で観点の意図や項目の重複などスピーディーに指摘するため、同期的に集中して実施した方が良いなと感じました。

Issueリストの作成

QAで発覚したバグはIssueリストへ集約し、トリアージを行い対応ステータスを明確にしました。対応完了後は起票者に再QAを依頼し、バグが再発しないことを確認しました。
IssueリストとQAリストを紐づけることで、再QAできる状態か明確になりました。

QA項目を正常系、異常系とタグで振り分け

重要な観点である異常系をどれくらい網羅できているのか可視化することで、異常系の観点が不足していることが分かり、QA観点を改善できました。

QA実施でバグを発見した際に切り分ける

大規模なリファクタリングなので、バグを発見した際は既存バグのエッジケースを踏み抜いたのかリファクタリング起因で発生したのか切り分け、トリアージを行いました。

検証環境で例外を発生させた

一定期間の間は検証環境で意図的に例外を発生させることで、異常系の検証をスムーズに進めることができました。ただ、その間は正常系の検証を並行して進めることができなくなるので、可能であれば例外発生用の環境を別途用意できると良いなと感じました。

まとめ

ちょうどサービスで障害が立て続けに発生した時期と重なっていたこともあり、エラー対応でリファクタリングに避ける時間が圧迫されるという苦しさがありましたが、Tech 一丸となり取り組むことでリリースを迎えることができました。結果としてコア機能の障害に起因したトイルが大幅に削減されました。リファクタリングの実施にGOを出してくれた組織に感謝すると共に、リファクタリングを通じて技術負債を解消する機会をしっかり設けることの重要性を実感しました。
学びの多いタスクフォースだったので、振り返りを共有させてもらいました。

明日のアドベントカレンダー藤崎 さん の 「Engineering Manager になって3ヶ月の私が大切にしていること」 です。

Discussion