初めに
こんにちは、マネーフォワードクラウド連結会計(以降、クラウド連結会計)のバックエンド開発に従事しているTaskと申します。 今回は、クラウド連結会計のコア機能を高速化した話と、それが原因で起こった金額の不整合障害から得られた教訓を紹介しようと思います。
本記事内では、前提として簿記2級相当の知識や用語が頻出します。
連結会計とは
まず、クラウド連結会計が扱っている連結会計について説明させてください。 連結会計とは、親会社・子会社など、支配もしくは従属関係にある複数の会社を1つのグループと捉えて、そのグループの決算を行うための会計手続きを指します1。 下の図の「連結グループA」の決算を行うイメージです。
この業務を「連結決算業務」と呼びます。
連結決算を行うことによって、会社の利害関係者(債権者や株主など)は各社単体だけではなく、グループとしての財政状態・経営成績・キャッシュフローの状況を知ることができます。
本記事では、「(複数の)グループ会社の残高試算表」と「連結仕訳」を入力、「連結財務諸表」を出力する手続きとして取り扱います。
クラウド連結会計での連結財務諸表作成
クラウド連結会計では、ユーザーへのシームレスな情報提供を実現するため、連結財務諸表を更新しうる操作(例えば、グループ会社の残高試算表取り込みや、外貨の換算レートの更新)をユーザーが行った際に、そのまま連結財務諸表が更新される、という機能があります。 以降、この機能をコア機能と呼びます。 また、連結財務諸表を更新しうるユーザーの操作をコア操作と呼びます。
例えば、子会社の残高試算表取り込みを行った場合、まず残高試算表から財務諸表を作成し、以下の処理を行いそれを更新します。
- 個別修正の適用
- 報告通貨を連結通貨に換算
- 当期純利益の計算
- 換算修正の適用
- 為替換算調整勘定の計算
- 為替差損の計算
最後に、全てのグループ会社の財務諸表と連結仕訳から連結財務諸表を作成します。
「連結財務諸表の作成の前段階として、取り込んだ残高試算表に対して複数の処理を適用しているんだな」くらいに考えていただいて構いません。
クラウド連結会計のバックエンドでは、1 -- 6までの処理をそれぞれ以下のように設計しています。
例: 当期純利益を計算するクラス
@Service class 当期純利益計算サービス( 財務諸表リポジトリ, 換算修正適用サービス, ) { fun 計算する(id) { val 財務諸表 = 財務諸表リポジトリ.selectById(id) // 当期純利益の計算を行う val 当期純利益計算済み財務諸表 = ... 財務諸表リポジトリ.update(当期純利益計算済み財務諸表) // 次の処理の呼び出し 換算修正適用サービス.適用する(id) } }
つまり、それぞれの処理に対して、
- DBからの財務諸表の取得
- 計算処理
- 計算した財務諸表をDBに保存
- 次の処理の呼び出し
という方針で設計していました。 この方針により、それぞれの処理の独立性を保ち、DBまでを含めた自動テストを書ける、というメリットがありました。
勘が鋭い方ならお気づきかも知れませんが、これはつまり財務諸表を作成するために必要な6つの処理全てにおいて、財務諸表のDBからの取得と保存が1回ずつ実行されるということです。
また、全ての子会社に対して影響があるコア操作(例えば、外貨の換算レートの更新)をユーザーが行った場合、全てのグループ会社の財務諸表に対して同様の操作を行う必要があるため、DBの呼び出し回数がグループ会社の数だけ倍になります。
リリース直後のデータ量が少ない状態ではこの処理でも問題ありませんでした。 しかし、リリースして1年ほど経ったタイミングで、以下の理由からこのコア機能の高速化を行うという意思決定がなされました。
- 今後、クラウド連結会計をより規模が大きい会社にも対応できるようにする。
- データ不整合を防ぐために、コア操作の処理中は他のコア操作を受け付けないようにしている。つまり、ユーザーは連結財務諸表の更新が完了するまでコア操作を行えない。ユーザー体験を損なわないように、このコア操作を行えない時間を短くしたい。
DB呼び出し回数の最小化
高速化には様々なやり方があると思いますが、今回は先述した問題点を踏まえて、DBの呼び出し回数を減らすという方針を取りました。 具体的には以下の手順になるように修正しました。
- コア操作の処理開始時に、対象の財務諸表をDBから一括取得しておき、メモリに保存しておく
- コア操作の処理を、メモリから取り出した財務諸表に対して行い、終了したらメモリに再度保存する
- コア操作の終了時に、更新された財務諸表をDBに一括保存する
変更をDBではなくメモリで持つことの懸念点として、データ更新の競合があります。 しかし、前述の通り「コア操作の処理中は他のコア操作を受け付けない」という設計をしているため、コア操作の処理中にDBの財務諸表を更新する処理は入りません。 ですので、メモリに保存しておくという方針を取っても問題ありませんでした。
高速化は目論見通り進み、これまで1分ほどかかっていた処理に関しても、数秒で完了できるようになりました。
計算ロジックには一切手を加えていませんでしたが、万全を期すためにフィーチャーフラグ2を利用し、この修正の公開、非公開を容易に行えるようにしました。
金額不整合障害
この高速化のリリースを終え、一週間ほど経ったある日、事件が起きました。 複数のユーザーから以下のような問い合わせがあったのです。
「連結財務諸表に表示される金額がおかしい」
私たちの環境でお客様の状況を再現してみると、確かに金額がずれていました。 会計システムにおいて正しくない金額が表示されることはとても大きな問題ですので、大慌てで修正を非公開にし、被害の拡大を抑えました。 変更を素早く巻き戻せたことで影響範囲を最小にできたことは不幸中の幸いでした3。
今回の高速化はDBの呼び出し回数を減らしただけで、計算ロジックには一切手を加えていませんでした。 ですので、なぜ金額の不整合が起きたのか開発者として不思議でした。
今回の修正箇所で金額不整合を起こしそうな箇所の見当がつかなかったこと、 コア機能なので広範囲で利用されていたこと、 また、影響が大きかったため慎重な調査を要したことが重なり、原因調査は難航しましたが、その末に原因が判明しました。
原因
原因としては、「当期純利益という勘定科目に対応する金額が財務諸表中に2つ存在しており、修正前はDB保存時に1つになるようになっていた。しかし、今回の修正により財務諸表データをメモリで持つようにしたのでその処理が行われず、その後の当期純利益の金額を利用する処理で金額不整合が発生した」というものでした。
詳しく説明します。 まず、コード上では財務諸表は「勘定科目と金額のタプル」のリストを持つエンティティとして表現されています。 ビジネスルールとしては、財務諸表中の各勘定科目の金額は1つのみですが、当期純利益を計算する処理にミスがあり、本来ならば「当期純利益のタプルが単体財務諸表中に存在する場合は金額を更新、そうでなければ追加」とすべきところが、「当期純利益のタプルを常に新規に追加する」という処理になっていました。 したがって、すでに当期純利益のタプルが財務諸表中に存在した場合、当期純利益の計算処理後に、財務諸表エンティティ中に当期純利益のタプルが2つ存在する状態になってしまっていました。
このままでは修正前でも金額不整合が起きるのですが、修正前では、この後、財務諸表を一度DBに保存する処理が入ります。
保存処理では、MySQLのon duplicate key update
処理を用いており、すでにレコードが存在する場合は更新する処理になっています。
このことから、財務諸表エンティティ中に当期純利益のタプルが2つ存在しても、DB保存時にはリストの後ろにあるタプル(既存の金額ではなく、新しく計算された金額)が保存されていました。
そして、当期純利益計算以降の処理は、DBから単体財務諸表を再度取得しているので、新しく計算された金額のみが当期純利益としてDBから取得され、結果として正しく金額が計算される、ということでした。
ところが今回の高速化で、DBの保存処理を最後の1回のみに修正したため、当期純利益の計算処理以降も、当期純利益のタプルが複数存在する状態となってしまい、金額不整合につながってしまいました。
教訓
今回の障害を受けて、私たちのチームでポストモーテムを実施し、以下の点を深掘りしました。
なぜ検出できなかったのか
まず、なぜ一般公開前に今回の金額不整合に気づけなかったのかについて深掘りしました。
その結果、E2Eテストを含む自動テストに関して当期純利益のタプルがすでに存在する財務諸表に対して、当期純利益の計算処理を行う、というテストケースが不足していたことが分かりました。 一方で、実際の業務では、当期純利益のタプルが存在している財務諸表に対して当期純利益の計算処理が行われることは多々あります。 このことから、実際の業務を自動テストに反映できていなかったと痛感しました。
また、実際の業務に則った操作を行うテストは、新機能実装時に実施されていましたが、今回の対象はコア機能の高速化であり、計算ロジック自体には変更がなかったため、不必要だと判断してしまいました。
このことから、実際の業務に則ったシナリオテストを、自動テストとして用意しておく、という教訓を得られました。
なぜ混入してしまったのか
次に、なぜ今回の障害の原因(財務諸表中に特定の勘定科目の金額が二重で存在している)がコードベースに混入してしまったのかについて深掘りしました。
話し合った結果、「財務諸表中では勘定科目は重複しない」というビジネスルールを、コードに落とし込めていない、という結論に至りました。 私たち自身は、そのビジネスルールは理解していたのですが、実装時にそれが却って「この財務諸表は重複した勘定科目を持っていないだろう!」という思い込みに繋がってしまいました。
このことから、ビジネスルールをコードに反映させ、不正な状態を防ぐことの重要さを身をもって体験しました。
一方で...
今回の障害において、フィーチャーフラグを用いて迅速に障害状態を切り戻せたのは、フェイルセーフの観点として良い判断だったと思います。
フィーチャーフラグを利用していなかった場合、リバートコミットを作成し、それをデプロイする必要がありました。 あるいは、修正が完了するまで、クラウド連結会計をメンテナンスモードにして、ユーザーが操作できない状態にする必要があったかもしれませんでした。 いずれにせよ、ユーザー影響を最小に抑えることができたので、改めてフィーチャーフラグを利用するメリットを実感しました。
終わりに
今回は、クラウド連結会計においてコア機能を高速化した話、そしてそれが発生した障害から得られた教訓を紹介させていただきました。
今回の障害で得られた教訓を生かし、今後も、プロダクトのリスク管理をしつつ、引き続き内部品質の向上や速度改善に取り組んでいきたいと思います。 それが、結果的に、ユーザーに素早くかつ継続的に価値を届けていくことに繋がると私たちは考えています。
最後になりますが、マネーフォワード関西開発拠点では、「Give it a try!」を掲げており、共に日々失敗、そして成長していけるメンバーを募集しています! 興味がある方は、ぜひカジュアル面談からご応募ください。