クックパッドiOSアプリの破壊と創造、そして未来

三木康暉氏:こんにちは、「@giginet」です。今日は、クックパッドのiOSアプリを例に、巨大で歴史のあるアプリの開発効率をどうやって改善していくかというお話をします。どうぞよろしくお願いします。

(会場拍手)

クックパッドのiOSアプリは多くのユーザーさんがいらっしゃる一方で、巨大で歴史のあるアプリです。最初のコミットは2012年の8月、その後3万5,000以上のコミットを経て、23万7,400行のコードベースがあります。1リリースで10名程度の開発者がコントリビュートしており、毎週最新版がリリースされています。

この開発規模になってくると大きな負担となってくるのが、ビルド時間です。

このグラフは開発者の毎日のローカル環境での累積ビルド時間です。これを見ると、1日累積で1時間近くビルドにかけている開発者もいます。ビルド時間によって大きく生産性が損ねられているのです。

そして、歴史あるアプリであるため、コードベースには今なお27パーセントものObjective-Cのコードが残っています。

現在のクックパッドアプリの課題は、膨大なビルド時間による生産性の低下です。今なお残るObjective-CもSwiftの表現力を妨げる一因となっています。ほかにも、コードベースが膨大すぎて全体像の把握が困難という問題もあります。

この状況を打破するために立ち上げたのが「霞が関プロジェクト」です。

開発者の生産性向上にはさまざまなアプローチがありますが、霞が関プロジェクトは、大きく「コード整理」「Objective-Cの破壊」「ビルド時間の改善」の3つをテーマにしています。

そこで最も効率的な手段と考えられるのがアプリのマルチモジュール化です。

マルチモジュール化は、各機能をDynamic Frameworkとして分離して、アプリケーションからリンクする方法です。ビルドはフレームワークごとに分散して行われるようになり、ビルド時間の改善が期待できます。

しかし、現実には巨大な1つのターゲットに全機能が押し込まれています。

この状態では、少しの変更でアプリケーション全体のビルドが必要になり、無駄が多いです。

マルチモジュール化によるメリットとObjective-Cの廃止

マルチモジュール化により、開発者は各機能が実装されているモジュールの変更のみで済むようになります。これにより、変更していない部分についてはビルドキャッシュが効き、開発時のビルド時間が改善できます。

そのほかに、Objective-Cの古い実装を隠蔽したり、各画面を疎結合にする効果も期待できます。これらについては後ほど見ていきましょう。

もう1つはObjective-Cの廃止です。

Objective-CとSwiftの混在は、Swiftの表現力を損なうだけではなく、ビルド時間にも大きな影響を与えています。iOSの開発環境では、SwiftとObjective-Cを相互に利用しようとしたときに特殊なブリッジを生成します。この過程がビルド時間に与える影響は無視できません。

霞が関では、具体的なメトリクスとして、「開発者の手元のビルド時間を1日30分以内に収めること」「Objective-Cを完全に消し去ること」の2つを掲げています。

かつてのアーキテクチャ

ここまでで現状の課題と霞が関の目標について説明しました。ここから、霞が関以前のクックパッドアプリのアーキテクチャを見ていきましょう。

以前までのクックパッドアプリは、典型的なModel・View・Controllerの、いわゆるMVC構造でした。

しかし、実態は巨大なViewControllerやシングルトンのマネージャーが存在しており、混沌としております。

(会場笑)

Objective-Cのコードも依然として多く残っていました。

そこで、1年半ほど前から現在まで、Clean Architectureの導入を進めてきました。このClean ArchitectureはVIPERというアーキテクチャをベースにしており、1つの画面を1つのVIPERアプリケーションと呼んでいます。

それぞれのVIPERアプリケーションは、大きく、APIリクエストなどを司るData Layer、そのデータを統合してロジックを記述するDomain Layer、それらを利用し実際に表示や画面を構築するPresentation Layerで構築されています。この仕組みにより、今までのアーキテクチャと比べて、画面間の結合を減らすことができました。

クックパッドアプリは、巨大なアプリケーションのターゲットが存在しており、その中で大きく「Cookpad」と「Tsukuru」、2つの名前空間に分割されています。

Tsukuruは最初に説明した古典的なMVCアーキテクチャの集合です。CookpadはVIPERアプリケーションの集合で、Clean Architecture導入後に実装された画面がこちらになります。こちらはすべてSwiftで実装されています。

これら2つのアーキテクチャはすべて1つの巨大なビルドターゲット内に存在しています。ここ1年ほどで、新規実装はCookpadへ、既存実装の置き換えの際はTsukuruからCookpadへの移行が行われていました。これが霞が関前のクックパッドアプリの現状です。

既存アーキテクチャを活かしたマルチモジュール化

ご覧のように、クックパッドアプリにはすでにClean Architectureが部分的に導入されており、画面間の結合を分離するための土壌は整っていました。霞が関では、この資産を活用しながらマルチモジュール化を進めていっています。

マルチモジュール化を進めるにあたって、まずXcodeプロジェクトの破壊に着手し始めました。

モジュール分離にあたって、Xcodeプロジェクトにドラスティックな変更が増えることが予想できたためです。

そこで導入したのが「XcodeGen」です。XcodeGenは、YAMLにプロジェクト定義を記述して、そこからプロジェクトを生成するためのユーティリティです。導入により、リポジトリからプロジェクトを完全に削除できました。

大きな利点として、プロジェクトへのビルドターゲット追加が容易になった点が挙がられます。

通常はターゲットの追加にXcodeのGUI操作が必要でしたが、数行のYAMLでビルドターゲットが追加・編集できるようになりました。導入以前まで、編集が難しく、コンフリクトの解消も困難でしたが、これによりプロジェクト変更に耐えられるようになりました。

次の利点は、ターゲットに含まれるソースをディレクトリ構造で強制できることです。

Xcodeプロジェクトは通常、個々のソースコードについて、どのビルドターゲットでビルドするか、個別に保持しています。しかし、人力で確認するのは難しく、レビューも困難です。XcodeGenでは特定のディレクトリ以下にあるソースはそのターゲットでビルドされるため、ディレクトリ構造を強制することができました。

マルチモジュール化を進めていくにあたって、まずプロジェクトの破壊から進めたことはうまくいきました。ビルドターゲットの追加が数行のYAMLの変更とファイルの移動だけで完了するようになりました。霞が関以外でも、プロジェクトのコンフリクトがなくなったという利点があります。

実際にマルチモジュールを導入する

ここからは実際にどのようにマルチモジュールを導入していっているかを見ていきましょう。

霞が関には3つのコンセプトが存在します。

まず第1に、書き換えを強制せずに移行パスを提供することが挙げられます。既存の実装は膨大なので、開発を止めずに書き換えを行うのは不可能です。既存のコードはそのまま、もしくは簡単な置換で済むようにして、徐々に書き換えていける仕組みが理想的です。

次に、Objective-Cの完全な隠蔽です。少しでもObjective-Cが存在すると、新規の実装もそれとのやりとりに引きづられてしまいます。すべてのコードをSwiftに書き直せるのが一番よいのですが、それは現実的ではありません。霞が関では、書き換え前のObjective-Cを残しつつも、いかに意識させないかを目標としています。

最後に、結合の分離と詳細の隠蔽です。クックパッドアプリの機能は膨大で、それぞれの画面のビジネスロジックを把握することは困難です。霞が関は、それぞれのVIPERアプリケーションを分散ビルド可能にして、それぞれが連携できる仕組みの構築を目指しており、Clean Architecture内での実装には関知しません。「既存の資産を活かす」というコンセプトと合わせて、現状のClean Architectureをそのまま利用可能になる仕組みを構築しました。

マルチモジュール化の将来像

ここから、霞が関の描くマルチモジュール化の将来像を見ていきましょう。

これが霞が関導入後のアプリケーションの依存関係です。アプリケーション以外のモジュールはすべてダイナミックライブラリとなっています。

CookpadCoreは、すべてのモジュールから利用する抽象的なインターフェースの集合です。API通信やロギング、画面遷移などを行うためのインターフェースが定義されています。

CookpadComponentsは、さまざまなアプリケーションから共通して使われるUIコンポーネントやユーティリティを保持しています。

その次のレイヤーがFeatureモジュールです。各VIPERアプリケーションがざっくりとした機能単位ごとに実装されています。例えば検索やレシピ投稿といった単位です。現実的には、アプリの起動時間に影響してくるため、3〜4個の分割を想定しています。

最後に、最も上のレイヤーにあるのがアプリケーション本体です。当面の目標は、このアプリケーションからVIPERアプリケーションを引き剥がし、Featureモジュールに移行していく作業です。

この構造では、下位のフレームワークほど抽象的な概念を持っています。まず前提として、このツリー上で各モジュールは自分より下位の依存しか知ることができません。アプリケーションはすべての依存を知っています。

CookpadCoreからは「Environment」と呼ばれるものが提供されています。これは各モジュールから依存を考えずに副作用を扱うための仕組みです。CookpadCoreに存在するものはEnvironmentのインターフェースのみであり、具体的な実装はアプリケーション側で行われています。

この仕組みはメルカリさんが導入していた仕組みを参考にさせていただいているのですが、我々はこれを「霞が関ゲートウェイ」と呼んでいます。

(会場笑)

この仕組みは一見複雑ですが、この設計により数多くの問題が解決できます。

Objective-Cの隠蔽と排除

まず、Objective-Cの隠蔽と排除です。霞が関ではアプリケーションより下位のレイヤーではBridging Headerを持ちません。

すなわち、構造的にObjective-Cのコードが存在できない世界になっています。そのため、「Objective-Cの実装をアプリケーション以外に持たせてはならない」という明確なルールになりました。

もし古いコードからObjective-Cのブリッジングが必要になったときは、すべてアプリケーション側にラッパーを定義します。

このように、Objective-Cの古い実装はアプリケーション側に持たせて、下位のモジュールでは隠蔽することで、Swiftのみで開発できるようになりました。

また、この設計は既存コードのSwift化を進める上でも大きな指針となります。今までの混沌とした状態では漠然と手をつけやすいところからSwift移行を進めていましたが、必然的に下位のモジュールから利用が必要なクラスからSwift化を進めていく必要があり、優先付けが容易になりました。

クックパッドアプリにはObjective-Cで実装された古いライブラリも残されています。これらのライブラリを直接利用することはSwiftの表現力に制限を生じさせます。霞が関ゲートウェイの仕組みは、古いライブラリの隠蔽の役目も果たします。例えば、各機能がObjective-Cのライブラリを使ってAPI通信を行う場合を見ていきましょう。

各開発者は、API通信の開発時に、Environmentの提供する抽象的なネットワーククライアントを利用します。Environmentとそれを提供するCookpadCoreモジュールは、そのリクエストが実際にどのように送られているかについて知りません。そこで、具体的な処理はアプリ側で実装します。

アプリは古いAPI通信のライブラリを利用していますが、この依存はアプリにのみ存在しています。

この仕組みにより、古いライブラリの利用部分を隠蔽できました。各機能の開発者は古いライブラリを意識する必要はありません。

ユニットテストの記述も容易に

ほかにも、この仕組みはユニットテストの記述も容易になります。今までは各機能が直接ネットワーククライアントにアクセスしていました。テストは、Objective-Cを使ってメソッド実行をテストケースのみで動的に書き換えることでStubを実現していましたが、この方法はSwiftでは推奨されていません。

そこで、Environmentから副作用を取り出すようにしました。この仕組みの導入により、テスト用の副作用の差し替えが容易になりました。Environmentが提供する副作用を差し替えることで、Dependency Injectionを実現しています。これによって、API通信やLoggerやエラーハンドリングといった、あらゆる副作用をStubしてテストできるようになりました。

残念ながら、現状はまだビルド時間の改善に結びついていません。現段階では、霞が関ゲートウェイの部分的な導入が完了し、実験的に一部のVIPERアプリケーションをFeatureモジュール上で動作させている状態です。

間もなく、新規のVIPERアプリケーションの開発が霞が関ゲートウェイ上で行えるようになります。その後、既存のVIPERアプリケーションもFeatureモジュール上で動作するように書き換えていく必要があります。

ほかにも、我々は無数に解決していかないといけない問題を抱えています。これについては、現在取り組んでいる最中です。

大規模で歴史のあるiOSアプリの開発効率の改善の取り組みについてお話ししました。詳細については懇親会やAsk the Speakerでお話ししましょう。

ご清聴ありがとうございました。

(会場拍手)