Merpay & Mercoin Tech Fest 2023 は、事業との関わりから技術への興味を深め、プロダクトやサービスを支えるエンジニアリングを知ることができるお祭りで、2023年8月22日(火)からの3日間、開催しました。セッションでは、事業を支える組織・技術・課題などへの試行錯誤やアプローチを紹介していきました。
この記事は、「Merpay iOSのGroundUP Appへの移行」の書き起こしです。
@kenmaz:今回は、「Merpay iOSのGroundUP Appへの移行」というタイトルで、iOSチームの@kenmazが発表します。
こちらが私の自己紹介です。
今回のメインテーマであるMercari iOSのGroundUP App(GU App)とは何かについて説明します。
GU Appは、メルカリ本体のコードをフルスクラッチで書き換え、さらにBazelによる高速・高信頼のビルド環境に置き換え、メルカリをイチから作り直すプロジェクトです。
メルカリの開発が始まってから10年以上経って、技術的負債がかなりたまってきている状態になったので、フルスクラッチで書き換えるのが本プロジェクトの目的です。
このプロジェクトではiOSではSwiftUI、AndoirdではJetpack Composeといった最新のUIフレームワーク、さらにDesign System v3.0という社内のUIライブラリを最新版に置き換えることが盛り込まれたプロジェクトです。
フルスクラッチで書き換えるプロジェクトなので、再構築期間中は新規機能開発を凍結し、フルスクラッチでのリライトに集中する流れで進みました。
参考記事
メルカリアプリのコードベースを置き換える GroundUP App プロジェクトの話
メルカリiOSアプリのBazelを使った高速・高信頼性ビルド
メルカリアプリの上にはメルペイの機能が載っています。メルペイにおいてのGU Appへの移行について説明します。
メルカリ本体のコードと比べるとメルペイのコードは、比較的技術的負債が少ない状況でした。開発が開始してから、当時で2〜3年ほどしか経っておらず、比較的クリーンなUIKit + Design System 1.5を使っていました。そのため、メルペイのコードについては、フルスクラッチで書き換えるよりも、最小限の変更でそのままGU Appに載せ替える方針を決めました。
さらにGU Appの裏で、メルペイでは新規機能開発の必要性があり、メルペイ自体の開発を止めるわけにはいきませんでした。移行期間中でもメルペイの新規開発は極力止めずにそのまま載せ替えることを目的としました。
こちらがプロジェクト全体の概要です。上の段の「Production」が実際にお客さまに届けるアプリの状況で、下の段の「Development」が社内で開発しているコード群です。
GU Appのプロジェクト自体は2020年4月頃に始まりました。最初はメルカリのコア部分のみをSwiftUIで書き換え、アーキテクチャや全体の構成などを固める作業からスタートしました。
その後2021年10月に全ての準備が完了して、本格的にリライトのプロジェクトが開始されました。この段階は、メルカリ側の機能群の開発を凍結し、全ての開発メンバーはメルカリのリライトに注力する期間でした。この期間が2022年8月まで続くことになります。
一方メルペイについては裏で重要なプロジェクトが動いていたので、機能群自体は変更せず、メルカリ側とメルペイ側のコードを Integrationするため、 IntegrationLayerと呼ばれる中間Layerのみを書き換えることで、メルペイの機能群のコードを変更せずに新しいアプリと古いアプリ両方で同じメルペイのコードが動作するように対応する必要がありました。
そこで IntegrationLayerの開発にメルペイチームが参加しました。このGroundUP Appのプロジェクトが始まった同時期に、社内では重要なプロジェクトがいくつか進みました。
メルペイのトップ画面をリニューアルするプロジェクトや、メルカードというメルカリのクレジットカードを開発するプロジェクト、メルコインというビットコイン関係のプロジェクトなど、いろいろなプロジェクトが走っている中で行われたのがGU Appです。
このメルカリと Integrationの実装が完了し、2022年8月に、Phase1としてGroundUP Appがリリースされました。この時点で、メルカリ側は全てSwiftUIにリライトされ、メルペイ側は今まで通りUIKitで動いています。
その後、2022年10月にメルペイ側でSwift UIで書き換えられたものがリリースされ、残りのメルペイの機能やメルカードの機能はまだUIKitで作られていました。2022年11月にメルカードの機能、翌年にメルコインの機能がリリースされました。
フェース3は進行中ですが、今残っている古いUIKitで作られてる部分を最終的には全部書き換え、最終的には100%Swift UIのアプリにする動きが続いています。
それぞれのPhaseについて説明します。まずはPhase1のMerpay SDK GU App Integrationです。
Merpay自体はSDKという形で開発されていて、比較的メルカリと疎結合の状態で設計されていました。
これにはいろいろな背景がありますが、一つは、メルカリの機能はメルカリが担当し、メルペイの支払いや決済に関する機能はメルペイで開発を担当していて、会社自体がわかれていたのでリポジトリを分けて、設計も極力結合なものにした方がよいという考えで、このような構造になっています。
メルカリアプリとは別にメルペイアプリ単体のアプリを作る話もあがっていたので、かなり疎結合でポータビリティを持った設計になっていました。
参考記事:メルペイのスケーラビリティを支えるマルチモジュール開発
GU App Integrationは4つのステップにわかれています。
一つ目は、準備段階としてメルペイ関連機能のSDKへ集約することです。
Merpay SDKの中にはメルペイの必要な機能が入っていますが、いくつかの機能はメルカリ側に直接実装されているコードもあります。そこで、メルペイが管理すべきコードは一旦全部Merpay SDKに集約することにしました。例えば、本人確認機能や売上金の振り込み申請機能などはSDKに移しました。
これらはUIKit + ReactiveCocoaやObjective-Cで書かれていましたが、一旦メルペイのアーキテクチャに合わせてUIKit + MVVMに書き換えました。
これで全てのコードがMerpay SDKに入ったので、次はビルドの環境を設定変更します。旧メルカリアプリはCocoaPods/Carthage/git-submoduleを使ってビルドを行っていましたが、新しいメルカリアプリであるGU Appでは、全てBazelを使ってビルドを行うように変更されました。
したがって、Merpay SDKを含む全ての社内外のコードをBazelでビルドできるように設定する必要がありました。
merpay-ios-sdkやDesign System、Google Maps SDK、Lottie-iOS、CryptSwiftなど社内外のコードをBazelで全てインポートし、一つのメルカリアプリとしてまとめてビルドできるようにする設定変更が行われました。
次はコーディングの段階です。メルペイにはMerpayDependencyRegistryというDIコンテナのようなものがあります。これを用いることにより、メルカリ側で実装されている機能をメルペイ側に注入できます。
全ての依存関係がここに集約されているので、メルカリ側の実装をメルペイに注入する作業をひたすら行います。実際に注入したコードとしては、Feature Flagやイベントログなど、図にある通りです。
これでメルペイのコードは全てMercari GU Appの上でビルドできるようになったので、最後に画面遷移の実装を行います。
GU Appでは、基本的に全ての画面はSwift UIで作られていますが、画面遷移周りはすべてUIKitで実装されています。Wireframe Layerというものがあり、そこでSwiftUIの画面は一旦UIHostingControllerでUIKitのViewControllerとして変換され、それをUINavigationControllerが画面遷移を制御します。
メルペイの画面はすべてUIViewControllerで作られているので、DependencyRegistryを経由して、UIViewControllerをWireframeにそのまま渡します。あとはWireframe内部で細かい画面遷移の実装を行います。
以上で統合完了です。この段階で1年ぐらいかけて行われてきたGU Appのアプリがリリースされました。メルカリアプリは全てSwiftUIで書き換えられていますが、メルペイの機能が集約されている「支払い」タブに関してはまだUIKitのままの状態です。
しかし、更なる最適化の作業が残っています。
まずはGitリポジトリ統合です。GU Appが始まる前は、mercari-iosリポジトリでメルカリのコードが管理され、merpay-ios-sdk、mercari-jp-ios-coreは別リポジトリとして管理されていました。
GU Appでは、メルカリ側のコードはmercari-groundup-iosという新たなリポジトリで管理されています。メルコインのコードもすべて同じリポジトリに実装されています。しかし、メルペイの機能やメルカリのいくつか残りの機能、たとえばメルカリのToday Extensionの機能やメルペイでしか使用していないDesign Systemのコードは、依然として別のリポジトリで管理されており、Bazelによって都度インポートされる構成になっています。
しかし、この構成には開発効率の観点から二つの問題があります。
一つはリポジトリが分かれているのでメルペイの機能の開発を直接行えないという問題です。メルペイの機能を開発する際は、まずMerpay SDKのリポジトリをチェックアウトして、ソースコードを編集・プッシュして、GU Appに戻って、Bazelでリポジトリをインポートし直してビルド・動作確認、という非常に煩雑なデバッグプロセスが必要です。
また、GU Appのビルドインフラを活用できないという問題もありました。GU Appとメルペイのモジュールはそれぞれ別のビルドインフラ上でCIが実行されます。GU AppはBazelでビルドが行われているのでユニットテストも非常に高速にできます。一方、メルペイモジュール単体のビルドにはBazelは使用していないので、その恩恵を受けることができません。
Gitレポリポジトリ結合では、すべてのコードを単一のGU Appのリポジトリに移動することによって、上記の問題を解決します。
なおDesign System1.5はメルペイでしか使われてない古いUIライブラリなので、これは例外としてこのまま別リポジトリとして、コードフリーズした状態でインポートすることにします。
リポジトリ統合にはいくつか方法がありますが、一番単純なのは、ファイルコピーです。一番簡単ですが、Gitの履歴が消えるという問題がありました。
なるべく履歴を壊さずにソースコードをGU Appのリポジトリに移動する手段として、Subtree Mergingというリポジトリの結合方法があります。これによって履歴は保持されますが、リポジトリ全体をマージしてしまうので、この2〜3年間で蓄積された不要なデータまでマージされてしまいます。リポジトリのサイズが増えることで、CIの時間に影響を与えてしまう問題があります。
そこで、もう一つの解決策としてgit-subtreeコマンドを使って、リポジトリを部分的に結合する方法をとりました。必要最小限のコードのみをピックアップして結合し、かつ履歴を保持することが可能になります。必要なコードをピックアップする必要があるので少々作業が煩雑になってしまいますが、これによってリポジトリの肥大化を抑えつつレポジトリ統合を行うことができます。
参照
Subtree Merging:https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging#_subtree_merge
git-subtree:https://git.kernel.org/pub/scm/git/git.git/plain/contrib/subtree/git-subtree.txt
リポジトリに全てのファイルを移動できた後は、Bazelビルドに合わせた最適化を行います。ソースコードのレイアウトの変更や、画像アセットを最適にして無駄なデータを含めないようにすること、Bazelのビルドとビルドターゲットとするために、それぞれのモジュールをBazelのモジュールとして定義する作業などが行われました。
また移行期間中に古いコードに変更が入るとコンフリクトが発生するので、コードフリーズ宣言を行ってコードの変更を禁止することで、コンフリクトを防ぎました。
間違いで変更してしまうこともあり得るので、変更を検知するためのモニタリングの仕組みなども入れ、リポジトリ統合を進めました。
このようにおよそ2ヶ月半ぐらいかけて、26モジュールを段階的に移行し、Gitレポジトリ統合が完了しました。
もう一つの問題は、いくつかのメルカリの機能のモジュールがMerpay SDKに直接依存している点です。これによって、テストバンドルのキャッシュが肥大化してしまい、最終的にCIの実行時間やキャッシュストレージの使用量が増加してしまうことがわかりました。
この問題を解決するために、Merpay SDKのDIコンテナのインターフェースのみを抽出し、別のモジュールとして分離する設計の変更を行いました。
これはモジュールの相関関係を表す図です。「MK Feature modules」がメルカリの各機能を実装したモジュールです。いくつかのメルカリの機能はメルペイの機能に依存しているので、それらはMerpay SDKの中の「MerpayCoreKit」モジュールに依存する必要があります。
ただ、MerpayCoreKit自体はさらに、Protocol Buffersのモジュールや、Design System1.5のモジュールなどの、メルカリの機能にとっては不要なモジュールにも依存しています。それらのモジュールはサイズが大きいので、メルカリの機能モジュールをビルドしてテストしようとすると、依存関係にある全てのモジュールがビルドされ、Bazelのキャッシュとして残り続けて、最終的にCIインフラのストレージの使用量の増加を引き起こしてしまう問題がありました。
そこで設計を変更して、MerpayCoreKitが提供していたコードのインターフェース部分だけをプロトコルとして切り出し、「MerpaySDKInterface」という別のモジュールとして切り出し、メルカリの機能モジュールは軽量なインターフェースモジュールのみに依存する形式に変更しました。
これによってモジュールごとのテストバンドルのキャッシュサイズが300MBから150MBまで削減され、ストレージの使用量の問題も解決されました。
またMerpay SDKの最小限の機能のみをメルカリ側に整理・公開する設計にしたので、SDKの債務の明確化が行われ、SOLID原則に従ったSDKの設計にも貢献できたという副産物もありました。
参照:Single Responsibility Principle in SOLID
以上で、Integrationのプロジェクト自体は完了しました。GU Appリリース後、Phase2として、次はメルペイの新規開発画面をShiftUI+GUアーキテクチャで開発するプロジェクトが始まりました。
ここで、メルカリアプリ自体のアーキテクチャの変遷について振り返っていきたいと思います。
旧メルカリアプリでは、10年の間に様々な内部的なアーキテクチャの変遷がありました。最初は純粋なMVC、そこからMVVM+ReactiveCocoa、さらにMicro View Controller + Stateアーキテクチャに変遷しました。しかも全てが変遷していたわけではなく、部分的には古いMVCが残っている状況でした。
一方メルペイは独自のシンプルなMVVM Without 3rd party libsというシンプルなアーキテクチャを採用していました。このように、GU Appの前はいろいろなアーキテクチャが一つのアプリの中に共存している状態でした。
参考資料
Mercari iOSにおける きらやばArchitectureとAutomation
Mercari iOSクライアント Re-Architectureのその後 / After Re-Architecture of Mercari iOS client
Introducing ViewModel Inputs/Outputs: a modern approach to MVVM architecture
GU Appでは、それが一新され共通アーキテクチャが策定されました。SwiftUIをベースとし、Reduxや TCA などからインスパイアされた単方向データフローの独自のアーキテクチャです。
メルカリおよびメルコインの機能は全てこのGUアーキテクチャに基づいて開発されています。メルペイもこの共通アーキテクチャに統合する方針を決定しました。
ちょうどメルペイのトップ画面のリニューアルする新規開発プロジェクトが別で計画されていたので、そこでGUアーキテクチャを試験的に先行導入することにしました。
Phase2の開発が完了しました。この段階でメルペイのトップ画面に関しては100%SwiftUIが達成できました。
メルペイのトップ画面以外の既存画面に関しては、未だにUIKitのままです。Phase3では、既存画面をすべてSwiftUIに書き換えます。これは現在進行中です。
ここで、なぜ最初に技術的負債が比較的少ないメルペイのコードを書き直す決断をしたのか、そのモチベーションについてお話します。
技術的負債負債が比較的少ないといえども、初期のコードは2018年に作られたものです。当然UIKitベースのコードなので、GUで刷新された基盤機能の恩恵は受けられせん。
SwiftUI自体ははもちろん導入できなくはないのですが、メルカリの機能で使用されているDesign System3.0を使用するには、GUアーキテクチャへの移行も必要になります。
アクセシビリティについてもGUアーキテクチャはかなり手厚くサポートされていて、新規イベントログ基盤もGUアーキテクチャにより最適化されたものになっていたなどの事情がありました。これらの事情によって、メルペイ側の既存コードも段階的にGUアーキテクチャに移植するのが良いという決断になりました。
とはいえ、メルペイの既存機能がかなりの数があるので、まず現状の仕様整理から始めました。まず、移植作業の計画を立てます。大量のメルペイの既存画面の仕様を整理・リストアップし、優先度を決めて移植作業を少しずつ始める計画を立てようと考えました。
大量の既存コード・仕様書はありましたが、ここで既存コードと仕様書の対応関係が不明瞭であったり、対応する仕様書が見つからなかったりといった問題が発生しました。仕様書によっては同じ画面に対して別の呼び方がされていることもあり、特に非日本語話者にとってはその理解が非常に困難な状況でした。
そこで既存の仕様を整理する前に、一旦全てのメルペイの画面を一意に特定する「Merpay Screen ID」を導入することにしました。
例えばここにあるように、”MP-BNK-001” はメルペイの銀行接続の1番目の画面を示します。このような採番作業を全ての画面に対して実施しました。
Screen IDをソースコード、仕様書、Figma上で横断的に記載することで、それぞれの関係を明確化しました。これによって、認識の齟齬を解消できる上、非日本語話者でも理解しやすくなりました。これは社内の開発ガイドラインにも組み込まれているので、この辺の整理が今後も進んでいくと思います。
これを導入したことによって、スクリーンに関して仕様書を探したいときにScreen IDで検索すればそれに関連する仕様書が全て出てきます。
またFigmaでUIレイアウトを確認したいときも、Figmaの検索ボックスにScreen IDを入れれば、正式なレイアウトを確認できます。
ソースコード中にこのようにScreen IDを埋め込んでおけば、ソースコードと仕様書の関係、その画面に関する実装についても発見できます。
このように、事前の準備をした上で計画を立て、現在絶賛移植作業中です。全体の77%は古いコードのままなので、今後数年かけて移植する予定です。
こちらが現在移植中の銀行接続画面の例です。
GU App Integrationの開発プロジェクトを進めることで、メルペイ自体の機能開発を止めることなく、GU AppのIntegrationが完了しました。メルペイのコードを変更することなくそのまま新しいGU Appのコードベースに移植することに成功しました。
またBazelのメリットを最大限に生かす構成に変更しました。さらにメルペイの既存画面をShiftUIで移行するにあたり、いろいろと基盤を整備しました。
以上です。ご清聴ありがとうございました。