こんにちは、技術本部 Contract One Devの小松です。
Contract One Devチームでは、2024年1月から2024年10月の約10カ月間に渡り、契約書更新履歴のリファクタリングに取り組んできました。 本記事では、このリファクタリングの背景から具体的な進め方についてまでを記載しプロジェクト全体を振り返ります。
時期によりメンバーが変動する形でしたが、総勢7名で取り組んだプロジェクトになります。このプロジェクトは多くのメンバーの工夫と貢献によって支えられてきました。そのため、本ブログ記事では、すべての活動をチームの取り組みとして記述します。
背景
そもそもが急ごしらえだった契約書更新履歴の誕生
Contract Oneにおける契約書更新履歴は、契約書に対する更新を「更新者・更新時間・更新内容」のセットで時系列に閲覧できる機能です。各契約書の画面には契約書更新履歴のセクションがあり、そこで更新時間の古い順から表示されます。
契約書更新履歴機能のリリースは、Contact Oneの立ち上げから約1年後に行われました。つまり、サービス開始から約1年間は契約書更新履歴の仕様が未確定のまま、契約書関連の更新データを蓄積していました。一方で、問い合わせ対応や運用のために契約書の更新内容と更新者を把握する必要があったため、どのような要件にも柔軟に対応できるように、スナップショット形式でデータを保持していました。
上記図のように、契約書に関連する各テーブルのスナップショットを保持するため、トランザクションテーブルをミラーする形でスナップショット用のテーブルを作成していました。契約書に対する操作(更新)が発生するたびに、新しいスナップショットを生成していました。このスナップショットには、更新対象のテーブルだけでなく、その更新時点で、更新対象の契約書に紐づくすべての関連データの更新前の値が含まれています。これが後のデータ肥大化や、アプリケーションでの扱いづらさにつながる最大の要因となっていました。
さて、「契約書更新履歴」の実装について話を戻します。スナップショット形式でのデータ保持は、SELECT INSERTを使用するため、保存時には比較的低コストで実装できます。しかし、そのデータから契約書更新履歴を生成する際には、スナップショット間のデータ差分を計算し、「更新者・更新時間・更新内容」を特定する必要がありました。当時の状況は、例えるならば「食材はあるものの調理器具がない状態で料理を作らなければならない」といったような状況で、困難を極めていました。
「契約書更新履歴」のリリース時点でリファクタリングを敢行するという選択肢もあったかもしれません。しかし、当時はProduct Market Fit前であり、次の四半期に自分たちのプロダクトが生き残れる保証がないという状況下では、必要最低限の機能拡充と他プロダクトとの差別化が最優先事項でした。そのため「契約書更新履歴」の実装では、テーブルの構造は変更せず、アプリケーション層で差分計算を行って履歴表示用のデータを生成する方針となりました。
度重なるインシデント
「契約書更新履歴」のリリースから1年の間に、Contract Oneはさらなる進化を遂げました。Contract Oneのコアドメインは、言うまでもなく「契約書」になります。この進化に伴い、「契約書」に関連するドメイン(テーブル)も増加の一途をたどりました。スナップショット形式で履歴データを蓄積していた私たちにとって、これは新しいテーブルが追加されるたびに、それに対応するスナップショット用テーブルも必要になることを意味していました。それにより、神業に近い形で実現されていたスナップショットからの差分計算はより複雑さを増し、最終的には契約書関連のテーブルを作成・変更するたびにバグが発生しやすい領域となり、明確な技術的負債と化してしまいました。
この時点、つまり「契約書更新履歴」のリリースから約1年後の時点において、契約書に関連する機能変更はこの先も多く発生することが予想できました。それに伴い、更新履歴への変更も必然的に生じることは明らかでした。このような状況を踏まえ、私たちは、「契約書更新履歴」に関してリファクタリングを敢行することを決意しました。それが2023年の12月あたりのことで、Contract Oneの最初のリリースから約2年半が経過した時のことでした。
新契約書更新履歴の概要
本リファクタリングでは、差分計算のロジックに加えて、データ保持の方法自体から見直すことにしました。更新履歴のデータは主に契約書の更新履歴表示に使用されており、データのユースケースが明確になっていたためです。このユースケースに必要なデータは、スナップショット形式と比較すると大きく異なっており、これがデータ保持方法を見直す動機の1つとなりました。
また、スナップショット形式でデータを保持するという意思決定に関連して、初期の契約書ドメインのモデリングも同様に広範な定義となっていました。1stリリースから2年半の時を経て、ドメインの境界がより明確になり、整合性を維持すべき領域を細分化できることが分かってきました。これは、トランザクションテーブルにもリファクタリングの余地があることを示唆していましたが、その範囲まで含めるとプロジェクトが肥大化する懸念があったため、履歴関連のテーブルのみを、当時の我々のドメイン理解に基づいて先んじて細分化することにしました。
データ更新のタイミングは、契約書の新規作成・更新の各タイミングで行います。その際、永続化するデータは、スナップショット形式(更新対象契約書の更新前の値をすべて保存)から、実際に更新する項目の前後の値のみを保存する形式に変更しました(以後、差分保持形式と呼びます)。これらの更新前後の値は、before_xxx
、after_xxx
として表現するようにしました。
このデータ構造の変更に伴い、アプリケーションロジックも変更が必要となりました。スナップショット形式の場合、データ変更に伴う永続化処理は比較的低コストで済んでいました。しかし、差分保持形式の場合は、更新操作を永続化するタイミングでその操作の差分を計算する必要があります。そのため、履歴表示のための差分計算ロジックだけでなく、各契約書操作(新規作成・更新)に伴うロジックもすべて修正しなければなりませんでした。ただし、スナップショット形式の差分計算とは異なり、差分保持形式の差分計算は、あくまでその操作前後の差分計算となるため、ロジック自体はシンプルにできました。また、リファクタリング以前に新規作成・更新された契約書については、スナップショット形式から差分保持形式へのデータ移行が必要だったため、バッチ処理の実装も行う必要がありました。
プロジェクトの進め方
冒頭で記載した通り、本プロジェクトは総勢7名で約10カ月かけて実施しました。プロジェクトは大きく3つの工程に分かれており、最初の1-2カ月を設計とプランニングに充てました。この段階では、契約書ドメインとバックエンドの有識者を中心に、技術本部のアーキテクト陣のレビューを受けながら進めました。
差分計算のロジック変更に加えて、契約書の新規作成・更新処理の各ロジックの修正や既存契約書に対するスナップショット形式から差分保持形式へのデータ移行のバッチ作成が必要だったため、実装フェーズでは最も多くのメンバーを配置し、タスクを可能な限り並列で進めました。実装フェーズのメンバーは、Contract One Devの横串チームとして公募により発足し、他の機能開発プロジェクトと兼務する形で進めました。
最後がテスト・リリースフェーズです。契約書の新規作成・更新に関する広範囲な修正を行ったため、テストでは契約書関連の操作をほぼすべてのパターンでチェックする必要がありました。また、リリースについても一度のビッグバンリリースではなく、段階的に実施することにしました。
リリース方法
私たちは、次の5段階に分けてリリースを行いました。
- 履歴表示はスナップショット形式から生成し、データの書き込みはスナップショット形式・差分保持形式の両方に対して実施
- スナップショット形式から差分保持方式への移行処理を実施
- 履歴表示は差分保持方式から生成し、データの書き込みはスナップショット形式・差分保持形式の両方に対して実施
- 履歴表示は差分保持方式から生成し、データの書き込みは差分保持形式に対して実施
- スナップショット形式で使用していたコード・データの削除を実施
1.履歴表示はスナップショット形式から生成し、データの書き込みはスナップショット形式・差分保持形式の両方に対して実施
このリリースは、実装完了後できるだけ早期にリリースすることを目指しました。ここでは、リファクタリング後の変更が本番環境に与える影響が限定的で、デグレがないことが確認できればリリース可能な段階でした。この期間中も各チームは契約書関連の機能開発を継続していたため、スナップショットと差分保持の両形式への書き込み処理をmasterブランチへマージすることで、実装中のコードとのコンフリクトを最小限に抑え、さらにリファクタリング後の実装方式を他チームへ実コードとして早く展開したい背景がありました。
2.スナップショット形式から差分保持方式への移行処理を実施
この段階が一番苦労しました。上述したように、この移行はバッチにより実現しました。冪等性が担保され、バッチ処理実施時に対象の契約書群を指定できるようにしていました。(デフォルトでは全契約書が対象となります。)このバッチ処理に対しても当然テストは書いていたのですが、本番データのバリエーションは多岐に渡ることが想定できたので、スナップショット形式と差分保持形式のそれぞれから生成される契約書更新履歴を比較できるバッチをリリースに合わせて用意しました。この差分比較バッチの結果が完全に一致するまで、バッチ処理と更新履歴表示処理の修正を続けました。
3.履歴表示は差分保持方式から生成し、データの書き込みはスナップショット形式・差分保持形式の両方に対して実施
この段階で、履歴表示は差分保持方式のデータから生成するように変更しました。履歴表示ロジックは、差分比較バッチにより、本番のデータを通してデグレが起きていないことがほとんど保証できていました。そのため、リファクタリング以後に取り込まれた契約書や更新された契約書に対して想定通りの動作をしているかについてテストをし、リリースを行いました。
4.履歴表示は差分保持方式から生成し、データの書き込みは差分保持形式に対して実施 この段階で、スナップショット形式でのデータ書き込みをやめました。ただし、前の段階で履歴表示については、差分保持形式から生成するようにしていたので、ここではデグレが起きていないことを確認し、リリースを行いました。
5.スナップショット形式で使用していたコード・データの削除を実施
最後はいわゆるお片づけになります。スナップショット形式で使用していたコード及びテーブルの削除を行いました。厳密には、コード削除→対象テーブルのリネーム→対象テーブルの物理削除の順で実施しました。
まとめ
まずは何よりやり切ることができて本当によかったと言うのが正直なところです。一方で、当初の想定以上に時間を要した部分もあり、長期プロジェクトならではの課題も経験しました。リファクタリングの主な目的であった履歴表示計算部分の複雑さとバグの問題については、今後の機能開発を通じて改善効果が徐々に明らかになっていくとは思います。実際に、問い合わせ対応時の履歴確認では、以前より直感的にデータを解釈できるようになったと実感しています。一方で、スナップショット形式のためデータが肥大化していた点については、今回のリファクタリングにより確実に解消できたと思います。
最後まで読んでいただきありがとうございました。
Contract Oneでは一緒に働く仲間を募集しています。
詳しくはこちらのリンクから採用情報をご確認いただけると幸いです!
Sansan技術本部ではカジュアル面談を実施しています
Sansan技術本部では中途・新卒採用向けにカジュアル面談を実施しています。Sansan技術本部での働き方、仕事の魅力について、現役エンジニアの視点からお話します。「実際に働く人の話を直接聞きたい」「どんな人が働いているのかを事前に知っておきたい」とお考えの方は、ぜひエントリーをご検討ください。