1. はじめに
こんにちは! 現在、バイトルiOSアプリのリアーキテクトチーム プロジェクトリーダーを務めています白数(@cychow_app)です。
アルバイト・パート求人サービス「バイトル」のiOSアプリは、2021年頃から大規模なリアーキテクトプロジェクトを進めてきました。(私自身はリアーキテクトプロジェクトに2023年頃から参画しています。)
本記事では、iOSアプリ版バイトルにおけるリアーキテクト、主にマルチモジュール化についてご紹介いたします。
2. RIZAP × dip合同勉強会について
本題に入っていく前に、2024/06/07 (金) にRIZAPさんのオフィスにて、RIZAPさんと弊社dipとの合同勉強会「dip×RIZAP iOS/Androidアプリ開発 勉強会」を開催しました。
勉強会では、弊社から@southcloud_7960と @cychow_appが登壇し、以下の内容でLTを行いました。
- 「つまりUI stateとは何なのか?」by @southcloud_7960
- 「バイトルアプリのリアーキテクト事情 ~マルチモジュール化について~」by @cychow_app
本記事はLTでお話しました「バイトルアプリのリアーキテクト事情 ~マルチモジュール化について~」の内容を掘り下げたものとなります。
※「つまりUI stateとは何なのか?」の詳細は以下でもご紹介していますので、ぜひご参照ください!
3. バイトルアプリが抱える課題とリアーキテクトの目的
では、本題に入っていきます。 バイトルのiOSアプリは、2010年にリリースしており、長い歴史をもつサービスとなっています。 アーキテクチャとしてはMVVMを採用しており、保守・運用を続けてきたのですが、長い歴史が故の機能の増加に伴うソースコードの肥大化によって、以下のような問題が浮き彫りとなってきました。
【サービスの問題点】
- ソースコードが肥大化しており、影響調査に時間がかかるため、スケジュールの確定に時間がかかってしまう
- スケジュール確定後も、予期せぬエラーが多発するのでスケジュールの変更が頻繁に発生している
- 結合試験以降で単体試験不足に相当する不具合が発生している。
【実装時の問題点】
- プロジェクトが巨大化し、コード変更による影響が見えづらい。
- Swiftのステップが大幅に増加している。
- 全体が見通せないほど複雑に絡まり合い、密結合となってしまっている。
- 歴史が古いため仕様・経緯の確認に時間を要するコードが多い。
- 10年近い歴史があるコードが存在する。
以上の課題は、簡単なリファクタリングでは到底解決できないほどに膨大なレガシーコードに依存しており、バイトルのiOSアプリ全体の設計を見直す必要がありました。設計を見直すことで、大きく「責務ごとに機能を分割すること (モジュールごとに機能を分割する。)」、「システムアーキテクチャの導入」の2つを行う必要がありました。
そこでサービス全体に対して大規模なリファクタリングを行う上で、以下の目的を掲げました。
歴史の長いバイトルアプリをシンプル化し、全体的なパフォーマンスを向上させ、この先5年、10年耐えられるアーキテクチャに変更し、ROI(投資利益率)を健全化すること。
上記の目的を実現すべく、「責務ごとに機能を分割すること (モジュールごとに機能を分割する。)」に対してはマルチモジュール化を、「システムアーキテクチャの導入」に対してはClean Architectureに基づくアーキテクチャの導入を検討することになりました。
4. マルチモジュール化について
4.1 マルチモジュール化とは
マルチモジュール化とは、サービスを主要機能ごとに切り分け、サービスのソースコードを機能ごとに決まったモジュールに分割することです。
マルチモジュール化を行うことで、以下のような恩恵を得ることができます。
- 単機能ビルドの時間が短縮でき、開発スピードが上がる。
- 過度な依存関係を解消するため、全体を疎結合にできる。
- どこに何の処理を書くか明確になる。
単機能ビルドの時間が短縮でき、開発スピードが上がる。
マルチモジュール化を進めていくことで、モジュールごとにビルドを実行できるようになります。これをSandboxアプリと呼ばれていたりします。
これによって、ソースコード全体的にビルドしていたものが、全体の一部分のみをビルドすることになるため、ビルド時間も大幅に削減することができます。バイトルアプリチームでは、Sandboxアプリを取り入れ、ビルド時間を短縮できるように現在も作業を進めています。
依存関係の解消が必要になるため、全体を疎結合にできる。
大規模サービスであればあるほど、各オブジェクト間の依存関係が密となっている状況、つまり密結合であること多いです。密結合である故にソースコードの変更にも弱い状態となっており、バグの修正の際には影響範囲が大きくなってしまうということや、ソースコード修正時に他の作業者の方とコンフリクトも発生してしまう状況でした。
バイトルのiOSも例外でなく、様々な箇所で使用している汎用クラスが存在していたり、一つのViewクラスに対してViewModelクラスが複数存在していたりなど、一つのオブジェクトに対して、無作為に依存関係を持っているオブジェクトが多数存在している状況でした。
マルチモジュール化を進めていく上で、上記のような依存関係を引き剥がしていき、密結合を解消していきました。
どこに何の処理を書くか明確になる。
大規模サービスを複数人で開発する際に発生する問題として、新たなファイルを作成した時にどのディレクトリに追加するかというものがあるかと思います。
マルチモジュール化を進めていく以前は、新規参画者の方が、どのディレクトリがどの役割を担っており、何の処理を行うファイルかなども含め、キャッチアップすることに大きな工数が必要となっていました。
マルチモジュール化によって、機能ごとのモジュールに分割することで、新規機能のソースコードをどこにいれた方がよいか、バグ修正を行う際に該当するファイルはどこのモジュールに保管されているかなど、よりわかりやすくなりました。
※ 過去にもマルチモジュール関連の記事も掲載していますので、ぜひご参照ください。
4.2 バイトルにおけるマルチモジュール化の方法
マルチモジュール化を実現する方法として、以下のような方法が挙げられます。
- Swift Package Manager (SPM)
- マルチターゲット
- マルチプロジェクト
バイトルアプリでは、ライブラリ管理ツールとしてCocoaPodsを使用していたこと、設計当初SPMに対応していないライブラリが存在していたこと、Xcodeプロジェクト管理ツールとしてXcodeGenを使用していたことから、バイトルアプリでは「マルチターゲットによるマルチモジュール化」を行うように決定しました。
4.3 マルチモジュールの作業工程
バイトルiOSアプリのプロジェクトは、マルチモジュール化以前は、Baitoruという大きなモジュールの中に機能を実装していくようにしていました。マルチモジュール化を進めていく上で、Baitoruモジュールから徐々に機能を引き剥がしていき、複数のモジュールに移動させていくようにしていきました。
また、バイトルアプリチームでは、リアーキテクトプロジェクト以外にも、新機能を開発するエンハンスプロジェクトも並行して複数動いています。そのため、並行プロジェクトとのコンフリクトが発生しないような柔軟に対応が必要となりました。
では具体的に、バイトルアプリチームにおけるマルチモジュール化の作業工程についてご紹介していきます。大きく5つの以下のSTEPに分けて実装を進めています。
■ STEP1 BaitoruモジュールからCoreを切り出す
■ STEP2 Feature Moduleを切り出す
■ STEP3 CoreからComponentを切り出す
■ STEP4 Core ModuleからLibraryを切り出す
■ STEP5 Sandboxアプリとしてビルドできるようにする
現在 (2024/06/26)、STEP2のFeature Moduleの切り出し部分まで完了しており、STEP3のCoreからComponentを切り出すところを考えているのですが、切り出したことによってコストが大きくなってしまう可能性もあるため、慎重に検討しているところになります。
また、STEP5のSandboxアプリの環境を構築することで、単機能ビルドが可能となり、実装や検証時間が大幅に削減することができます。Sandboxアプリの環境をどのように構築するかも含め、検討しながらプロジェクトを進めています。
4.4 実装時の問題点とその解決方法
マルチモジュール化を進めていく上で、様々な問題に遭遇しました。 本記事では遭遇した問題の事例を幾つかご紹介します。
ボイラープレートコードが多い
プログラムやソフトウェア開発において頻繁に使用される標準的なコードのことをボイラープレートコードと呼ばれ、必要以上に多くのコードが含まれていたり、プロジェクトを複雑化させる弊害があります。
バイトルのiOSアプリでは、マルチモジュール化を行う過程で、依存を抽象化するためのインターフェースを作成したり、依存性注入を行うためのコンテナのコードを追加したりなど、構造としてはほとんど同じで繰り返し、記載する必要があるコードが数多く存在していました。
そういったコードを毎回、手動で追加していると非常に多くの時間を費やしてしまい、本来行いたいリファクタリングまで工数的に行えなくなってしまいます。
そこで、バイトルアプリチームでは「Sourcery」というコード自動生成ツールに注目しました。
Sourceryは、Swift Syntaxをベースに構築されたSwift言語用のコードジェネレーターで、stencilというテンプレートファイルをベースにコードを自動生成することができ、ボイラープレートコードをテンプレートファイルとして用意することで、自動で冗長なコードを記述することができます。具体的な用途として、テストコードなどで使用するモックやスタブなどの自動生成、Enumのcaseの自動生成、Codableのオブジェクトの自動生成であったりと、stencilファイルで表せるものであれば、Swiftコードを自動生成することができます。詳しい内容は、Sourcery Referenceをご参照ください。
以上のようなSourceryの特徴を活かし、バイトルアプリチームではEnvironmentやComponent、Builderなどのテンプレートを用意した上で、シェルスクリプト内でsourceryコマンドを呼び出すことで、新規作成したViewやマルチモジュール化に伴うファイル移動対象のViewに対応するSwiftコードを自動生成しています。
※ バイトルアプリ内での詳しい使用方法は過去の記事をご参照ください。
モジュール間を跨ぐ、責務が偏った共通パーツの扱い
DRY原則に基づく場合だと、同じようなコードが増えないという点ではすばらしいですが、マルチモジュール化の場合だと、同じモジュール内で使用するのみの共通パーツであれば、そのモジュール内で管理すればよいため、扱いが簡単となります。ただ、モジュール間で跨いで使用している、かつ責務としては、あるFeatureモジュール内に含めたいというUIパーツが存在したりするため、DRY原則には則らずに、同じようなコードを別モジュールに配置する必要があります。バイトルではこういったUIパーツが数多く存在しており、一旦はCoreモジュール内に配置はしていますが、将来的には各モジュール内で再定義していきたいと考えています。
新規メンバーの立ち上がり
バイトルアプリチームでは、エンハンスチームとリアーキテクトチームに分かれており、リアーキテクトチームのメンバーの入れ替わりも、たまに発生していたりします。そういった新規参画メンバーにも、できるだけマルチモジュール化の概念であったり、リアーキテクト自体の目的を認識していただいた上で、プロジェクトに進めていく必要があり、インプットの工数もエンハンスチームと比べると多くかかってしまいます。シンプルな解決手段ではありますが、私たちのチームでは、ドキュメントをできるだけ用意することを心がけたり、有識者メンバーが新規メンバーにコミュニケーションを積極的にとりにいくことで、できるだけ早く立ち上がれるようにしています。
5. 今後のリアーキテクトについて
バイトルiOSアプリでは、マルチモジュール化プロジェクトの山場でもあったFeatureモジュールの切り出し部分が一旦完了し、全体的にも疎結合な構成にすることができました。
ただ、リアーキテクトプロジェクト自体はまだ終わっておらず、テストコードをより書きやすくすることや、メンバーのコード粒度の統一なども行いたいため、システムアーキテクチャとして、Clean Architectureの概念に基づくアーキテクチャの導入・検証も行なっています。
また、Apple純正ライブラリに置き換えることができるような、サードパーティ製のライブラリも数多く存在しているため、サードパーティ製ライブラリの削除対応なども現在取り組んでいます。
バイトルアプリのリアーキテクトチームでは、「歴史の長いバイトルアプリをシンプル化し、全体的なパフォーマンスを向上させ、この先5年、10年耐えられるアーキテクチャに変更し、ROI(投資利益率)を健全化すること。」という目的を達成すべく、引き続き作業を進めていきます。