株式会社ビズリーチに今年4月入社した立柳紀林(タマセン)です。
10月頃から内定者インターンとして働き始めて、3月末までBizHint事業部でiOSアプリの開発をしていました。
エンジニアとして内定をもらいましたが、アプリ開発未経験なので実務経験を積むため(と、半年留年しちゃって変な時期に卒業することになったので)、お願いして、インターンとして働かせてもらっていました。
2023年12月、株式会社ビズヒントの全保有株式をスマートキャンプ株式会社に譲渡しました。
インターンのある日
エンジニアのホリさんが、新宿駅山手線ホームでBizHintのアプリを起動すると通信中のぐるぐるが止まらなくなって、それ以後操作できなくなってしまうことを発見しました。
新宿駅山手線ホームではかなりの確率で “ぐるぐる問題” が発生するようで、同じように通勤中にアプリを使ったユーザーは同じような体験をしているはずです。 BizHintは、ビジネスパーソンを対象としたニュースアプリであるため、通勤中のユーザー体験の悪さはとても大きな問題でした。
この問題を解決せよというタスクがぼくのところに回ってきたのです。
何をすればいいか考えた
最初 “ぐるぐるがとまらない” とは一体どんな現象なのかわかりませんでした。
しかし、ネットワーク状況が悪い環境で起こりそうだと予想できたので、機内モードにしてアプリを起動してみると “ぐるぐる問題” が発生し、操作不能になることが再現できました。
デバッグモードで動作をたどっていくと、画面表示処理の途中で処理が止まっています。その箇所は通信処理で、ネットワーク状況が悪いとサーバーとの接続成功を待ち続けているようです。
結論として、“ぐるぐる問題” を解決するためには、タイムアウトで通信を失敗させれば良いとわかりました。 それをホリさんに伝えたところ、3点の要求がありました。
-
- ネットワーク状況が悪いことがエラーの理由だとユーザーに伝えてほしい
-
- ネットワーク状況が悪い状況下の動作を確認するテストを書いてほしい
-
- ネットワーク状況が悪くても通信成功確率が上がるよう実装を工夫してほしい
通信には Aramofire
を使用していたので、通信処理をエラー終了させることはタイムアウトの設定で実現できそうでしたが、それをどうユーザーに通知すべきなのか、どうテストを書いていいのかがわかりませんでした。
そこでメンターのとみーさんに相談して、アプリのアーキテクチャを把握するところからはじめました。
BizHintのiOSアプリはSwiftで書かれていて Model -> ViewModel -> View
という構造になっています。
Modelで LocalDBからデータの取得やAPI通信などを行い、ViewModelにはビジネスロジックが書かれています。ViewModel層ではViewのステートを管理していて、ステートに応じて画面の表示内容が変化します。
1.ユーザーにエラーを通知する
には以下の3STEPが必要だとわかりました。
Alamofire
にタイムアウトを設定する- ViewModelが Modelから通信エラーを受け取ったらViewのステートをネットワーク不安定エラーにする
- Viewはステートがネットワーク不安定エラーになったらエラーダイアログを表示する
2.エラー状態のテスト
は、ViewModelに通信エラーが渡ったときにViewのステートが正しく変化することを確認するテストを書くことにして、次の2STEPを行います。
- ViewModelがModelの実装に依存しないようにProtocol化し、テスト時はModelのモックから通信結果を渡すようにする
XCTest
とRxTest
でテストを書く
テストをするためには、様々な種類の通信エラーを返すModelが必要ですが、ネットワーク状況を都合よく悪くすることが難しいため、Modelのモックを作ってエミュレートすることにしました。そこで、既存Modelの定義をProtocolにして、本番とテスト用のモック、2つの実装を用意します。
3.通信成功確率の上昇
は、Model層の通信に RxSwift
の retryWhen
を挟むことで実現します。
1. ユーザーにエラーを通知する
1.1. Alamofireにタイムアウトを設定する
URLSessionConfiguration
にタイムアウトを設定し SessionManager
に渡しました。
|
|
1.2. エラーの通知処理を作る
まずは ViewModel
がステートとしてエラーを持てるようにします。
|
|
enumの引数で原因となっているエラーのインスタンスを渡せるようにしました。
ViewModel
はModelからエラーを受け取ったらステートを エラー
に変更します。
1.3. Viewでエラーを通知する。
View
はもともと ViewModel
のステート変更によってUIを切り替えるようになっているので、 エラー
に変更されたら View
でエラー内容のアラートを出すことにしました。
将来アラートからエラー用のUIに切り替えることになっても、ステートを見てViewを変更しているだけなので、修正範囲は View
内で完結するはずです。
2. エラー状態をテストする
2.1. ViewModelが Modelの実装に依存しないように変更する
Model
のプロトコルを定義をすることで依存度を下げます。
このようにすると、 ViewModel
は ModelProtocol
にしか依存しなくなり、コンストラクタで実装を差し替えることができます。
|
|
2.2. 通信エラー処理のテストを作る
ViewModel
内で Model
が通信エラーを返したときに正しくステートが変更されるかを XCTest
と RxTest
でテストします。
まずは ModelProtocol
に準拠する形で ModelMock
を実装します。
テストのために、API通信で得られるデータの流れを RxTest
の TestableObservable
で置き換えます。
ModelMock
内の getData
はレスポンスからもらったデータを取得するメソッドです。
|
|
モックが完成したら ViewModel
にモックを渡すユニットテストを書きます。
|
|
これで正常に通信がエラーになるユニットテストが書けました。
実際にはUIからのリトライ処理などが挟まって状態がもう少し複雑になるので、テストできると安心してリリースできます。
3. リトライ処理
通信の成功確率を上げてほしいという要求は、API通信部で RxSwift
の retryWhen
メソッドを呼ぶことで実現しました。
APIInvoke
は Alamofire
による通信をWrapして Observable<Response>
を返すメソッドだと思ってください。
|
|
retryWhen
は少し分かりづらいですが、エラーが流れてきた場合にのみ反応するメソッドです。
retryWhen
が引数でとっているクロージャは 流れてきたエラー -> リトライ条件
で、リトライ条件の Observable
が発行されるとリトライを行います。
今回はネットワークのエラーが発生したら3秒ごとにリトライを試行し、3回失敗したらエラーをobserverに流しています。
学んだこと
ぼくは学生時代に研究用のスクリプトを書く程度で、アプリケーション開発やチーム開発は未経験でした。
そのため、アーキテクチャや疎結合、可読性など言葉だけは知っていたものの、それらが開発時にどんな体験をもたらすのかいまいちピンと来ておらず、本の中で説かれていることの価値を本質まで理解できていないという感覚をずっと持っていました。
BizHintのインターンでは、課題の特定、アーキテクチャの理解、機能の実装、テストの実装といった一連の開発を任せてもらいました。
そして、経験を通して、このようなことを学びました。
- コードを読んで理解することの重要性
- 複数人で開発するときには可読性の高いコードをかく必要があること
- レビューは、コードの品質を担保する役割であること
コード量が多くなっても、アーキテクチャがしっかりしていると、どこに何を書けばいいのかすぐにわかるというのも非常に大きな学びでした。
疎結合にすることで、変更しやすくなったり、コードを書いているときに余計なことを考えなくて済むといったありがたみも体験できました。
文中にでてるひとより
新宿駅では弱々しいWi-Fi拾っちゃうんです。特に山手線ホームで。
昨今、通信環境は強く安定的なので意識する機会は少ないですが、堅牢な通信処理を実現できてこそアプリエンジニア、との思いでタマセンに頑張ってもらいました。 〜 ホリ
インターンを始めたときはアプリ未経験でしたが、インターン終盤ではアプリ開発のすべてを任せられるようなりました。配属後の活躍が楽しみです。 ちなみに五反田駅は空いているため気が付きませんでした。渋谷勤務で山手線沿線に住むなら渋谷~五反田間が空いていておすすめです。 〜 冨永