レガシーコードに挑む!kintone Androidチームが実践した段階的リファクタリングの道のり (第1回:モジュール再分割とテスト自動化)

はじめに

こんにちは!kintone開発チームのAndroidエンジニアのトニオ(@tonionagauzzi)です。この記事は、CYBOZU SUMMER BLOG FES '25の記事です。

kintoneのAndroidアプリは2019年のリニューアル以降、技術の進化やビジネス要件の変化に対応するため、継続的な改善が求められてきました。しかし、長年の開発で蓄積されたレガシーコードや複雑なデータフローが、新機能開発や新規メンバーのオンボーディングの障壁となっていました。

本連載では、私たちkintone Androidチームがどのようにして段階的なリファクタリングを実践し、アプリの保守性・再利用性を高めたのか、その道のりと工夫について全3回でご紹介します。

第1回となる本記事では、リファクタリングの前半にかけて力を入れたモジュール再分割手動テストの一部自動化について詳しく解説します。

リファクタリング前の課題

まず最初に、なぜリファクタリングをしようと思ったのか、前段の背景に触れます。リファクタリング前の課題はいくつかありました。

  1. 新機能開発に想像以上の工数がかかる
  2. 新規メンバーが技術に慣れるまでに想像以上の時間がかかる
  3. データフローの追跡が困難

それぞれ、詳しく説明していきます。

1. 新機能開発に想像以上の工数がかかる

既存のコードは、RxJava、独自のViewModel、画面遷移ロジック、そして多くのシングルトンインスタンスを抱えていました。また、モノリシックに近いモジュール構成でした。そのため、以下の課題がありました。

  • 機能同士が密結合:ある処理を変更すると他の処理にも影響が及びやすく、安全に変更するのが難しいため、実装とレビュー、テストに予想以上の時間がかかっていた
  • 新しいライブラリを導入しづらい:Kotlin CoroutinesやAndroidX ViewModel、Hiltのような便利ライブラリを導入しづらく、最小限に導入すると循環参照などの不整合が起きるため、それらの導入を見送っており、新機能の開発に想定以上の工数がかかっていた

それを強く感じたのが、複数ファイルプレビューと呼んでいた機能の開発でした。1つの場所に添付された複数のファイルをプレビューし、画面内で前後のファイルに移動できる機能のことです。ファイルデータをWebから取得し画面表示するだけでなく、スワイプや拡大縮小のジェスチャー入力にも対応する必要がありました。

要件を聞いた段階では1週間程度でできそうと考えていましたが、実際の作業には約半年かかりました。1週間が半年になったと聞くとびっくりしますが、実際は他チームによるWebインターフェースの実装を待っている期間がありました。しかし、Androidアプリを開発しやすくなり、Webで行っている処理をネイティブ側に移すことができれば、チーム内で解決できることが増えるのではないかと話し合っていました。

チーム内でできることを増やしたくても、モダンなフレームワークと従来のフレームワークや独自のロジックを共存させるのが難しい状態になっており、モノリシックなモジュール構成が開発の足かせとなっていたのです。

2. 新規メンバーが技術に慣れるまでに想像以上の時間がかかる

新規参加メンバーがkintone Androidアプリの開発に慣れたと言えるまで、だいたい3ヶ月から半年程度の期間を要していました。この長期化は主に以下のような要因で起きていました。

  • コードベースの複雑さ:長年の機能追加により、機能間の依存関係が複雑化
  • ドキュメント不足:アーキテクチャの意思決定の背景を伝えるための十分な資料がなく、モブプログラミングによる口頭伝達で補っていた
  • 特定技術スタックの学習コスト:RxJavaおよびそれをベースとした独自ロジックに慣れていないケースが多かった

3. データフローの追跡が困難

慣れているメンバーであってもデータの流れを追うのが難しく、デバッグに時間がかかる状況でした。これは以下のような要因でした。

  • モジュール間の複雑な依存関係:どのモジュールがどの機能のデータを持っているのか把握しづらい
  • 状態管理の問題:複数箇所で状態が管理され、データの変更箇所を特定するのが困難
  • 非同期処理の複雑さ:RxJavaチェーンが複雑に絡み合い、処理の流れを追いにくい

段階的リファクタリングの全体像

私たちが実施したリファクタリングは、以下の施策に基づいて段階的に行われました。

1. 技術と機能を境界としたモジュール再分割
2. 手動テストの一部自動化
3. RxJavaからKotlin Coroutinesへの移行(第2回で解説)
4. 独自ユーティリティクラスの利用最小化(第2回で解説)
5. シングルトンインスタンスの削減(第3回で解説)
6. 一部ViewのJetpack Compose化(第3回で解説)

また、それらの決定を残すため、ADR(Architecture Decision Records)を残すようにしました。

blog.cybozu.io

これにより、設計判断の背景と理由を文書化し、新規メンバーの理解促進や将来の意思決定の参考資料として活用できるようになりました。

本記事では、1.と2.について詳しく取り上げます。

段階的リファクタリングの詳細

1. 技術と機能を境界としたモジュール再分割

モジュール再分割の背景

リファクタリング前のkintone Androidアプリは、巨大なモノリシックに近い構造となっていました。具体的には、次の4つのモジュールで成り立っていました。

再分割前のモジュール構成

リファクタリング前のモジュール構成

モジュール名 役割
app kintone Androidの本体。UIや主要な画面・機能を持つ。
domain kintone Androidのドメイン層。ビジネスロジックやViewModel等の中核ロジック。
slash-android サイボウズのクラウド基盤cybozu.comへの認証を行うためのAndroid用実装。
slash-domain cybozu.comに認証するためのドメイン層。ビジネスロジックやリポジトリのインターフェース等。

:appと:domainの分離は、SOLID原則の依存関係逆転の法則を意識して行われており、domain系モジュールがビジネスロジックを持つ構成にすることで、ビジネスロジックのテスト容易性を高めていました。ビジネスロジックのテストは充実していましたが、UIのテストは不足していました。

また、kintoneの機能とcybozu.comの機能も分かれていました。これはcybozu.comの機能を他のプロダクトと共通で利用しようとした名残でしたが、appの複雑性を下げる役割は一定水準で果たしていました。

このように、当初のモジュール構成にも良いところは色々とありました。

しかし、このモジュール構成の懸念が目立ち始めていました。先述の背景のとおり、機能追加や修正のたびに影響範囲が広がりやすく、調査の際は目的のコードを見つけづらい状態だったのです。新規加入者が全体像を把握するのも困難で、開発効率や品質維持に課題がありました。

そこで、この4つのモジュールを再分割することにしました。

再分割後のモジュール構成

再分割後のモジュール構成は、以下のようになりました。

リファクタリング後のモジュール構成

モジュール名(UI層) 役割
:ui:login ログインに関する画面を持つ。
:ui:filepreview ファイルプレビューに関する画面を持つ。
:ui:pushnotification :プッシュ通知の通知領域への表示処理を持つ。
モジュール名(Domain層) 役割
:domain:login ログインに関するロジックを持つ。
:domain:filepreview ファイルプレビューに関するロジックを持つ。
:domain:pushnotification プッシュ通知に関するロジックを持つ。
モジュール名(Data層) 役割
:data:login ログインに関するデータソースを持つ。
:data:file ファイルに関するデータソースを持つ。
データソースはプレビュー用とは限らないのでfileと命名。
:data:pushnotification プッシュ通知に関するデータソースを持つ。
モジュール名(その他) 役割
app kintone Androidへの入り口として必要最低限の機能を持つ。
log kintone Android内で発生するログを取りまとめ、Firebaseに送信するなどの機能を持つ。
test-shared 複数のモジュール間で共有したいテスト用のユーティリティクラスを配置する。

モジュールは他にもありますが、一部のみ載せています。

再分割で心掛けたこと

GoogleのAndroidアプリアーキテクチャガイドを参考にし、機能ごと、レイヤーごとに分割したモジュールを作成しました。これにより、機能追加や修正の際に影響範囲を少なくすることができ、調査や学習の際に目的のコードを見つけやすくなりました。

また、既存コードの実装をできるだけ変えず、クラスやメソッドをまとまった単位で新モジュールに移動したことで、ロジックの回帰テストを行うコストを低く抑え、安全にモジュール分割しました。

得られた効果

  • モジュールごとにビルド・テスト可能になり、機能ごとの独立性が高まった
  • モジュールごとの責務を明確化できた
  • 新機能開発やリファクタリングをする際、影響範囲が限定されるため、安心してできるようになった

感じた課題

  • 初期のモジュール移動や依存関係の整理は想定通りいかないことが多く、見積もりを超過することがしばしばあった
    • あるクラスを移すとモジュール間の循環参照が簡単に起きてしまう状態で、都度リファクタリングを余儀なくされた
  • リファクタリング前の構成で意識していた「通信先がkintoneか、cybozu.comか」という判別はしづらくなった
    • データソースの在処でモジュールを区切るよりも、機能と技術でモジュールを区切るほうが可読性が高く、総合的に良いと判断した

2. 手動テストの一部自動化

テスト自動化の背景

リファクタリング前の機能テストや回帰テストは、手動テストに大きく依存していました。

ロジック単体の自動テストは充実していましたが、UIの自動テストが不足していたため、仕様変更やリファクタリングをする際、テストエンジニアの手を借りる必要がありました。テストエンジニアを確保しようとすると他チームとの間で調整が発生し、実装のリードタイムが伸びてしまいやすい状態でした。

また、機能テストと回帰テストは項目書ベースで、実装とは別にテスト設計が必要でした。

自動化後のテスト状況

仕様変更やリファクタリングと並行し、UI部分のユニットテストとインテグレーションテストを拡充していきました。また、CIでコミット毎にユニットテスト、レビューApprove毎にインテグレーションテストを自動実行するようにし、コード変更時の安全性担保を強化しました。

自動テストは、以下の優先度に基づき、高、中、低の順番で作成を優先しました。

自動化コスト:少 自動化コスト:大
ユーザー影響:大 自動化優先度:高 自動化優先度:中
ユーザー影響:小 自動化優先度:中 自動化優先度:低

全て自動化できると良いですが、現実はそうはいかないので、自動化コストが高くユーザーへの影響が小さい機能は、手動テストを継続する方針にしました。

得られた効果

  • 実装をコードベースにマージする前にバグに気づくことが増えた
    • リファクタリング前は実装の受け入れ完了後にバグが発覚することがあったが、リファクタリング中の2024年6月から2024年12月までの間、受け入れ完了後のバグは0件だった
  • リファクタリングで機能を壊すのではないかという心理的ハードルが低下した
  • テストを通じて仕様を把握したりデバッグしたりしやすくなった

感じた課題

  • すべてを自動化するのは難しく、手動テストとのバランスを見極めるのが難しかった
    • 自動化に時間をかけすぎたり、テストを作ることが目的にならないよう気をつけた

まとめ

本記事では、kintone Androidアプリの段階的リファクタリングのうち、モジュール再分割と手動テストの一部自動化についてご紹介しました。これらの取り組みにより、開発効率と品質を両立しつつリファクタリングを進める基盤を整えることができました。

次回予告

次回は、再分割した各モジュール内でRxJavaからKotlin Coroutinesへ移行したことと、独自ユーティリティクラスの利用を最小限まで減らしたことについて、実際の進め方や工夫、得られた知見を詳しく解説します。お楽しみに!