序文
こんにちは、私達は「かけたくないコストを削減し、クラウドワークスをスケールさせる環境をつくる」ことを目的としているチームです。 今回は半年以上かけてやってきた施策である「決済機関上への返金」の仕組みの導入とそれに伴うDBテーブルの段階的な切り替えについて効果や工夫した点など紹介します。
なぜ「決済機関上への返金」を導入したか
今までクラウドワークスではユーザーからお支払いいただいたお金を返金する場合、クレジットカード等の決済機関経由の支払いの返金も、ユーザーの銀行口座に対してクラウドワークスから出金していました。 このような仕組みだと、決済機関経由で支払ったユーザーが返金を受け取りたい場合、出金の申請を行う必要があるという課題がありました。 この課題を解決するために、「決済機関上への返金」の仕組みの導入施策に取り組みました。 (具体例:「クレジットカードで支払ったお金は、クレジットカード決済会社経由で返金するようにする」です)
施策に取り組む上で出た改善案
この施策を導入すると返金の仕組みが増えることになります。 既存の仕組みに新しい概念を導入するにあたって、既存の返金テーブルに「銀行口座への返金」と「決済機関上への返金」の区別がつくような仕組みを追加する必要が出てきました。 しかし、既存の返金テーブルは複数の役割・機能を担っており、更に役割を増やすと複雑になると考えました。 そこで、既存の返金テーブルから役割を剥がして、「銀行口座への返金」と「決済機関上への返金」に分けて、それぞれのテーブルの役割をよりシンプルにしようと考えました。 ざっくりとですが、施策の前後のイメージは以下のようになります。 ※ 詳細情報は省いています。
施策前
施策後
対応の時系列
施策を実施するに当たり、大きくフェーズを2段階に分けて実施しました。
- フェーズ1(3ヶ月)
- チーム内で施策の前提知識を揃えた
- 移行先のデータ構造の追加
- 並行稼動ロジックの追加
- シナリオテスト
- フェーズ2(5ヶ月)
- 並行稼動ロジックの削除
- 移行したデータ構造の削除
以降で詳しく説明していきます。
準備期間の設定
返金およびその周辺に関連する操作や概念に関して各メンバーの保有知識・経験のばらつきがかなりあったため、チーム内で返金に関する前提知識をそろえるための準備期間をもうけました。 具体的には、返金が発生するユースケース (16 件) をすべて洗い出し、それぞれのユースケースに対応する返金データ生成の内部処理を簡易フローチャートとして書き出して相互レビューのかたちで確認をしました。
返金が発生するユースケース
# | 仕事の依頼形式 | 状況 |
---|---|---|
1 | 固定報酬 | 条件変更 |
2 | 固定報酬 | 契約途中終了 |
3 | 固定報酬 | 仮払有効期限 |
4 | 固定報酬 | 報酬キャンセル |
5 | 時間単価 | 差額分返金(全額返金) |
6 | 時間単価 | 差額分返金(部分返金) |
7 | 時間単価 | 報酬キャンセル |
8 | コンペ | 仮払有効期限 |
9 | コンペ | 報酬キャンセル(全額返金) |
10 | コンペ | 報酬キャンセル(部分返金) |
11 | コンペ | 下書き状態で仮払い |
12 | コンペ | コンペ途中キャンセル(全額返金) |
13 | コンペ | コンペ途中キャンセル(部分返金) |
14 | タスク | 報酬キャンセル |
15 | タスク | 差額分返金(部分返金) |
16 | タスク | 下書き状態で仮払い |
並行稼働の工夫
本施策によりデータ構造を変更する必要がありました。 たかがデータ構造変更、されどデータ構造変更ということで変更をどのように行なったかを書いていきます。
どんなデータ構造の変更か
銀行口座への返金テーブル を作成して 返金テーブル のカラムを一部移動させます。構造の変更自体は単純です。 ※ 他テーブルとの関連は下記図より複雑ですが、今回はカラムの移動のみに焦点を当てています。
データ構造の変更
データ構造を変更するにあたって
データ構造を変更する前に調査とデータ構造の変更プロセスを考えました。
調査
あらゆるメソッド内で利用しているロジックと 返金テーブル に対応して作成されたモデルクラス内で対応が必要な箇所を調査しました。主に削除するカラムを参照している箇所です。 返金テーブル の移動するカラムが使われている箇所をリポジトリ内から洗い出します。 検索条件で悩みましたが、 返金テーブル 名と 返金テーブル の単数系の名前を検索するようにしました。 検索結果は「2,227」件でした。 テストファイルは移動作業中のCIで落ちたら修正するという方針にしたので検索結果から除きます。 検索結果以外ではポリモーフィック関連を使って、別名で参照している箇所を探しました。 返金テーブル のモデルクラスのメソッドやバリデーション、ActiveRecordのスコープ、他テーブルとの関連付けを合わせて、修正箇所は「162」件まで絞り込むことができました。
データ構造の変更プロセス
データ構造の変更において一度のリリースにまとめてしまうと影響範囲が大きいので、問題が起きたときに調査や切り戻しが大変になります。そのため、細かくリリースを行うことで影響範囲を小さくし、安全にリリースを行いたいと考えました。 これを実現するために、テーブル構造の並行稼動をする決断をしました。
下記の図の通り、並行稼働期間を設けて 返金テーブル 、 銀行口座への返金テーブル で一定期間同じカラムが存在するようにしました。 この並行稼働期間でメソッドやロジックやデータ移行を行い、準備が整い次第 返金テーブル からカラムを削除するようにしました。 ※ 背景塗りつぶしてある箇所が移行対象カラムです。
テーブル構造の並行稼動
この並行稼働期間のことを考慮して下記の変更プロセスをチーム内で決めました。
- 銀行口座への返金テーブル を追加、それに伴い対応するモデルクラスの作成
- 返金テーブル のレコード作成・更新ロジックに 銀行口座への返金テーブル にも同期するように修正
- 返金テーブル から 銀行口座への返金テーブル へ過去データの移行
- 返金関連クラスのロジック複製と切り替え
- プロセス2 で対応した箇所で 返金テーブル の作成・更新ロジックを削除
- 返金テーブル のカラムを削除
1. 銀行口座への返金テーブル を追加、それに伴い対応するモデルクラスの作成
新規にテーブルを作成するのは他テーブルに影響がないため、日中に行いました。 それに併せて対応するモデルクラスも作成しました。
2. 返金テーブル のレコード作成・更新ロジックに 銀行口座への返金テーブル も同期するように修正
このタイミングで 銀行口座への返金テーブル の作成・更新を始めました。 プロセス3 のデータ移行を先に行なってしまうと、データ移行後からこの対応までの間に 返金テーブル の作成・更新処理が行われる可能性があり、値にずれが生じてしまうため プロセス3 より先に行いました。 また、作成ロジックよりも先に更新ロジックのリリースをしました。 理由は、作成ロジックがリリースされてから更新ロジックのリリースを行う間に 返金テーブル の更新処理が行われた場合に、 銀行口座への返金テーブル の値が更新されず不整合が起きてしまうからです。
3. 返金テーブル から 銀行口座への返金テーブル へ過去データの移行
データ移行対象のレコードの件数が約70万件ほどありました。 この70万件のうち、今後カラムの値が更新されうるレコードと不変なレコードで分けることにして、それぞれ20万件と50万件になりました。 不変な50万件は何日かに分けて1日数回データ移行を行い、更新されうる20万件は深夜メンテナンスでデータ移行を行いました。 データ移行は移行用のスクリプトを作成して実施しました。
4. 返金関連クラスのロジック複製と切り替え
調査結果で修正必須な箇所で 返金テーブル のカラムを参照している場合は 銀行口座への返金テーブル のカラムを参照するように変更しました。 また、 返金テーブル のモデルクラス内のメソッドやActiveRecordのスコープ、他テーブルとの関連付けは 銀行口座への返金テーブル へ移行しました。 複数のプルリクエストでリリースを行いましたが、安全に移行するために移行の方法にも一手間加えました。 「移行」ではなく 「複製して(1週間以上経過してから)削除」 の方が適切な表現かもしれません。 移行をしてから旧メソッドがどこかで参照されていた場合、エラーが起こる可能性や最悪なケースだと誤った値を画面に表示してしまう可能性があります。 問題が起きる可能性を少しでも減らすために、一度新ロジックを作成して1週間以上経過して旧ロジックが利用されていないことを確認できてから旧ロジックを削除しました。 旧ロジックが利用されていないことを確認するために、ロジック内に専用のロガーを仕込みました。 未使用メソッドかどうか検証するためのUnusedMethodLoggerクラスが元々用意されていました。
class UnusedMethodLogger # # 未使用メソッドが使用されたことを Rollbar に通知する # # @param [String] class_name クラス名 # @param [String] method_name メソッド名 # def self.post(class_name:, method_name:) Rollbar.info( "unused method has been used. #{class_name}\##{method_name}", class_name: class_name, method_name: method_name ) end end
引数にクラス名とメソッド名を指定して、このメソッドが呼ばれた場合にRollbarというサービスを経由してSlackへ通知される仕組みです。 一度もSlackに通知されないまま移行が無事終わりました。
5. プロセス2 で対応した箇所で返金テーブルの作成・更新ロジックを削除
返金テーブル のレコード作成・更新ロジックを削除しました。 その後、削除するカラムのsetterにUnusedMethodLoggerを仕込み、通知がこなかったのでカラムを削除できる確証を得ました。
6. 返金テーブル のカラムを削除
返金テーブル のカラムを削除するマイグレーションファイルを作成し、一度すべてのRSpecを実行してエラーが起きないことが確認できたので、カラムを深夜メンテナンスで削除しました。
今回のデータ構造の変更では影響範囲が想定以上に広かったので慎重にならざるを得ず、当初見積もりよりも多くの時間を要してしまいましたが、並行稼働をしたことで安全にデータ構造の変更が行えました。
シナリオテストの実施
実装が終わりリリースの目処がついた段階で、シナリオテストを実施しました。 RSpec を用いた単体テストに加えて実際に返金までの流れをシミュレーションし、画面遷移や表現に問題・違和感がないかを確認することが目的です。 シナリオ作成とその実施はプロダクトオーナーを含めたチーム全員で手分けをして行い、施策開始前に行った「ユースケースに対応した簡易フローチャート作成」のおかげでスムーズにシナリオ作成ができました。 また、シナリオテストの作成者と実施者を別にすることで、シナリオ(=仕様)の理解やシナリオ作成のナレッジ共有が図れました。
その他付随する作業として
集計などに使っているクエリの修正
クラウドワークスではデータウェアハウスとして Amazon Redshift を利用しています。サービスのDBと同期をとっていて、Redshift上でスクリプトによる集計が行われたり、マネージャーやプロダクトオーナーが分析のために Redash でSQLを書いて利用したりと重要な役割を担っています。 今回行った対応ではデータ構造が大きく変更となったためあわせて Redshift 側にもテーブル追加とカラム削除の修正を行い、さらに集計・分析に使っているクエリも修正が必要になるなど、サービス外にも影響が及びました。 事前にある程度把握はできていたので大きな問題にはなりませんでしたが、Redshift もしくは Redash の利用者への連絡と修正の依頼やスクリプトの修正など気にしないといけないことが予想以上にあったことは学びになりました。
大変だったこと
共通言語(ユビキタス言語)がなかった
本施策の開始時点では返金およびその周辺に関連する操作や概念に関して各メンバーが使用している名称・呼称がバラバラで、チーム全体の共通言語が確立できていない状況でした。 そのまま設計・実装作業を始めてしまうとその過程で誤解が生じたり共通認識の形成が阻害される危険性もあったことから、 ご利用ガイド や よくある質問 などの公開文書を参照しながらユーザーサポートチームやプロダクトオーナーにもヒアリングを実施し、チーム内で議論を重ねつつ共通言語を策定していきました。
要修正箇所・影響範囲が大きかった
クラウドワークスのサービスにおける基幹部分に含まれる返金、およびその周辺に手を入れることになる本施策は要修正箇所・影響範囲が大きくなることが施策実施前から予想されていました。 実際、本施策の開始時点で影響範囲のあたりをつけるためにソースコードに対して キーワードで grep をかけたところ、前述 のように 2,227 箇所も該当しました。 また、本施策の開始時点のソースコードでは返金クラスで ActiveRecord の after_create callback を便利に利用していたのですが、当該処理を修正・変更するための修正 プルリクエストは (テストファイル等も含めて) 54 ファイル、 追加: 2,591 行 / 削除: 416 行 (合計: 3,007 行) という巨大なものになってしまいました。 開発フェーズを分割して段階的に施策を進めることで、なんとか確認可能な範囲にタスクを切り出して施策を進めることができました。
クラス構造・テーブル構成のアイデアが噴出した
本施策ではクラウドワークスの返金処理の根幹機能に手を入れるにあたって、テーブル設計・対応コスト・過去データの扱いを慎重に検討する必要があることから、試作初期に開催した検討会ではその変更方法に関して各メンバーからアイデア・意見が噴出しました。 特に今回は段階的に施策を進めることとなったため、クラス構造・テーブル構成の変更も段階的になることから、その中間段階の構成含めていろいろなアイデアが議論されました。 Miro を使用して各自のアイデアを共有・修正しながら各案のメリット・デメリットに関して議論を重ねて、納得のいくかたちで最終的な設計をチーム内で合意することができました。
関係部署との調整
本施策では返金のユーザー体験に大きな影響があることから、問い合わせへの回答方法含めてユーザーサポートチームとは事前の擦り合わせを行いました。 また、返金方法が新規に追加されてお金の流れも変わることから月次で実施しているお金周りの集計にも項目追加というかたちで影響があったため、経理チームとも事前の相談および集計方法の変更に関する擦り合わせを行いました。
施策の結果
この施策の目的であった、ユーザーの返金に関するコスト削減は次の通り達成できました。
返金由来の出金申請数がピーク時の約30%に減少
銀行口座への返金数がピーク時の約15%に減少
月間の返金額の約半分が支払い元決済機関から返金された(ユーザーの出金の手間が減っている)
また、ユーザーの返金に関するコストが減ったことで、社内の出金対応運用コストも10%に減りました。 ユーザーと社内運営チームともにメリットとなり、返金のあり方に納得感もでる施策だったと思います。
We're hiring!
クラウドワークスでは技術的負債の解消に興味があるエンジニアを募集しています! 興味のある方はこちらからご応募ください。