集計結果と集計軸を切り離した段階的リファクタリングの記録
はじめに
長く運用されているアプリケーションでは、一つのモデルが徐々に多くの責務を抱え込んでいく。
本記事で扱うのは、「集計結果」と「集計軸」を同時に担っていたモデルを、止めず・壊さずに分割した事例である。
これは置き換えの話ではない。
役割が増えすぎたモデルを、より扱いやすい粒度に整理する話だ。
分割前:一つのモデルが二つの責務を持っていた
分割前、results テーブルは次のような役割を同時に担っていた。
results
- 集計結果(例:スコア、数値)
- 集計軸(例:部署、カテゴリ、単位)
例えば、1レコードが次のような意味を持っていた。
- 部署:営業部
- スコア:A
一見シンプルだが、この構造には問題があった。
何が問題だったのか
この構造では、集計軸の変更がすべて集計結果に影響してしまう。
例えば、
- 部署名の変更
- 部署の統廃合
- 部署の再編成
といった「組織構造の変更」は、本来
- 集計ロジック
- 集計結果そのもの
とは別の理由で発生する。
しかし、集計軸と集計結果が同じテーブルにあることで、
- 部署変更 = 集計結果の変更
- データ修正の影響範囲が広がる
- 意図しない変更が起きやすい
という状態になっていた。
分割後:責務ごとにモデルを分ける
そこで、責務を次のように分離した。
segments
- 部署:営業部
results
- スコア:A
-
segmentsは 集計軸だけを表す -
resultsは 集計結果だけを表す
結果として、
- 部署変更 →
segmentsだけが影響 - スコア再計算 →
resultsだけが影響
という、責務に沿った変更範囲を作ることができた。
それでも一気に分割しなかった理由
理屈の上では、この分割は「正しい設計」に見える。
しかし現実には、
-
resultsはプロダクトの中核 - 常に本番トラフィックを受けている
- 多数の機能から参照されている
ため、一気に分割することはできなかった。
そこで選んだのが、段階的に責務を切り離すアプローチだ。
安全に分割を進めるための全体プロセス
この分割では、参照先の変更とデータ移行を同時にやらないことを最重要原則とした。
そのために、
- 物理テーブル
- VIEW
を併用し、移行をフェーズ分割した。
フェーズ1:分割の足場を作る
results # 集計結果 + 集計軸を持つ既存テーブル
segments_tmp # 集計軸専用の新しい物理テーブル
segments (VIEW) # results から集計軸を見せる VIEW
この時点では、
- アプリケーションの挙動は変わらない
- 新しいテーブルは存在するが未使用
- VIEW は
resultsをそのまま見せる
何も変えていないが、分割できる状態を作った。
フェーズ2:参照先を VIEW に固定する
アプリケーションが参照する「集計軸」を、常に segments(VIEW)経由に統一する。
results
segments_tmp (物理)
segments (VIEW)
- 実体がどこにあるかをアプリケーションは意識しない
- 分割の存在を知らずに動き続ける
この時点で、参照名は固定された。
フェーズ3:集計軸データの分離(BackFill / FrontFill)
参照経路を固定した上で、集計軸データを新しい物理テーブルへ移していく。
-
BackFill
- 既存データから集計軸を抽出してコピー
-
FrontFill
- 新規更新時に、集計結果と集計軸をそれぞれ反映
この FrontFill を共通処理として構築した点が、今回の分割で最も慎重さを求められたポイントだった。
一番神経を使ったポイント:FrontFill
FrontFill では、次の点を特に警戒した。
- 集計結果と集計軸がズレないか
- 既存リクエストのパフォーマンスを落とさないか
- 分割ロジックが通常コードに侵食しないか
そのため、
- 二重書き込みは共通処理に集約
- 通常コードは「集計する」だけ
- 分割の存在を意識させない
という境界を最後まで守った。
フェーズ4:参照実体の切り替え(SWAP)
集計軸データが完全に分離できた後、VIEW と物理テーブルの名前を入れ替える。
segments_tmp (物理) → segments
segments (VIEW) → segments_view_tmp
- アプリケーションコードは変更しない
- 参照名は同じ
- 実体だけが切り替わる
最も危険な変更を、最も軽い操作にした。
チェック・監視・ロールバック
- 一定期間、
resultsとsegmentsの内容が一致しているかを検証 - 差分が出た場合はアラートを発報
- 問題があれば SWAP を戻すだけで即時ロールバック可能
この分割で得た原則
- 分割は「置き換え」ではない
- 責務が違えば、並存させればいい
- 影響範囲が大きい変更ほど、実行手順は単純に保つ
- 分割そのものを設計対象として扱う
おわりに
この取り組みは、results テーブルを捨てる話ではない。
-
resultsは今後も集計結果を担う -
segmentsは集計軸だけを担う
それぞれが自分の理由で変更される世界を作るための分割だった。
同じように
「消せないが、このままにもできない」モデルを抱えている人の
判断材料になれば幸いです!