MoneyEasyのAndroidアプリを大規模リファクタリングした話

はじめに

初投稿となります。株式会社フィノバレーでAndroidアプリを開発している照井(id: hotdrop77)といいます。
私は2018年1月に入社し、以降ずっとMoneyEasyのAndroidアプリ開発を担当してきました。
本記事では、私が入社してから最初に行った大工事であるAndroidアプリのフルリファクタリングについて書いていこうと思います。

入社当時の状況

2017年末にMoneyEasyプラットフォームを使用した最初のサービス「さるぼぼコイン」がリリースされました。
私はこのリリースから1ヶ月後くらいに入社したのですが、当時様々な事情がありAndroidアプリの保守運用は引き継ぎなく私一人で開発することになりました。
この時のAndroidアプリの状態を簡単に表すと

  • FatActivity
  • ドキュメントなし

という「まあよくあるよね」という状態でした。
誤解がないよう補足しておくと、収益が見込めるかまだ分からないサービスに対してはとにかくスピード重視で迅速にサービスをローンチすることが重要だと思うのでこういう状態になってしまうのは仕方ないのかなと思っています。
私自身はエンジニア界隈でも言われている通り品質とスピードは相反するものではないという考えに賛同しますが、一方で「とにかく動くものを今の最適リソースで作るにはこれしかなかった」という事情も理解できるのです。
そんなわけで入社直後はこの状態下で溜まっている様々な課題や機能追加を担当することとなりました。

こんな状態で大丈夫?

私は前職がガチガチのSIerで、Androidといえば趣味&ポートフォリオ用にアプリを作った経験のみでした。(DroidKaigiアプリには毎年お世話になっております。)
そんな私にとって幸か不幸か当時のAndroidアプリのコードは上述の通りViewに「描画処理、ビジネスロジック、Http通信」まで全てが1クラスに集約されており、また使用しているライブラリも最低限であったため構成の把握は比較的容易でした。
中途半端にトリッキーなDIをされていたりオレオレMVXな構成でなかったことは不幸中の幸いだったと思います。
そうはいっても、このコードを今後メンテしていくモチベーションを保つのはなかなか辛いものがありました。
せっかくSIerから転身したしDroidkaigiのアプリを主に見ていた関係でもっとモダンな設計やライブラリを使って開発したかったのです。

そして2ヶ月後

入社して1ヶ月後くらいにDroidkaigi2018が開催されました。2017年まで私は個人参加していましたが、この年から会社のスポンサー枠で参加させていただけることになりました。
ブースの手伝いは最低限で好きなセッションどんどん見て良いよと言われましたが、お金出していただいているしちょっとは手伝いしないと悪いと思って、結局ブースの方が他社のアプリ開発者と話せる機会が圧倒的に多いので半日以上いた記憶があります。
2017年のGoogle I/OAndroidの正式言語にKotlinが含まれたこともあり、当時はよく「そちらのAndroidアプリにはもうKotlin導入しました?」みたいな感じで会話のきっかけを作っていました。
Droidkaigアプリのコードも読んでいてとても面白く、そして羨ましかったことを覚えています。

話はMoneyEasyに戻ります。
比較的大きな機能をリリースした2018年3月くらいでしょうか、開発メンバーから技術的負債返却の声が上がりはじめました。
当然、Androidアプリだけがこんな構成になっている訳ではなくサービス全体が同様の構成になっており、今後需要が高まりそうなサービスでしたので早めに負債を返却することが急務となりました。
この辺りはチーム内でとても色々なやりとりがあったのですが、それだけでこの記事が埋まってしまうので割愛します。 当時サーバーサイドを担当していたメンバーが強烈に後押ししてくれたこととPMが「責任を持ってやってくれれば自由にしていいよ」と理解ある感じだったため自分はAndroidアプリのリファクタリングを行うことになりました。
責任を持ってやるのは当たり前だ!という話もありますが「そんなリスクは許容できない」「全テストが必要なのでダメ」などそもそもチャンスすら与えられない環境が多い中、入社して浅い自分にリファクタリングを任せてもらえたことはとても嬉しかったです。
顧客も金融機関では珍しく技術に理解を示してくれているようで各ステークホルダーに恵まれた環境なのかもしれません。

大規模リファクタリングにあたっての制約

今回のリファクタリングでは一部分ではなく根幹から全て修正することにしました。
リファクタリングを行うにあたっての業務的な制約に加え個人的な制約(誓いに近いです)を念頭に置いて計画を立てました。

  1. 業務的な制約
    1. スケジュールの関係で7末までに完了させること(8月に機能全テストが必要なリリースがあるのでそれに間に合うことが必須)
    2. 既存アプリへの機能拡張&保守運用は最優先で行う
  2. 個人的な誓い
    1. MoneyEasyのAndroidアプリの設計や実装について胸を張って話せるようになる
    2. チームの期待を裏切ることはしない

2-1が私の中で一番大きな要因で、自身が開発しているアプリのことは業務的にもコード的にも胸を張って話したいので出来るだけモダンな設計/実装にしたい想いがありました。
また、PMや開発メンバーが信頼して任せてくれたこととサーバーチームの方々にも設計や方針検討など協力してもらったため、期待通りにやりきることと重大なバグを発生させないことを最優先に考えて取り組みました。

取り組む前の設計/検討

この大規模リファクタリングに当たって考えることは山のようにありました。
コードの改善はもちろんのこと、CI/CDやテストコード導入などやりたいことは多くありましたがリソースとスケジュールの兼ね合いで断念したものも多くありました。
最終的に以下の6項目に分けてそれぞれ設計/検討しました。

  1. アーキテクチャ見直し
  2. 採用言語の検討
  3. 使用ライブラリ検討
  4. リソース設計
  5. ログ設計
  6. エラー設計

これらは一つ一つが1記事書けるくらいになってしまうし、今となってはより良い設計がデファクトスタンダードになっているものもあるため詳細は割愛します。
スケジュールがタイトでしたがいきなりコードを触り始めると絶対失敗するので4月の1ヶ月間はこれらの設計および検討に注力することにしました。
そのため、コードに手を入れる期間はGW明けの5月〜7月末の3ヶ月間となりました。
最終的にKotlinを採用しAndroid Architecture Components(この表現は長いので以下AACとします)を使ったMVVM構成で、各レイヤーの繋ぎこみはRxJavaを使用、DIはDagger2という構成で再構築することにしました。
ちなみに構成はDroidkaigi2018の影響を思いっきり受けています。

具体的な進め方

必要な設計/検討はどれも重要でしたが、それとは別にどうやって進めていくのが最も良いのかを考えることも重要でした。
なにせまだAndroidアプリの仕様全貌はつかめておらず不明な機能もそこそこある状態でした。ドキュメントもテストコードもないため動かして確認するしかありません。
最終的に次の2つを厳守した状態で進めていくことにしました。

  1. 既存アプリとリポジトリを完全に分割
  2. 常に動く状態を保つ

1については既存アプリとそのコードは完全にそのままの状態とし無用な事故を防ぐ目的もありました。
2については本当に細心の注意を払いました。
想定する設計でリファクタリングしてしまうと元コードの状態が跡形もなく消えて無くなるため、ある程度リファクタリングを進めてしまった後にビルドエラーや不思議な挙動でどうにもならなくなった場合、最悪動いていたところまで手戻りが発生し調査に無駄な時間がかかります。
当然、全てリファクタリングした後に動かすなんてリスクが高すぎて取れません。
こういう時テストコードがあれば心がとても安らぐのですがこの状態でテストコードを書くのは不可能に近かかったので(書くならせめてビジネスロジックをViewから分離した後)この時は「常にアプリが動作する状態にする」ことを徹底しました。

「常にアプリが動作する状態にする」にした強い理由がもう一つあって8月頭から全機能テストが始まるため、7末に実装を完了させたところで「アプリを1度も動かしていない状態」ではテスト工程に入れるレベルの品質に達していないのは明白で話になりません。
かといって、7月中旬あたりにリファクタリングを全て終わらせて2週間くらいで動作確認しまくる、というスケジュールも高確率で破綻すると考えました。
それならばチマチマ動作確認をしながら進めることで実装完了とともに簡単な動作確認も完了させてしまうのが最も早そうという結論に達しました。
この進め方にはメリットがあって、この方針でいく場合は機能間の結合度が高いとチマチマ動作確認する意味がなくなりますので可能な限り機能同士が疎結合になることを意識して進めました。
これは改めて意識するまでもなくコード書く上で当たり前のことですが今回は後述するようにある程度元コードを保った状態で進めていくことにしたので油断すると元コードの密結合をそのまま引き継いでしまう恐れがあったのです。それを回避するため改めて意識する必要がありました。

この状態を維持しつつFatActivityからいきなりAACを使用したMVVM構成にするのは難易度が高すぎるので段階的にリファクタリングできるよう4つのポイントを設けることにしました。

4つのポイント

リファクタリング期間3ヶ月を大きく4つのポイントで区切って段階開発していきました。
各ポイントの概要は以下の通りです。

  1. ポイント1: FatActivityからビジネスロジックを切り離しレイヤー構造にする
  2. ポイント2: AAC以外のライブラリ導入
  3. ポイント3: 1機能のみKotlin+AAC対応
  4. ポイント4: 全機能Kotlin+AAC対応

これらはどのポイントでリファクタリングを中断しても一応コードは使える状態であることを意識しています。
6月にいきなり既存アプリへ大きな機能が追加となる可能性も0ではなかったため、その場合は4月〜5月のコストがなるべく無駄にならないように配慮した結果でした。
ポイント4まで到達しないと自分の目的は達成できないのでそういったことがないことを祈りましたが。

ポイント1 FatActivityからビジネスロジックを切り離しレイヤー構造にする

何をやろうにもまずFatActivityだとどうにもならないので、真っ先にやることはViewからビジネスロジックを切り離すとともにクラスやメソッドを意味のある単位で分割しレイヤー構造にすることでした。
具体的には以下のイメージ図のように内部構成を変えました。

f:id:hotdrop77:20200619175138p:plain

このポイント1は5月中旬くらいまでに完了させる予定で行い、ほぼオンスケで完遂できました。

ポイント1での注意点は「内部ロジックは可能な限り変更を加えない」ことでした。
これをやっている間、凄まじくリファクタリング衝動にかられましたが無駄になるかもしれないし余計な混乱を招く恐れが高かったため変数名一つ変えず極力そのままレイヤー分割に徹しました。
ただ、そうはいってもどうしても追加修正しないといけないコードは出てくるのでそういったものは修正しています。
なぜこんな注意点に徹したかという理由ですが、コンパイルエラーやおかしな動作をした時に元コードとの比較が容易になるためです。
たまにおかしな変数がおかしな値を持っていることがあり、そのままにしておくと元コードでの検索が容易になるため何を入れていて何をしていたかの調査が容易でした。
実はSIer時代にこの時とはちょっと違いますが巨大な機能のリファクタリングしたことがあってその時の失敗経験をここで活かすことができました。

ポイント2 AAC以外のライブラリ導入

AACを導入するとViewModelが間に挟まり元コードの名残がだいぶ消えてなくなるので、まずそれ以外のライブラリを順次導入していきました。
ライブラリの選定はとても慎重にやりました。ここはAndroidエンジニアでないと選定が難しいので自分一人で検討することになりますが、ありがたいことにOSS自由に選定して良いという方針でした。
ただ、MoneyEasyはお金を扱うサービスですのでちょっと便利だからという理由でライブラリを入れまくることはしたくありませんでした。
自分がよく知っているもの、デファクトといっていいくらいAndroid界隈では有名なもの、活動が活発でドキュメントが需実しているものなどいくつか基準を設けてかなり絞って選定しました。

導入の際にインパクトが大きかったのはDagger2とRxJavaでした。特にDagger2はApplicationからDIするので部分導入できずほぼ全クラス修正が必要となりなかなか苦戦しました。
また、Dagger2は以前から趣味アプリで使っていましたがComponentsやModuleを理解せず使用していたためこのタイミングで1からDagger2を学習しました。
Rxは自分のお気に入りライブラリトップ3に入るくらい好きで以前から使っていたため学習は不要でしたがこれも導入時は大量に修正が必要でしたので時間がかかりました。
このポイント2までは5末までに完了させる予定でした。実はここでちょっと足が出て6月1週目に食い込んでいます。

ポイント3 1機能のみKotlin+AAC対応

次は1機能だけKotlin+AACで動作させるようリファクタリングしました。
これをやると完全に元コードの面影が消滅するのでエラー解析が今までより面倒になりますが、まずView〜RepositoryまでKotlin化してしまい一度動作確認、それからAACを導入することでUseCase〜Repositoryは動作確認できている状態になるため調査範囲をViewModel&Viewに限定できて効率化できたかなと思います。
Kotlinらしいコードに書き換えるのはこの後にやりました。
ポイント3は6月中旬くらいまでかける想定でしたが、ポイント2でちょっと足が出ていたため予定より期間はありませんでした。
ただここはちょっとバッファを持っていたためリカバリでき、当初予定通り6月中旬ちょっと前にこのポイントを終えることができました。

f:id:hotdrop77:20200619175314p:plain

この最終構成はDroidkaigi2019などで弊社ブースで配布していたポストカードにも載せています。

f:id:hotdrop77:20200619175547j:plain

ポイント4 全機能Kotlin+AAC対応

ここからは7末までひたすら全力全開で全機能リファクタリングをしていきました。当時は大雑把に分割しても25機能でだいたい70画面くらいあったと思います。
ポイント3で大体やり方がわかったのでこのポイントは比較的すんなり進めたのですが、それとは別に「KotlinがJavaと100%互換である」という素晴らしい特性を持っていたことですんなり進めたと思います。このおかげで1機能ずつ「リファクタリング→動作確認」という手段を取れました。

ポイント4は無事7末で完了でき、動作確認も逐一行なっていたためそのままテスト工程へ突入しても差し支えない程度の状態にすることができました。
実際この後、8月に全機能テストをしていただき無事にリリースすることができました。
当然テストではバグがかなり出たのですが致命的なものはほとんどなく、またテストをしっかり行なっていただけたこともあってリリース後に大きなバグも発生しませんでした。

まとめ

細かい点はまだまだ多く苦労した点も面白かった点もたくさんあるのですが、それを書くと長編になってしまうのでまたの機会にしたいと思います。
リファクタリングはエンドユーザーにはほとんど見えない部分ですが実はTextFieldなどはMaterialDesignに準拠して改善していたので所々UIに小さな変化を加えていましたし、全体的にかなり性能改善をすることができました。
特に起動周りとお店一覧は劇的に改善させたのでPMや開発メンバーは気づいてくれて嬉しかったです。

自分にとってこのリファクタリング作業はとても大きな成果でした。
強制的に全コードを読む羽目になったためAndroidアプリの全容を知ることができ、可読性も凄じく向上したためその後の開発効率が格段に上がりました。

それから少ししてAndroidエンジニアがもう一名参画し、ユニットテスト/UIテスト、CI環境などを整備してくれて開発効率がさらに上がりました。
やはり一人では限界があるのでとても感謝しております。

この大工事からそろそろ2年が経ち、少しずつ当時の設計や導入ライブラリが劣化していると思っています。
ライブラリは小まめにバージョンアップしているものの影響範囲が大きいものはなかなか入れ替えが難しいのが現状です。
しかし、継続的なリファクタリングはサービスの寿命も伸ばしますしエンジニアのモチベーションにも開発効率の向上にも繋がるはずです。
次の大きなバージョンアップではCoroutineを導入する予定で、これからも継続的なメンテナンスは大事にしていきたいと思います。