5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

集計結果と集計軸を切り離した段階的リファクタリングの記録

はじめに

長く運用されているアプリケーションでは、一つのモデルが徐々に多くの責務を抱え込んでいく。

本記事で扱うのは、「集計結果」と「集計軸」を同時に担っていたモデルを、止めず・壊さずに分割した事例である。

これは置き換えの話ではない。
役割が増えすぎたモデルを、より扱いやすい粒度に整理する話だ。


分割前:一つのモデルが二つの責務を持っていた

分割前、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 では、次の点を特に警戒した。

  1. 集計結果と集計軸がズレないか
  2. 既存リクエストのパフォーマンスを落とさないか
  3. 分割ロジックが通常コードに侵食しないか

そのため、

  • 二重書き込みは共通処理に集約
  • 通常コードは「集計する」だけ
  • 分割の存在を意識させない

という境界を最後まで守った。


フェーズ4:参照実体の切り替え(SWAP)

集計軸データが完全に分離できた後、VIEW と物理テーブルの名前を入れ替える

segments_tmp (物理) → segments
segments (VIEW)     → segments_view_tmp
  • アプリケーションコードは変更しない
  • 参照名は同じ
  • 実体だけが切り替わる

最も危険な変更を、最も軽い操作にした。


チェック・監視・ロールバック

  • 一定期間、resultssegments の内容が一致しているかを検証
  • 差分が出た場合はアラートを発報
  • 問題があれば SWAP を戻すだけで即時ロールバック可能

この分割で得た原則

  • 分割は「置き換え」ではない
  • 責務が違えば、並存させればいい
  • 影響範囲が大きい変更ほど、実行手順は単純に保つ
  • 分割そのものを設計対象として扱う

おわりに

この取り組みは、results テーブルを捨てる話ではない。

  • results は今後も集計結果を担う
  • segments は集計軸だけを担う

それぞれが自分の理由で変更される世界を作るための分割だった。

同じように
「消せないが、このままにもできない」モデルを抱えている人の
判断材料になれば幸いです!

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?