こんにちは。楽天ラクマ モバイルアプリケーショングループのdarquroです。
今回は「楽天ラクマ」iOSアプリのフルSwift化を、約4年かけてやり遂げた話を書きたいと思います。
ことの始まり
私は2018年10月1日に楽天グループに入社しました。 当時採用面接で驚いたのは、アプリエンジニアのチーム体制を聞いたところ、iOS1名、Android1名という状態だったことでした。
なので、私がiOSエンジニアとして入社し、やっとiOSアプリは2人体制になったというわけです。
2018年は「楽天ラクマ」の前身である「フリル」を運営する株式会社Fablicを吸収合併し、それに伴い開発組織としても再構築していく時期でした。 そういったチャレンジングなタイミングに入社を決めたわけではありますが、iOSアプリのコードの状況はというと、Objective-Cという大きい技術的負債を抱えており、なかなかメンテナンスをしていくのが大変だと感じました。
私としては、Objective-Cの経験もありましたし、今後もメンバーを増やしてプロダクトの安定的に開発していくためにも、フルSwift化をリードしてやり遂げることが目標の一つでした。
なぜ4年近くもかかったのか
結果として、最後のObjective-Cクラスを削除するPRをマージできたのは2022年8月3日でした。
そして、今回この記事を書くにあたり、Swiftが登場した2014年以降、「楽天ラクマ」のSwiftとObjective-Cの比率を時系列でグラフにしてみました。
最初にSwiftのコードが入ったのは、2015年9月頃だったことがわかりました。 Swiftのバージョンでいうと1.2と2.0の間くらいですね。
私は2018年入社なので、それ以前のことはわからないのですが、2016年から2017年に一度大きくリファクタリングされており40%まで引き上げられていました。 2020年からまた引き上げは、2人だったiOSエンジニアが4人、5人・・・と優秀なメンバーを採用することができ、安定してリファクタリングに時間を避けるようになりました。
Swift化にどうしてこれだけの時間がかかったのかは、様々な要因があります。
- Objective-Cで作られたクラスがかなり多い
- いくつかの巨大なObjective-Cのクラスがあらゆる画面で参照されており、依存関係が複雑かつ密結合
- テストコードはほとんどない
- 機能開発は止められない
- エンジニアが少ない(当時)
私が入社した2018年10月当時にリリースしたv7.6.0のコードをcloneしてきて、コードの言語比率を出してみると、以下のような結果でした。
------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- XML 139 250 54 102789 Objective-C 180 6605 2193 29105 Swift 357 5530 3112 26257 JSON 121 0 0 9861 C/C++ Header 173 1114 1612 2426 Markdown 10 166 0 425 YAML 1 0 0 16 ------------------------------------------------------------------------------- SUM: 981 13665 6971 170879 -------------------------------------------------------------------------------
- 計算には https://github.com/AlDanial/clocを使用しています
Objective-Cが50%を超え、かなりの量のコードが残っていました。 それでいてテストコードは一部のみで、各画面はほとんどがViewControllerにすべてのロジックが書かれているような状況でした。
さらに、機能開発を止めずプロダクトを成長させていかなければならないため、レガシーコードをどうリファクタリングしていくのか、戦略を立て段階的に移行していく必要がありました。
リアーキテクチャーの方針を立てる
まずは大規模なリアーキテクチャーを2019年頃に行いました。 それまでは各画面のViewControllerと、ModelというディレクトリにEntityクラスが入っていて、それ以外は○○Managerといったクラスが複数あり、APIリクエストは巨大な1クラスの中に全APIリクエストのロジックが書かれていました。
そこで、PresentationレイヤーとDomainレイヤーとを分け、PresentationレイヤーはMVPのアーキテクチャーを採用し、DomainレイヤーはUseCase、Repository、DataStore、Entity、APIClientといったレイヤーで分けていく方針を立てました。
現在のアーキテクチャーは更にSwiftUIとMVVMも混在したアーキテクチャーになっており、詳しくは以前勉強会で発表した資料もご覧ください。
また、ViewControllerについては今もUnitTestは基本書いていないのですが、それ以外のレイヤーはUnitTestを書いていく方針で進めていきました。
スタンダードを選択することのメリット
ここで少し余談にはなりますが、Presentationレイヤーのアーキテクチャーについては、2019年当時はRxSwiftも流行っており、多くのアプリでRxSwiftを使用したMVVMアーキテクチャー採用していたと思います。「楽天ラクマ」はObjective-Cからの移行をしなければいけなかったため、なるべくFoundationやUIKitの標準のフレームワークと親和性の高いMVPを採用しました。
Combineが存在している現在から考えるとRxSwiftをもし導入していたらそれからの移行が発生していたので、正しい選択だったと思います。
また、「楽天ラクマ」はCarthageが登場してからもライブラリ管理はCocoaPodsだけにまとめていました。(現在は概ねSwift Package Mangerに移行しています) Xcode12でCarthageのライブラリがビルドできない問題があり、多くのアプリで対応に追われていたなか、「楽天ラクマ」はスムーズにXcode12移行ができたことがありました。
そして、一度XcodeGenの導入を検討し、調査を進めていたこともありました。 ビルドできるまでの道のりが大変だったり、Bitrise上でビルドすると失敗することが最終的に解消できず、自分達では今後メンテし続けることは困難と判断し、導入しませんでした。
現在もアプリのモジュールを分けていないため、開発しているとproject.pbxprojファイルのコンフリクトはよく発生しますが、解消方法自体は単純なため、コンフリクトを修正しながら乗り切っているという状況です。
これについては、Swift Package Managerが登場してモジュールの切り出しをSwift Packageとして管理することで、project.pbxproj
ファイルのコンフリクトを減らせることもできるので、今後はその方針も検討しています。
他社様のアプリ事例でXcodeGenを引き剥がすのにとても苦労したという話も聞いていましたので、もしXcodeGenを下手に導入していたら、それのメンテナンスに追われていて、開発がストップさせてしまう状況になっていたかもしれません。
(とはいえXcodeGenの調査を経て、不要なプロジェクト設定を精査すること繋がりました)
RxSwift、Carthage、XcodeGenなどの当時の先端技術を選択せず、iOS開発のスタンダードな手法を選択してきたことで、少ないメンバーの時期も安定的な開発体制を維持できたと思っており、技術選定はとても慎重に行うべきだと教訓になっています。 例え少し先進的なものを導入しても、ロールバックや別プランに切り替えることができると見通せていることが大切だと思います。
巨大であらゆる画面に依存したクラスのリファクタリング
「楽天ラクマ」には数千行からなる巨大で、あらゆる画面に依存したObjective-Cのクラスが複数存在していました。
例えば出品画面や検索結果画面といった「楽天ラクマ」の主要機能もObjective-Cで、FatViewControllerとなっていました。 また、"商品"や"検索条件"といったドメインのモデリングしたクラスも巨大なObjective-Cクラスで、「楽天ラクマ」は商品を中心としたアプリですので、あらゆるクラスに引き渡されているため、複雑な密結合を紐解いていく必要がありました。 (2022/8/4時点の画面)
レガシーで巨大に膨れ上がったクラスは、そのクラス自体のテストコードがなく、テスタブルな設計実装がないため、そのクラスに依存したクラスもまたmockやstubを用いたテストが書けない状態でした。
巨大なクラスはメソッド単位や機能別のSwiftクラスとして分解して、その分解したクラスに対してテストコードを書き、巨大クラスの依存を減らしていくという手法を取りました。
単純にObjective-CのクラスをSwiftクラスとして書き直して一括置換することは機能開発を続けていくためには不可能に近く、依存が少なくなった段階で残りを置換することで、テストのカバレッジを徐々に上げ、安定性を担保しながら移行できたと思います。
作業分担の可視化
リファクタリングを進めていた当初は人数も少なかったですし、口頭で何をリファクタリングするか確認しあうだけで十分でしたが、メンバーが増えてきた段階で、残りのファイル一覧と、誰が担当するか、何と依存しているかなど、作業分担の可視化も行いました。 基本的なことかもしれませんが、すぐに状況が見えることはリファクタリングを進めていくうえで、そのスピードを上げていくのにとても重要でした。
それでも発生する不具合
ユニットテストもしっかり書き足していきましたし、「楽天ラクマ」ではQAチームが開発チームとは別にあり、組織横断のナレッジを活かしながら自動テスト、手動テストを実施してくれています。 毎回のアプリリリースはQAチームにもテストを細かく実施してもらいましたが、それでも不具合は出てしまいました。
特に難しかったのが、Objective-Cでもnullable
とnonnull
の定義を書けるようになっていたものの、あらゆる画面で使われているクラスに導入すると変更差分が大きすぎて、導入が後手に回ったことです。
それにより、SwiftのコードからだとString!
のようにOptional型だけどunwrapせずそのまま参照できてしまう(Implicitly Unwrapped Optionals)ため、型の確認をしつつunwrapしていく必要があり、それが漏れていることでnilが入りクラッシュしたり、逆に余計なところまでguard文を挿入してしまったことで、挙動が変わってしまったといったことなどがあります。
こういったケースは正常ケースでは見つけにくく、特定の条件が揃った場合や異常ケースでのテストで網羅できていないことが原因でした。
こういったテストの漏れに対する対策としては、漏れていたユニットテストを追加するだけではなく、以下のようなこともしました。
- レビュアーの数を最低1名から最低2名に増やす(これはチームの人数が増えてきたことで可能となった)
- すべてのPRに対し、インパクトレベル(4段階)を設定し、QAチームに共有する。最もインパクトレベルの高いLevel4についてはTech Leadが必ずコードレビューに入る
- リファクタリングのスピードを落とす
リファクタリングのスピードをあえて落とすという選択
Swift比率が95%を超え、最終局面といったフェーズでは、リファクタリングのスピードをあえて落とすという選択もしました。
スピードを落とした理由は、短期間の間に不具合が連続してしまったからです。 明確な因果関係が難しいのですが、リファクタリングが進むにつれ、チームとしても速く進めたいという気持ちの高まりや焦りのような空気感もありました。 そういった空気感の中で、巨大なPRや大量のPRにより細かなレビューが行き届かなかったり、結果QAチームにも影響範囲が正確に伝わらなかったりといった結果に繋がっていったと感じています。
一つ一つの不具合には直接原因として実装考慮漏れ、テスト漏れといったものがありますが、根本的にはチームの空気感も影響していると考え、一旦クールダウンさせ、最後の詰めを丁寧にやっていこうと舵を取ったことで、顕著に増えてしまったリファクタリングによる不具合はなくなっていきました。
システムも作っているのは人間ですので、その変化や影響は人間の感情から少なからず受けると思います。 チームの空気感やカルチャーもシステムの品質向上には大切だと学びました。
おわりに
このObjective-Cのリファクタリングは優秀なメンバーが奇跡的に揃い、全員で協力して進めていくことで成し遂げることができました。 メンバーのみんなには本当に感謝していますし、自分のエンジニア人生の中でも、共に戦った戦友として大きな信頼を寄せ、そして誇りに思っています。
約4年という長い道のりでしたが、レガシーコードとの一つの戦いに決着をつけ、新たなステージへと向かうよい区切りを付けることができました。 これからもチャレンジをし続け、より良いプロダクトにしていきたいと思っています。
「楽天ラクマ」では、Ownershipをコア・バリューの一つに掲げ、一緒に開発をしてくれるメンバーを募集しています。 興味を持っていただいた方は、ぜひカジュアル面談でお話できればと思います。