【書き起こし】メルペイのスケーラビリティを支えるマルチモジュール開発 – 上田 雅道【Merpay Tech Fest 2021】

Merpay Tech Fest 2021は、事業との関わりから技術への興味を深め、プロダクトやサービスを支えるエンジニアリングを知れるお祭りで、2021年7月26日(月)からの5日間、開催しました。セッションでは、事業を支える組織・技術・課題などへの試行錯誤やアプローチを紹介していきました。

この記事は、「メルペイのスケーラビリティを支えるマルチモジュール開発」の書き起こしです。

上田雅道氏:「メルペイのスケーラビリティを支えるマルチモジュール開発」というタイトルで発表させていていただきます。Product Engineering iOSチームの上田雅道と申します。よろしくお願いします。

まず、自己紹介ですけれども、今、Product Engineering iOSチームに所属しています。約3年前の2018年にメルペイに入社して、メルカリとメルペイの連携部分であったり、旧メルペイのトップ画面の開発を行ってきました。

本日の内容になります。まず、マルチモジュール開発について話しまして、そのあと、実際にメルペイがどういう構成になっているかについて話します。具体的にメルペイのモジュール間連携の仕組みを詳しく解説したあと、マルチモジュールを活用した開発プロセスについてご紹介いたします。

マルチモジュール開発について

まずは、マルチモジュール開発についてですけれども、ここでモジュールというのは、フレームワークやライブラリのことを意味します。マルチモジュール開発というのは、複数のフレームワークやライブラリを組み合わせてアプリを開発することを指します。

アプリの構成なんですけれども、こちらはよくあるアプリの構成かなと思います。まず1つのアプリターゲットがあって、例えばCocoaPodsやCarthageのなどのパッケージマネージャーを使ってサードパーティのライブラリを入れて、アプリをつくるというモノリシックな構成というのが、まず一番簡単な形であるかなと思います。

次に、アプリケーションを各層に分割する構成もあるかなと思います。例えば共通で使うUIコンポーネントだとか、モデル層の部分とかというのを別ターゲットに切り出すという形です。こういうふうな構成にすることで、層が分割されて 構成がわかりやすくなるというところだったり、よく使う機能というのをあらかじめフレームワークにまとめておいたりとか、例えばモデル層のExtensionも、 アプリケーションとアプリケーションのExtensionで共通して使うことができたりします。あと、ビルドタイムを短縮できる場合があります。モジュールを再ビルドしなくてもいい変更であれば、そのビルドタイムを短縮することができるという構成になるかなと思います。

さらに、そこからアプリの機能ごとを分割していくという構成が考えられます。もともとアプリに入っていた機能をさらに細かいモジュールに分割していくことによって、機能1の対象の機能だけを備えたサンドボックスアプリをつくって、そこだけの機能を持ったアプリを開発することができます。機能ごとにモジュールがほとんど分かれているので、複数チームで独立して同時開発しやすくなるという点があるかなと思います。メルカリ、メルペイはこちらの構成を使っています。

メルペイの構成

次に、実際にメルペイの構成についてご紹介いたします。

メルペイの構成の全体を見るとこういう形になっていて、メルペイ自体はメルカリアプリの中に統合された形で実際にはリリースされています。なので、これ全体がメルカリアプリの構成だと思ってもらえばよくて、青い部分がメルペイの機能の部分になります。

まず、一番下の紫の部分ですけれども、こちらがメルカリの部分とメルペイの部分で共通して使っているモジュールになります。例えば何があるかというと、共通して使っているデザインシステムだったり、gRPCのクライアントだったり、共通して使っているサードパーティのライブラリというのがSharedの部分としてあります。

その上にメルカリ、メルペイの機能が乗っかっているという形ですけれども、メルカリは今回省略してメルペイのところで見ていくと、メルペイのところはメルペイところで、さらにメルペイ全体で共有して使っているSharedのモジュールがあります。コア機能が入っているところだったり、API通信の機能が入っているモジュール、あとは、メルペイで共通して使っているサードパーティーのライブラリとかも入っているところがあります。

その上に、各Feature、機能ごとのモジュールがあります。例えばNFCの機能モジュールだったり、クーポンのところ、QRコードの機能のモジュールがあって、これが何個かたくさんあります。これらがすべてアプリケーションのターゲットにリンクされていて、アプリケーションターゲットからは各Featureに必要な依存している機能というのをDependency injectionして、各機能で連携しながら使っています。ここの仕組みについては、またあとで詳しくご紹介いたします。

さらに詳しく見ると、機能モジュール、Featureモジュールのところは、現状、メルペイでいう18個のFeatureモジュールがあります。すべてこれらスタティックライブラリになっています。スタティックライブラリはバンドルを持てないので、それぞれモジュールごとに専用のバンドルがあります。ここも機能のモジュール間では直接参照はしていない、それぞれ機能間では独立しているような形になっています。メルペイのSharedのところは、3個の共通モジュールがあって、こちらはすべてのFeatureモジュールからインポートされるのでダイナミックフレームワークの形でつくっています。これらのモジュールは、すべてXcodeGenでプロジェクトを管理している形になります。

次に、メルペイのプロジェクト組織の構成ですけれども、メルペイではプロジェクトが同時でいくつか並行して進んでいっているのですが、組織でいうと、そこにiOSチームからエンジニアがそれぞれのプロジェクトにアサインされていくような形になっています。ほかにもAndroidチームだったり、Backendチームからそれぞれのプロジェクトにメンバーがアサインされていきます。

例えばAが終わって、新しくプロジェクトBというのが立ち上がったときに、ここでメルペイの新規モジュールをつくる場合が多いです。すべてではないですけれども、そういう形で新規モジュールをつくることが多いです。

新機能を開発していくときのプロセスですけれども、まず、テンプレートから新モジュールを生成できるようになっています。init_frameworkというスクリプトを用意していて、例えばinit_framework.sh Merpay Tech Fest Kitを実行するとそこのモジュールが生成されると。モジュールテンプレートに含まれるものとしては、XcodeGenの設定ファイルだったり、各種ターゲットのフォルダですね。その機能自体のところ、ここがスタティックライブラリのバンドルになっているんですけれども、それをテストするターゲットだったり、その機能のサンドボックスアプリのターゲット、プレビュー用のアプリですね。このプレビューに関しては「Xcode PreviewsでUI Kitベースのアプリ開発を効率化する」というプレゼンを去年、iOSDCでメルペイのkenmazさんが発表しているので、ぜひ参照してもらえるといいかなと思います。

実際にちょっとやってみましょうということで、init_framework.sh MerpayTechFestKitというのを実行するとフレームワークが生成されます。こんな形でいろんなフォルダが出てきて、ここでメイクしてプロジェクトファイルができあがるので、これをワークスペースのところでMerpay Tech Fest Kitのプロジェクトファイルを追加します。

実際に追加するとワークスペースとしてはこんな感じで、Merpay Tech Fest Kitのプロジェクトができています。

サンドボックスというControllerをつくって、これはサンドボックスアプリ用のViewControllerなんですけれども、ここにいろいろ機能を表示できるようなものを入れていくと、サンドボックスアプリが試せるというような形になっています。

現在の構成の利点なんですけれども、Feature moduleは独立しているので、各プロジェクトが独立して開発していきやすいというところがあるかなと思います。あと、アプリ全体をフルビルドしなくても、サンドボックスアプリで開発できるのでビルド時間が短縮できるというところですね。

こちらもNFCの機能とかをテストするときに、実際に活用しているスライドで「Developing Apple Pay In-App procuring in Merpay」ということで、過去にkenmazさんが発表されているので、ぜひ見てみてください。機能モジュールはスタティックライブラリなので、モジュールが増えても起動時間に影響を与えません。なので、機能モジュールがいくら増えてきても大丈夫になっています。

Appleのドキュメントにも、Dynamic Frameworkの数を減らせば、APP launchタイムは削減できると書いてあるんですけども、スタティックライブラリは起動時間に影響を基本的には与えないということになるかと思います。

プロジェクトファイルをXcodeGenで管理しているので、よくあるプロジェクトファイルはコンフリクトするということも発生しないですし、ファイルの追加をテンプレートからつくるというところもかなり楽になっています。ここはXcode Genじゃなくてもいいかなとは思うんですけども、何らかのプロジェクト管理ツールは必要かなと思っています。あと、新プロジェクトを立ち上げるのに新モジュールをつくることで、そのときにモチベーションが上がるということは実際あって、これからそこを育てていく感じだとか、そこのモジュールに対するオーナーシップというのが持てるということがあります。

チーム内でのナレッジシェアなんですけれども、やっぱり自分が関わったモジュール以外の機能が分かりづらいというのは実際にあって、そういったところはチームミーティングで、具体的なコードや機能をシェアする時間を設けるだとか、週替わりで発表するということでいろいろシェアをしています。そういったところで得た知見として、ほかモジュールで似たようなところがあったりすると参考にしてつくっていったりしています。

モジュール間連携の仕組み

ここからは、モジュール間連携の仕組みの具体的なところを説明していくんですけれども、機能を開発しているとモジュール間で画面遷移が必要になることがあります。例えばNFCの関連の画面からQRコードを呼び出したりとか、クーポンからQRコードを呼び出したりとか、そういったことが必要になってきます。ここをモジュール間で直接参照してしまうと関係が複雑になったり、循環参照が発生してしまったりするので、メルペイではモジュール間で直接参照しないというふうにしています。

そこはDependency Injectionで連携できる仕組みを構築しています。

具体的にどうやっているかというと、まず、SharedのCoreの部分にMerpay Sceneというenumを定義しています。そこのそれぞれのケースとしてモジュールのケースを並べていて、その各ケース、何々Kitのケースに対してさらにNFC KitのSceneというのはこういうSceneがありますというのを個別に定義していっています。NFC Kit SceneのScene1、Scene2、QR Kit SceneのScene1、Scene2という感じですね。

さらに、CoreのところにMerpay Scene Factoryというものを定義しています。これはプロトコルで、Scene Factory TypeというViewControllerを取得できるプロトコルを定義していまして、CoreのところにMerpay Scene Factoryという全部のScene FactoryをまとめるようなScene Factoryがあります。このクラスの中にはFactoriesというのがあって、このFactoriesの中にそれぞれのFeatureモジュールと、Merpay Scene Factoryがレジストされてくると。ViewController関数のところには、このFactoriesをfor文で回して、それぞれのFeature FactoryからViewControllerを取り出して、ヒットしたものがあればViewControllerを返すという実装にしています。

DependencyのところにScene Factoryを入れているという構成ですね。

Featureのところを見ていくと、各FeatureごとにScene Factoryというのがつくとします。例えばNFC KitのScene Factoryというのがあって、これはMerpay Scene Factory Typeを実装していて、ViewController functionのところで、まずは渡ってきたSceneというのが自分のKitのSceneだよねというのを確認して、あとはswitch文で各ケースに対するViewControllerをリターンするという仕組みになっています。これはすべてのFeatureのところで、Scene Factoryが実装されています。

アプリケーションターゲットのところでは、まずは、大元のMerpay Scene Factoryをつくって、そこにすべてのFeatureのScene Factoryを登録していっています。NFC Kit Scene Factory、QR Kit Scene Factory、Coupon Kit Scene Factoryという形でFeatureのScene Factoryをレジストしていって、DependencyにScene Factoryを渡すと。これをViewControllerに渡していくという仕組みになっています。

実際、NFCからQRを連携していくときにはどういうふうにするかというと、NFCのScene1というControllerがあったとして、ここでshow QRという形でQRを出したいというときには、このDependencyのScene FactoryのViewController関数を呼び出してQR KitのSceneを渡すと。そうすると、ViewControllerが取得できるので、そこにプッシュしていくというような仕組みでモジュール間連携を実装しています。

こうすることで、モジュール間で直接参照しなくても良いということです。あとは、モジュール内のViewControllerをすべてinternalにすることができます。なぜかというと、ViewController自体は同じモジュール内のScene Factoryで生成されます。使うときは、UIViewControllerで抽象化されるので、型をパブリックにしなくても連携して使っていくことができます。以上が、モジュール間連携の具体的な仕組みです。

マルチモジュールを活用した開発プロセス

次に、マルチモジュールを活用した開発プロセスについてご紹介いたします。

メルペイでの機能開発プロセスなんですけれども、新機能の開発をしていくときに、まずは、先ほど言ったように新規モジュールをつくります。スクリプトでモジュールをつくって、その機能用のFeatureブランチをつくります。Feature Merpay Tech Festというブランチをつくって、ここのFeatureブランチで新しい機能を開発していくと。mainブランチとの差分が開発している間にどんどん出てくるので、この差分をデイリーでmainブランチをFeatureブランチにマージしながら開発していきます。開発ができてきたら、このFeatureブランチでメルカリアプリをビルドしてQAをやっています。

図にするとこういった形で、複数のFeatureが同時に開発を進行しているので、mainから1つのFeatureを切って、mainをどんどんそこにnightly mergeしてQAしていくと。一方、ほかのFeatureでもmainをどんどんデイリーでマージして、QAして、QAが終わったらmainにマージしてリリースという形のプロセスでやっています。

ここの問題としては、コンフリクトがやっぱり発生します。デイリーでmainブランチをマージするときによくコンフリクトが発生します。例えばローカライザーブルストリングスとかで文字列が追加されたときや、共通モジュールのところ、Coreのモジュールのところでお互いに編集しているときとかにコンフリクトが発生したりします。そこがコンフリクトしてしまうので、毎日ブランチを管理するコストがかかってきます。数十分とかでも毎日やっていると結構なコストになってしまうというところですね。

あとは、リリース時のマージについてなんですけれども、大きなFeatureのリリースが重なると、コンフリクトが大きくなる可能性が高いです。あと、本来はすべてのFeatureのコードがマージされている状態でリリースされるときにはQAすべきというのがあります。

理想としては、開発中のコードもMainブランチにマージするんですけれども、新規モジュールはリリースまでアプリに入れないというのが理想になるかなと思います。新規モジュールをアプリに入れてしまうと、アプリサイズが増えてしまったりとか、その新しいモジュールを解析して見られてしまったりすると情報漏洩になってしまうので、新しいモジュールはリリースまでアプリに入れたくないというのがやっぱりあるかなと思います。

最近取り入れている方法としては、新モジュールをつくったときに、そのコードをMainブランチにマージします。開発中のコードもどんどんMainブランチにマージしていくんですけれども、アプリ本体にモジュールをリンクしないという方法で進めています。モジュールをリンクしなければ、リリースされるバイナリにはそのモジュールは含まれないので漏洩することは基本ないです。機能開発自体は、サンドボックスアプリで進めることができるので、モジュールを本体にリンクしなくても開発自体は進めていけるというような形になっています。

とはいえ、Feature開発中は、Featureブランチは最終的には必要になってきます。というのも、既存の機能への変更だったり、既存の画面の変更というのがどうしても発生する場合があるので、そういったものはあらかじめMainに入れるということはできません。ものによってはFeatureフラッグとかを使えばできたりもするんですけれども、できない場合もあります。あとは、QAのときにはモジュールをリンクしてQAしないといけないので、モジュールをリンクしたアプリのQAのときにFeatureブランチを使っていく必要があります。

ただ、Featureブランチに全部入れておくよりは、どんどんMainブランチに入れていくほうがdiffがかなり減るということがあります。diffが減るので、ほかのブランチとのコンフリクトの可能性も減ってくるかなと思います。共通モジュールのところにあらかじめ変更が入っている状態でマージしていけるので、そこをどんどん解消しやすくなるかなと思います。これ自体、マルチモジュールだからこそできる開発プロセスになっているかなと思います。「Feature branchを使わないFeature開発」というタイトルでメルペイのkuさんが発表しているので、これも参照してみてください。

まとめ

まとめです。メルペイのスケーラビリティを支えるマルチモジュール開発ということで、メルペイのようにたくさんのプロジェクトが同時に進んでいてもマルチモジュールを使うことで、それぞれのチームが独立して小さく早く開発している構成になっています。かつ、機能モジュールのところはすべてスタティックライブラリにしているので、モジュールがいくら増えていっても起動時間に影響しないという構成になっています。3番目のところで説明したモジュール間連携の仕組みによって、機能モジュール間の依存関係がないクリーンな構成になっていますので、循環参照も行わなくなっています。

あとは、最後に説明したような形で、マルチモジュールならではのブランチマネージメントとか、開発プロセスというのも取り入れて開発していっています。これらがメルペイのスケーラビリティを支えるマルチモジュール開発ということになります。

発表は以上になります。ご清聴ありがとうございました。