Tabelog Tech Blog

食べログの開発者による技術ブログです

食べログAndroidアプリの技術的負債解消への道のり

こんにちは。食べログAndroidアプリをメインで担当しているsadaです。

私は食べログシステム本部 アプリ開発部の基盤チームリーダーをしています。
基盤チームでは機能開発はあまり行わず、リファクタリングや開発環境(IDEやCI/CDなど)の整備、ライブラリ選定、アーキテクチャ設計など、食べログアプリの下支えをしているチームになります。

私は2018年にカカクコムに入社し、食べログにJoinしました。

その頃は基盤チームのAndroid専任が不在という状況で、あまりAndroidについての改善が進められておらず、かなりの技術的負債が溜まっており、調査・設計・実装・テストといった工程全体のスピード感や品質に影響がでるような状態でした。

そこで私が入社して以降、食べログAndroidアプリでどのような技術的負債の返済をしてきたか、その道のりをお伝えしたいと思います。

目次

食べログAndroidアプリの技術的負債(2018)

私が入社した時、実際にあった負債の一部をご紹介します。

1. Javaが主力言語

当時はそのほとんどがJavaで書かれており、実に約90%がJavaとなっていました。

当時は、Google I/O 2017 ですでにKotlinがAndroidの公式言語である旨は発表されており1、Google I/O 2019 ではKotlinファーストが強化される2など、より開発のしやすいKotlinへの移行を進めたい状況でした。

しかし、Kotlinは導入したての状況だったため、コーディング規約のようなルールや、ライブラリの選定もできていないため、Kotlinを使った場合にどうのように実装を進めればいいのか、メンバーが悩んでしまっていました。

また、食べログアプリは多くの画面・機能をもっているため、コードベースもかなり大きく約20万ステップ以上あり、利用しているライブラリも多いため、テスト工数がかなりかかる状況でした。

※参考までに当時のコードベースをclocというツールで出力した結果です。(2018/08頃)

Language code %
Java 187,952 91%
Kotlin 18,596 9%
SUM 206,548 -

2. 開発環境がレガシー

当時、Android Studioはリリースされている安定版の最新バージョンではありましたが、その他gradleや各種ライブラリのバージョンが古いため、アップデートが必要な状況でした。

また、CI環境についてはGitHubのマージやタグ生成をhookして、自動ビルドなどを実行するような仕組みは構築されていましたが、PR時のチェックや自動テストなども組み込まれていませんでした。
そのため、もちろんテストコードもいっさい存在しませんので、テストコードを書く環境を一から作る必要がある状況でした。

3. 旧バージョンのAPIの残骸

過去に食べログ内部のAPIバージョンをバージョンアップした際に、APIやレスポンスのペイロードが大きく変わったことがありました。
しかし、当時はAPIのレスポンスをパースしたデータクラスを直接Viewで参照しており、置き換えるには全ての画面の修正が必要になってしまうので、対応するには大変困難な状況でした。
そこで、新しいAPIレスポンスから古いAPIレスポンス用のデータクラスに変換するコンバータをアプリ内に実装し、画面の修正はほぼなしでAPIのバージョンアップに対応するという手段を取りました。

これは新しいAPIレスポンスから古いAPIレスポンス用のデータクラスに変換するコンバータをアプリ内に実装する様を説明した画像です。

元からの実装の問題もあるため、その時の一時的な対応としては致し方ない部分かもしれませんが、それがそのまま長い間利用されてしまうこととなり、APIの修正時には新旧両方のAPIレスポンスに関して調査・設計する必要があり、余計な開発工数が掛かってしまっている状態でした。

4. 設計が統一されていない

こちらが一番大きな問題でした。

当時はメンバーの入れ替えや、開発に携わる人数が多かったこともあり、全体的に設計思想が統一しきれていない状況でした。
そのため、「依存関係が整理されていない」・「責務が分離できていない」などの課題があり、よくある「利口なUI(FatActivity/FatFragment)」であったり、Managerという名のついた様々な「神オブジェクト」が生まれていたりするなど、色々な課題がありました。

これはManagerクラスが複数存在する様を説明した画像です。

その他にも課題は色々

上記挙げたものは一部で、他にも様々な課題がありました。

  • View Bind ライブラリは Butter Knife (古いバージョン) を利用していた
  • Pub/Sub型のイベント通知ライブラリである Otto を色々なところで多用していたので、デバッグがしずらく、影響範囲の把握が困難
  • メンテナンスされていない独自フレームワークが存在

これらを一度に全てを改善することは難しいので、それぞれの課題と向き合った上で、どのように改善していくのか道筋を見つける必要がありました。

まずは Before / After

課題について説明させていただいたので、ここからはどのように変わったか変わったか・どうやって変えていったのかを記載していきます。

最初に、結果としてどのように変わったのかを数値で示せる観点でまとめましたので、もしこの結果に興味を持った方は、ぜひ続きの具体的な部分も読んでいただければと思います。

Kotlin/Javaのコード割合

年月 java code java % kotlin code kotlin % total code
2018年8月 187,952 91% 18,596 9% 206,548
2022年12月 149,884 40% 228,924 60% 378,808

先述した通り、私が入社した当初のKotlinで書かれたコードは全体の約9%という状況でしたが、現在では約60%(コード量だと約12倍)まで広がっています。
こちらの詳細については後述いたします。

複雑度

今回はJavaのコードはKotlinに書き換えるという前提で、Kotlinコードの複雑度のみを detekt というツールを使って計測しました。

年月 lloc total code mcc total code smells mcc per 1,000 lloc code smells per 1,000 lloc
2018年8月 12,367 2,846 1,950 230 157
2022年12月 163,533 24,371 5,741 149 35

それぞれの値の意味を以下に記載します。

  • lloc : 論理行(logical lines of code)
    • 空白行、コメント行を除く、実際のコードが書いてある行のこと
  • mcc : 循環的複雑度(cyclomatic complexity)
    • プログラムの複雑度を測る値で、初期値1で分岐がある度に1加算される
  • code smells : コードの臭い(wikipedia
    • 深刻な問題がありそうな兆候があることを示しています

lloc が13倍になっていますが、1000行あたりのmccやcode smellsが低下しています。
Kotlinで新しく書いた部分や、Javaから置き換えたコードの品質、そしてリファクタリングの効果が数値として出ているのがわかります。

ユニットテストの導入状況

私が入社した時はユニットテストは導入されていませんでした。

いきなり全体のカバレッジを100%目指そうとしても土台無理な話なので、ユニットテストを実装する箇所を絞って対応を進めています。
具体的には Clean Architecture や Layered Architecture でいうところのレイヤーで例えると Presentation/UseCase/Infra(Repository) の部分に重要なロジックがあるので、そこへの適用を行っています。
ただし、入社当時のコードはそのままだとテストコードを書くのがかなり難しい状態だったため、設計の刷新に合わせて、テストコードを書くようにしています。

現状のカバレッジは以下の通りで、まだまだPresentatioinレイヤーやRepositoryのC1に関してカバレッジが低いですが、徐々に増えてはきています。

レイヤー C0(命令網羅) C1(分岐網羅)
Presentation 30% 21%
UseCase 74% 64%
Infra(Repository) 62% 18%

クラッシュ率

品質という点で忘れてはならないのはやはりクラッシュ率です。

食べログAndroidアプリは多くのユーザーにご利用いただいています。
そのため、ちょっとした不具合が原因でも多くのクラッシュが発生してしまうこともあります。

参考までに、2022年12月現在での3ヶ月のクラッシュは2.7万件のうち、改善を行ったソースコードのパッケージでのクラッシュは436件となっており、全体の 約1.6% 程度になっています。 もちろん間接的な問題になっていることもありますので、パッケージで区切っただけではわからないこともありますが、ある程度の品質を保てている部分があることはわかるかと思います。

どんなアプローチを取ってきたか

いまだ道半ばではありますが、前述のような改善が進められました。
そこで、次は現在に至るまでどのようなアプローチをとってきたのかを説明したいと思います。

初期

入社当初はまず現状を把握することから始めました。
コードリーディングや、利用しているライブラリの確認はもちろんですが、iOSアプリは一度フルリプレイスを実施していたので、その際の設計や実際にかかった工数、周辺(企画、デザイナー、バックエンドetc)とのやりとりなど、色々な情報を収集しました。

その中で一番大事だと思ったのは現場の声で、当時Androidアプリの開発をしているメンバーだけでなく、iOSのメンバーもリプレイス後の悩みなどないか、ヒアリングしました。

そしてそれらで得た情報から、解決したい課題の整理を行い、理想系を固めつつ、そこに至るまでの道のりを検討しました。

まず、特にやるべききこととしては以下の3点にまとめられました。

  • 設計の刷新(リアーキテクチャ)
  • 開発環境の最新化
  • Kotlin化の促進

この中で特に難易度の高いのはリアーキテクチャで、具体的なアプローチ方法は一昨年にTECH HILLS #1というイベントで登壇させていただいた時の資料をもとに説明させていただきます。

アプローチ方法としてはビッグリライト(フルスクラッチ)か、リアーキテクティング・リファクタリングの2軸に分けられることが多いかと思います。
簡単にいうと全部作り直すか、既存コードを改善していくかになります。
それぞれのメリット・デメリットはこちらです。

これはビッグリライトとリアーキテクティング・リファクタリングのメリットデメリットを説明した画像です。

実際の選択としてはリアーキテクティングをしてからミニリライトしていく手法を選びました。

これはリアーキテクティングを選択したことを説明する画像です。 これはリアーキテクティングを選択したことを説明する画像です。

スライドに書いてあるような判断をしたのはもちろんですが、それ以外にも機能開発を止めるのは事業的にかなり厳しいことであったり、決まった期間ですべてをやり切るのは難しいといった点も判断理由の1つになります。

また、レガシーコードの改善を行う時によくある問題として、以下のようなものもあります。

他にも、性能目標が追いつかなかったりして、当初想定していた期日より時間がかかってしまうことはよくあります。
こういったことはレガシーコードのリプレイスに関わったご経験がある人は、似たようなことを経験したこともあるのではないでしょうか?

この時、期日が最優先事項になってしまっていると、綺麗にしているはずなのに汚い状態でリリースすることになってしまい、これでは本末転倒です。
もちろん期日を蔑ろにしたい訳ではないですが、期日より品質などを優先事項にうまく設定するために、こういった判断に至った部分もあります。

準備期間

次は本格的な負債の解消に向けての準備です。
まずは具体的には以下のような対応です。

  • 開発環境の最新化
  • Kotlin化促進のための規約作成など
  • テストコード導入に向けてフレームワークの選定など
  • CI環境の整備
  • リファクタリング

これらの対応はリアーキテクチャの方針に拘らず対応するべき内容だったので、初期の課題整理が完了した時点で、いくつかの準備作業は検討と並行して実施していきました。

そして、もう一点大きな課題として、人の問題がありました。
冒頭でも少し触れましたが、これらの技術的負債の返済へのアプローチを担当する基盤チームのAndroid専任エンジニアが私1人という状況でしたので、手が足りなすぎるという状況でした。

大きなことをするには仲間集めがとても大事です。
この仲間集めとは、採用活動だけでなくチーム内外とのコミュニケーションも含みます。
直接一緒に対応を進める仲間だけでなく、いろいろな協力者を増やしていくと、物事が進めやすくなることもあります。

これは仲間集めの重要性を説明した画像です。 ※ちなみに今の基盤チームAndroidメンバーは5名体制です。

設計の刷新(リアーキテクチャ)開始

ある程度人も増えてきて、方針も決まった段階で、いよいよリアーキテクチャの開始です。
最初は適度に機能も多くかつ、その当時の修正頻度がそこまで高くない画面を選定し、対応を進めました。
そういった画面を選定した理由はいくつかあります。

  • 最初なので実験的に進めたい
  • 機能が少なすぎると参考にならない
  • 修正頻度が少ない画面の方が修正中に仕様変更が少ない

こういった条件に合う画面を選定した上で、あとは進めながら細かいところは調整しつつ進める形で始まりました。

始めてから感じた課題はやはりリソースの確保です。

これはリソースの確保が重要であったことを説明した画像です。

ここについては今だにうまく解決できているわけではないですが、リアーキテクチャを進めるときはなるべく専念できるようにタスクを調整しています。
しかし、リアーキテクチャ以外にも対応すべき課題があるため、なかなか苦戦しているのが現状です。

思い返してみて

私が入社してから4年半が経ち、リアーキテクチャを開始してからは3年が経ちました。
振り返ってみて、やってよかったことや反省点についてまとめます。

やってよかったこと

開発環境の最新化やテストフレームワーク導入

やはり、開発環境は生産性に直結するので、最新化やツール導入はやっておいた方が良いです。
例えば、 Android StudioAndroid Gradle Plugin をバージョンアップすることで、ビルドパフォーマンスが向上したり、強力なツールが使えるようになったりします。
Kotlin についても、バージョンアップで良い言語機能がたくさんリリースされていますので、できるだけ新しいバージョンを利用した方が良いです。
とはいえ、それに伴い Deprecated になったり、削除されてしまうAPIもあるので、そことの兼ね合いではありますが。。。

また、テストコードに関しては、0からのスタートなので、まずはテストフレームワークの選定からでした。
自分の考えとしては以下の2点を重視して検討しました。

  • テストコードだからフルKotlinで書きたい
  • モダンなフレームワークを導入して、メンバーに関心を持ってほしい

また、食べログのサーバーサイドは Ruby で書かれているので、モバイル側のエンジニアでも RSpec ならテストコードを書いたことがあるという人もいたのも踏まえて、 SpekMockK を採用するに至りました。

そこからはリファクタリングをする際にテストコードを書いてから実施するなど、積極的に書くようにして、テストコードを増やしています。

これはテストコードが4年で飛躍的に増えたことを説明した画像です。

イベント 年月 テストコード
入社 2018年8月 0
最初のテストコード 2019年4月 445
リファクタリング開始 2019年11月 15,323
リアーキテクチャ開始 2020年1月 47,502
現在 2022年12月 83,199

その分、テスト実行時間が増えてしまうので、現在はCI環境ではGradleの並列実行設定を行い、なるべく効率化するようにしています。

tasks.withType(Test) {
    final Boolean onCI = System.env.CI != null
    if (onCI) {
        maxParallelForks = Runtime.runtime.availableProcessors()
    }
}

Kotlinのコーディング規約作成

歴史を振り返りながらその時のコード割合を出してみました。

これはJavaに代わりKotlinのコードに補完されていったことを説明した画像です。

イベント 年月 java code java % kotlin code kotlin % total code
入社 2018年8月 187,952 91% 18,596 9% 206,548
Kotlinコード規約作成 2019年4月 199,598 88% 27,795 12% 227,393
リアーキテクチャ開始 2020年1月 205,311 68% 96,043 32% 301,354
現在 2022年12月 149,884 40% 228,924 60% 378,808

こうしてみると、入社後からKotlinコーディング規約を作るまでの間は順当にコードベース(Java/Kotlin共に)が増えるだけでしたが、規約作成後は大幅にKotlinのコード量が増えています。

そもそもコードベースを減らした方が良いという議論はさておき、この段階ではまだJavaをKotlinに書き換える作業を推し進めてはいなかったので、これは単純にKotlinでの開発がしやすくなったということを示しています。

ちなみに規約と言っても、書き方を制限するようなものではなく、ベースはAOSPの Kotlin Style Guide に準拠しつつ、記載のない項目についての注意点などをまとめたものにしていて、少しでもKotlinでの開発がしやすくなるようにまとめました。
また、Java/Kotlin相互運用時の注意点をまとめたのも良かったかと思っています。

そして、現在ではJavaコードの画面をKotlinでリアーキテクチャを実施し、JavaとKotlinの割合が逆転するに至っています。
とはいえ、まだまだ6割程度なので、今後も積極的に推し進めていきたいと思っています。

ちょっと失敗したと思うこと

Kotlin Android Extensions : Synthetic views

リアーキテクチャを開始する時に、View Bind ライブラリに何を選択するかものすごく悩みました。
ちょうど、当時のAdventCalendarでこちらについての記事も書いていたのでよかったらご参考までに。

この時、ViewBindingがまだベータ版だったために、個人的に書き味が好きだった Kotlin Android Extensions の Synthetic Views を採用しました。

しかし、その後ViewBindingがStableリリースされ、Kotlin 1.4.20では Synthetic Views は非推奨となりました。
結果論ではありますが、将来性を考えてGoogle公式からリリースされるViewBindingをベータ版だったとしても採用した方が良かったと思っています。

過ぎたことは致したかないですが、リアーキテクチャ時に Synthetic Views で実装を進めていますので、ViewBinding に移行する必要がでてきました。
この際にリアーキテクチャ済みの画面は単純作業で置き換えることができたのに対し、整理されていない画面はやや大変だったので、皮肉にも Synthetic Views を採用したことにより、リアーキテクチャの効果を肌で感じることができました。

ViewBindingへの移行に関しては AdventCalendar2022 で弊チームのメンバーが記事を書いていますので、よかったらご参照ください。

マルチモジュール化

続いて、マルチモジュール化です。
入社当時は依存関係が整理されておらず、少しリファクタした程度ではモジュール分けすることも難しい状況でした。 また、リアーキテクチャを進めて行っても、 Manager クラスとの依存関係を断ち切るのはすぐにはできない状況だったため、一旦モジュール分けを後回しにして進める判断をしました。
その他にも、相互依存している画面(例:店舗詳細画面と口コミ詳細画面が行き来できるようになっている)があったり、機能単位の部分をうまく分けるところが整理しきれてなかったのも要因の一つでした。

ただ、ここに関してはAndroidアプリの場合はモジュールを分けることで、依存関係を強制できるので、かなりメリットがあると考えています。
ですので、最初からマルチモジュールにするというよりは、リアーキテクチャをしながらモジュール単位でも分けられるように進めてもよかったかもしれないと思う面もあります。

しかし、その分リアーキテクチャをする際の既存コードの依存関係を整理するリファクタリングも実施する必要がでてきてしまい、難易度とコストがやや上がるので、その分設計部分をクリーンにする作業が遅れてしまう面もあるので、判断の難しいところだと考えています。

今後について

リアーキテクチャをしたからといって、今後技術的負債が溜まらないわけではなく、気づけばどんどん溜まっていきます。
近年では非同期処理を RxJava/RxKotlin から Kotlin Coroutines へ移行する流れも多くありますし、UIの実装はXMLではなく Jetpack Compose (宣言的UI)を利用するシーンも増えてきたかと思います。
こういった新しい技術が、より開発効率を向上させたり、品質を良くできるなら、移行することも考えていくべきです。

食べログAndroidアプリでも、現在 RxJava/RxKotlin から、Coroutinesへの移行を段階的にできる設計で進めています。
この段階的移行はリアーキテクチャを進めている画面だけが適用できる状況です。
※リアーキテクチャ前の画面は設計的にも言語(Java)的にも導入が難しい

こういったことが可能になっただけでも、リアーキテクチャしている価値が感じられます。今後も、開発効率を向上させるようなモダンな技術を実験的に導入できるような構成を維持し、積極的に導入できるようにしていきたいです。

また、現在はユニットテストこそ徐々に実装されてきていますが、まだまだ品質保証の際には手動テストに頼っています。技術的負債の返済だけでなく自動テストなどの仕組みをもっと構築し、品質の担保をより低コストでできるようにしていきたいとも考えています。

まとめ

今回は食べログAndroidアプリの技術的負債をどのように解消しているか紹介させていただきました。
食べログアプリほどの規模だと、コードベースが大きいため、一つ一つの対応のためのコストが増えがちです。例えば、リアーキテクチャなどは数ヶ月で終わるような話ではありません。
長い時間をかけて対応していれば、それだけ様々な変化の影響を受けることもあります。例えば、利用しているフレームワークなどは流行り廃りの影響でサポートされなくなってしまうことがあります。
上記は外部要因の変化ですが、内部的にも対応を進めることで新たな発見により、変化を受け入れる必要があるときがあります。

最初にすべてを決め切って最後までやるというのはかなり難しいので、状況が変わったら思い切って方針を変えるのも手かもしれません。(ケースバイケースですが)

大きな対応ほど、しっかり設計して進めるので、どうしてもこだわりたくなる気持ちもあるかもしれませんが、それはかえって負債を作り込むことになりえます。
そのため大事なのは、変化に強くなる・変化を受け入れることだと感じています。

システム的な面では、依存関係や責務を整理して、変更容易性が高ければ状況の変化にも対応しやすくなります。また、人としても状況の変化を受け入れる姿勢でいた方が、自身も成長し続けられるし、むしろモチベーションも高く保てる気がします。

技術的負債って聞くとネガティブなイメージが強いですが、その負債に気がついたということ自体は、自分の成長であったり学びがあってこそなので、ポジティブなものだと思います。
システムも人も成長のチャンスと思って、変化を楽しめるようになると良いのかなと思います。

食べログAndroidアプリ開発にご興味を持っていただけた方はぜひ気軽にお声がけください! カジュアル面談も大歓迎ですので、ご希望の方はフリーテキストに、「カジュアル面談希望」と記載ください。