ハンドメイドマーケット「minne」アプリを担当

伊藤拓海氏(以下、伊藤):「中~大規模アプリのminneはどうアーキテクチャを選定したか」ということで、GMOペパボの伊藤が発表したいと思います。よろしくお願いします。

まず軽く自己紹介します。伊藤といいます。よろしくお願いします。GMOペパボではパートナー(GMOインターネットグループに置ける従業員の呼称)同士をあだ名で呼び合う風習があります。僕は名前が拓海なので「tick-taku」と呼ばれています。

もともと関西出身ですが、2020年12月にGMOペパボに入社して、そのタイミングで関東に引っ越しています。新卒で前職に入社して、初めてJavaを触って、その流れで初めてAndroidのプロダクトを担当しました。そこからだいたい今と合わせて、4年ぐらいAndroidアプリのエンジニアをやっています。

AWSの認定資格を取っていて、好きな言語はKotlinですが、モックサーバーとかを作ったりしていて、その時にPythonを使っていたので、Pythonも好きです。

趣味は広く浅くいろいろとやるのですが、自転車が好きで、最近はちょっと体重が気になっているので、1日15キロぐらいを目標に自転車で走っています。そのほかいろいろと日々の煩悩などを吐き出すTwitterアカウントがあるので、よければチェックしてみてください。

まず、「minne」について、さらっとだけ説明させてください。

minneとは、ハンドメイドマーケットサービスで、ハンドメイドの作品をオンラインで売ったり買ったりできるサービスです。僕も1ユーザーとして使っていて、きれいなインテリアだったり、実用的な雑貨とかとたくさん出会えるので、とても楽しいアプリサービスになっています。最近マイホームの表札をminneで買いました。もし興味があればぜひ触ってみてください。

というところで本題に入っていきたいと思います。今日はこんな感じで話していこうと思います。時間も短いので駆け足になるところもあるかもしれませんが、よろしくお願いします。

前提としてはこんな感じで進めていきます。今回は学生の人もたくさんいらっしゃるとのことなので、想定のリスナー層はAndroid初級者~中級者向けで話していきたいと思っています。

アーキテクチャを採用するメリット

では本題に入っていきますね。「中~大規模アプリのminneはどうアーキテクチャを選定したか」ということで、Web界隈では、政治や宗教や野球やアーキテクチャの話は論争があるのでけっこうタブーとされていたりするのですが、今回はその中のアーキテクチャの話をしようと思っています。

この話でみなさんが「自分やチームにとってより良いアーキテクチャは何だ?」と再考するきっかけになれば、今日の僕の狙いは達成かなと考えています。あとは「GMOペパボってAndroidやっているんだ」と思ってもらえたらうれしいですね。

というところで、まず「なぜアーキテクチャを採用するのか?」というところから深堀りしていきたいと思います。

アーキテクチャを採用する理由は、一般的にいろいろとあるとは思うのですが、主に5つピックアップすると、まず1つ目は、レイヤーごとに責務を置くことで、何をするかが明確になる。実装をある程度強制できて、レイヤーごとにクラスが分かれるので、再利用性が高まるということが挙げられると思っています。

2つ目。各レイヤーを実装ごとそのままモジュール化できるようになります。Androidアプリでは、マルチモジュール構成にするとリソースの管理が楽になったり、各モジュールがパラレル・ビルドになるのでビルド時間の短縮にもつながったり、開発スピードの向上にもつながっていきます。

3つ目。責務が明確になるので、どこで何を担当しているかがパッとわかります。なので、アプリケーションのステートだったり必要なデータをコードから把握することが楽になります。

4つ目。アプリケーションの解析・把握がしやすいため、機能追加や変更の時にどこをどう修正すればいいかがわかりやすく、迅速な機能の追加・修正につながります。

5つ目。特にAndroidでは、Android依存のクラス、Activityだったりが出てくると、途端にユニットテストが書きづらくなりますが、責務ごとにレイヤーやモジュールに分けているとAndroid依存のクラスと関係ない処理のテストが書きやすくなります。つまり、品質が担保できるということになります。

これら5つを言い換えると、それぞれがシステムソフトウェア品質の保守性の副特性に当てはまると思います。これらすべてが向上するということは、すなわちアーキテクチャを採用することが保守性を高めるというところにつながると思っています。

これをもう少し踏み込んで言うと、アプリケーションのアジリティを高める。それすなわち、ユーザーへより高品質なサービスを迅速に提供するためにアーキテクチャを採用する、と僕は考えています。アーキテクチャの採用は、開発者が実装しやすくするためのものですが、巡り巡ってユーザーへの価値提供につながると考えています。

アジリティを高めるには当然保守性だけではだめですが、アーキテクチャパターンに沿って実装することで、一定水準の保守性は担保されるようになるので、積極的にアーキテクチャを検討していきましょう。

「minne」におけるアーキテクチャ採用の歴史

今日僕が一番言いたいことはこれなのですが、では実際minneではどうなのかというところを話していきたいと思います。その前に、minneではいろいろアーキテクチャを採用してきた経歴があるので、minneのアーキテクチャの変遷を時系列で古い順に追いながら、Androidでよく採用されているオーソドックスなアーキテクチャパターンをざっと紹介していこうと思っています。

まず、これはアーキテクチャパターンでもなんでもなくて、なにも採用していないという混沌の時代がありました。何かというと、Activityの中で全部やっていました。Activityの中ですべてやって、これに似た画面を作る時は別のファイルにそのままコードをコピペして作るという、負の遺産が生まれているのが見受けられました。

これではまずいということで、「MVC」と呼ばれるアーキテクチャパターンを採用します。 MVCは何かというと、実装を3種類のレイヤーに分けます。

それぞれ、サンプルコードをベースに見ていくのですが、Modelは、ビジネスロジックと言われるアプリケーションのUIに関係ない部分の操作を担当するレイヤーです。ViewがModelのデータを取得して表示したり、Controllerから更新処理を受け付けたりします。Viewからの操作方法はないので、そこは注意してください。

続いてViewです。Modelのインスタンスを持っていて、データ取得したりControllerへ通知したりという責務を担います。

Controllerは、それを受けてModelの更新をしたり、Viewに描画の依頼をかけたりします。このへんのUIに関係ある部分は、プレゼンテーションロジックと呼ばれたりしますが、そこを担当するレイヤーです。

MVCによって実装を3種類のレイヤーに分けたことによって、これらのメリットが得られると考えています。

ただし、AndroidアプリでMVCを採用する時にみなさん悩むポイントが1つあると思うんですよね。Activityを今の例ではControllerでサンプルを示したのですが、「ActivityはViewなの? Controllerなの?」という問題が発生します。

ActivityはControllerとして紹介はしましたが、「UI操作しますよね。レイアウト配慮ありますよね。それはもはやViewじゃん?」というところで、「MVC的にControllerはユーザーからのActionを受け付けることもあるよね。ということは、ActivityはViewとController両方やるのでは?」みたいなことにつながっていきます。

これはひとえにActivityの概念が広すぎることが原因として考えられると思っていて、その結果、UIロジックとプレゼンテーションロジックが同居するActivityになる。つまり、FatActivityになる。この場合テストが書きにくいので、実際にビルドして確認するまでロジックが正しいかがわかりません。

というところで、「もしかしてMVCは規模が大きいAndroidアプリには向いていないのでは?」という結論になりました。そこで解決策として、2つの制約を導入していくことになります。

まず1つ目。MVPと呼ばれるパターンです。MVPは、MVCと同じように実装を3つのレイヤーに分けて実装します。何が違うかというと、この2点です。

前提として、MVPはViewとPresenter間がインターフェイス越しのコールバックになります。インターフェイスにすることによって、より疎結合になっているという特徴があります。

ViewとModel間のデータフローがなくなったので、Presenterが必ず仲介するというデータフローの変更が1つあります。Viewの中ではModelのインスタンスがなくなって、Presenterの参照のみになりました。MVPではActivityはViewに入るので、さっき出ていた「Activityはどこだ?」みたいな問題が解決されます。

次にPresenterですが、Presenterは、ViewからのActionを受け取って、必要であればModelを操作してデータを取得して、Viewを操作します。Controllerと似たようなことをしますが、データフローがPresenterに集約されることになります。

MVCと比べて一番大きなポイントは、先ほど言ったようにデータフローが単一になったことによって、Presenterを見ればだいたいのアプリケーションのデータの流れを追える、というメリットが得られることです。ただし、インターフェイスによる抽象化によって冗長な実装が出てきたりするので、その分どうしてもMVCに比べて実装コストは上がってしまいます。仕方ないけど、というところですね。

なので、「規模が大きくなれば、その分データフローや責務の強制みたいなことが追い風になって、長期的に見るとMVCより実装スピードは上がるけど、短期的に見るとちょっと落ちるかもね」というところが挙げられると思います。

もう1つが、MVVMです。これは最近Androidアプリ開発を調べた方ならご存じかもしれませんが、Googleが推奨しているアーキテクチャパターンです。

違いは、ViewとViewModelの間がData Bindingと呼ばれるObserverパターンになっているところです。

Viewは、ViewModelのインスタンスを持って、Observerパターンによって通知されてきたデータを反映します。ViewModelは、ViewからのActionをハンドリングしたり、Viewのステートを管理する役割を持っています。

MVPと比べて何が違うんだというところですが、Googleからのサポートが得られるというのが一番大きなメリットではないかと思います。ただし、当然そのData Bindingの勉強をしないといけなくなるので、その分の学習コストはどうしても発生してしまいます。

というところで、Androidでオーソドックスなアーキテクチャパターンを紹介しました。ほかにもFluxなどあると思いますが、今回は時間的にこの3つに絞ってお話を進めていこうと思っています。

それぞれのアーキテクチャの採用基準

では「どう選定するの?」というところで、個人的なお話にはなりますが、僕は迷ったらごちゃごちゃ考えるより先に「これをもとにやってやる!」みたいなアーキテクチャの採用基準が自分の中にあって、それぞれこんな感じで採用していこうかなと考えます。

MVPやMVVMは、ある程度自由にライブラリが使える環境であれば、公式であるGoogleがサポートしてくれているJetpackとMVVMが相性がいいので、今からアプリを作るとなったら乗っかっておくのが長期的に見てもコスパがいいのではないかなとは考えています。ただ、どうしてもライブラリがNGという環境は存在して、その場合はViewModelの構築を自分でしないといけなくなるので、実装コストを考慮してMVPを選ぶというのもあると思います。

あと、Presenterはインターフェイスによるコールバックなので、アプリケーションのデータの流れが直感的に追いやすいというところで、実装スピードや納期を気にする場合にはMVPを採用することもあると思います。

こんな感じで、アプリケーションの規模や抱えている課題によって変わることはあるのですが、まずはこんな感じで始めてみて、その中で課題を見つけてより良いアーキテクチャに昇華していけるといいのかなと思っています。

「minne」ではMVVM+UseCase+Repositoryの3層Layered Architectureを採用

では本題です。minneではというところで、まずminneアプリの特徴はこんな感じです。

これらを踏まえたうえで、minneではMVVM+UseCase+Repositoryの3層Layered Architectureを採用しています。

依存関係を図に示すとこんな感じです。機能モジュールはUseCaseを、UseCaseはRepositoryを参照しています。

いきなり出てきたので、「RepositoryやUseCaseはなんぞや?」というところで、それぞれ解説していこうと思います。

Repositoryとは何か

まずRepositoryですが、RepositoryとはRepositoryパターンと呼ばれるデザインパターンの一種です。アーキテクチャの話の中で出てきたModelに該当するレイヤーで、とりわけデータアクセスを担当するレイヤーをRepositoryと呼びます。

なぜRepositoryを採用するのかというと、理由は大きく3つあると思っていて、まず、データアクセスを再利用するというところが挙げられると思います。2つ目、データの取得時に「どこからデータを取ってくるか?」みたいなことをPresentationレイヤーから隠蔽して意識させないようにすることが挙げられます。これによってPresentationレイヤー以下のコードをシンプルに収められると考えています。

最後、これが一番大きいかなと僕は思っているのですが、データアクセスにおいてさまざまなクライアントライブラリを使うことがあると思います。例えばminneでは、REST APIにRetrofit、GraphQLにApollo Androidというクライアントライブラリをそれぞれ使っていて、そのライブラリの依存がPresentationレイヤー以下にはみ出ないようにするという狙いを持っています。

「Presentationレイヤーはどこからどうデータを取ってくるかを知る必要はなくて、データさえ取れればいいから、無駄に依存を漏らすことはいいこととは言えませんよね」という考え方です。

もしライブラリに破壊的な変更があった場合でも、Repositoryのレイヤーの修正だけで済むようになります。これらのためにRepositoryパターンを採用して、データアクセスに関する保守性を高めています。

例としてはこんな感じです。外のレイヤー、Repositoryに対してフェッチすると、どこから取得してくれるかはわからないけど、データが取れるので、データをどう使うかというところに集中できるようになります。

minneのアプリの特徴として、このあとのUseCaseレイヤーでもそうですが、各レイヤーのデータのやりとりは、独自のresultクラスみたいなものを作ってデータのインターフェイスを統一して、成功や失敗を通知しています。リクエストが失敗した時に伝えるエラーは、ApolloExceptionとかを使うと依存関係が漏れてしまうので、独自のネットワークエラーを定義して、Repository以降にはみ出さないようにしています。

Repositoryパターンで一番大事なのは、あくまでプレーンのデータの取得に留めることかなと考えています。Repositoryは再利用されることを考慮して作るので、データを変形したりするのは外のレイヤーに任せようと思っています。

では、Repositoryの外のレイヤーはというと、UseCaseと呼ばれるレイヤーです。

UseCaseとは何か

「UseCaseとは何か?」ですが、クリーンアーキテクチャと呼ばれるアーキテクチャ思想の中に単語が出てきます。クリーンアーキテクチャの中では「アプリケーション固有のビジネスロジックを担当するレイヤー」と書かれています。これは概念図なので、パッと見ると少しわかりにくいかもしれません。

この概念図を実装ベースに置き換えた図がこちらです。おそらくAndroidのクリーンアーキテクチャの話では、一番有名なGitHubのリポジトリではないかなと思います。先ほどRepositoryでお話ししたマッピングを実行するアプリケーションに必要なビジネスロジックを担当するレイヤーとしてUseCaseを添えています。

「3層Layered Architecture」と言ったのですが、こういう感じで3層に分かれているので「3層Layered Architecture」という名前がついています。

「なぜUseCaseがいるのか?」ですが、おそらくRepositoryパターンを採用したチームでは必ず考えると思います。minneでは、Repositoryはデータアクセスをするレイヤーです。再利用されることを想定して作るべきなので、プレーンなデータの取得に留めます。

ということは、結果、アプリケーション固有のビジネスロジック、つまりマッピングだったりバリデーションだったりがViewModelに入り込むことになってしまって、結局FatActivityがFatViewModelに変わっただけになります。しかもビジネスロジックであるModelの部分がViewModelに侵食しちゃっているので、これは良くない。

というところで、解決策としてUseCaseが出てきます。

実際の例は、こんな感じです。例えば、minneでは画面表示のタイミングで、複数のエンドポイントにリクエストします。そういう時の待ち合わせをViewModelでやってしまうと、それはアプリケーション固有のビジネスロジックになるので、それをUseCaseとして切り出します。UseCaseで1つにまとめて、ViewModelが機能要件に集中できるようにしています。

minneでは、クリーンアーキテクチャはUseCaseがアプリケーションに依存するビジネスロジックと定義されています。アプリケーションに依存するということは、画面の要件に依存すると捉えて、ほぼ機能モジュール、ViewModelとUseCaseは1対1で作ろうとしています。

UseCaseとRepositoryを採用した結果得られたメリット

minneは長い歴史のあるアプリで、このアーキテクチャを採用し始めたばかりです。なので、実際のプロジェクトは最初に紹介したアーキテクチャ全部が入り乱れるキメラティックアーキテクチャみたいになっているのですが、それを前提に、今回UseCaseとRepositoryを採用したところと比べてお話ししていければと思います。

まずデータアクセスが再利用できたり変更に強くなったというところです。minneでは多くの画面が存在していて、例えば作品情報データなどはたくさんの画面が必要です。Repositoryが採用すると、例えばRESTからGraphQLに変わった時も、ライブラリの依存関係が外のレイヤーに漏れていないので、変更箇所はRepositoryの中だけで収まるから、移行コストがグーンと下がります。

先ほどの例だと、UseCase以降のレイヤーに被害が及ぶことがないので、(スライドを示して)これがこれになるだけでアプリケーションはGraphQLに移行できたと言えます。ライブラリのレスポンスやエラーを使っていると、外のレイヤーにも修正作業が入るので大変かなと思います。

また、より細かい粒度でモジュールが作れたというところ、ViewModelの見通しが良くなったというところ、機能追加・修正の見積もり/実装がしやすくなったというところ、最後に、さらに細かくテストを書くことで品質の担保につながっているというメリットが得られたと思っています。

冒頭で話したように、保守性の副特性のメリットが得られています。なので、結果、アジリティが高まったと考えています。

実際の計測はしていないので体感なりますが、採用してみて見積もりに時間がかからなくなりました。スクラムなのでポイントで計算しますが、MVCやMVPの画面に比べて工数は2~3ポイントぐらい減少していると感じています。今後がっつり移して、きっちりファクトを計測できたらいいなと考えています。

ただ、アーキテクティングにゴールはありません。特にアプリの規模が大きくになるにつれてアジリティに対する絶対値が上がるので、あれらのパターンをベースに、さらに詳細なアーキテクティングを継続していく必要があると思っています。

ということで、これを機にAndroidアプリやってみたいという人が増えて、Android界隈がますます盛り上がってくれることを願って締めとします。

「LET’S ENJOY ANDROID!」ということで、伊藤がお送りしました。ご清聴ありがとうございます。