RoomClip 開発者ブログ

[iOS]特定のイベントを他のViewControllerに(安全に、楽に)伝える

オハツのカキコです☆★☆( ´ ▽ ` )ノTunnelでiOSアプリの制作を担当している富樫です。

さて、本題に移ります。RoomClipでは、投稿されている写真に対して「いいね!」したり、「フォルダに保存」したりすることができます。
それらの機能をスムーズに楽しんでもらうためには、いくつか実現しなければならないユーザ体験があるのですが、その中でもとても基本的でとても渋いものとして、「ある画面で一つの写真に対して『いいね』『保存』などを行った時に、他のスタックしている画面に戻った時にその画面でも該当写真の状態変化が反映されているようにする」というものがあります。以下はイメージ図です。


iOSではこれを実現するために、NSNotificationCenterと、UIViewControllerのpostNotificationを使って別ViewControllerにイベントの通知を行ったりもしたのですが、

  • それぞれのアクションを行った後にpostNotificationする時、いちいち通知の仕様(パラメータの正確な変数名など)に気を配る必要がある
  • 通知を受け取る各画面で、通知のパラメータの仕様(パラメータの変数名や型)に気を配る必要がある

ということがしんどかったので、開発メンバーに相談を行いつつ「安全に、楽に」他画面に通知を出来る仕組みを自前で作りました


通知をするために新たに作ったclass達

醜い図(mini quiz)で恐縮ですが、以下のような構成で4つのclassを準備しました。

NotificationManagerはシングルトンのクラスになっていて、アプリの中で一つだけ存在するようになっています。
NotificationManagerの中でEventは、実際は複数種類のものが存在しており、それぞれTが異なっています。
Tは通知のために必要なパラメータの型が当てられていて、EventsオブジェクトはNotificaionの中で下記のように定義されています。

上記の例の「AddLikeParam」は、いいねをした時に他の画面に伝える必要な情報(いいねした写真のIDなど)を包括しています。


実際の実装


通知を受け取る必要がある画面では、NotificationManagerのなかの然るべきイベントにsubscribeする。

subscribeの引数として、通知を受け取った際の挙動を記述したblockを充てます。
ちなみに、これらの処理は全てUIViewControllerの中に記述するものとしています。


①を行ったUIViewControllerではsubscriptionをフィールドに保持しておき、UIViewControllerが破棄される際には、必ずsubscriptionをunsubscribeする。

Subscriptionはunsubscribeを呼び出すことにより、自身を破棄します。これを、UIViewControllerのdeallocなどで呼び出します。そうしなければUIViewControllerがリークしてしまうことがあります。


通知をPostする必要がある箇所で、然るべきパラメータを準備し、postを行う

これにより、①で記述したアクションが実行されます。
ちなみにviewControllerもPOSTしているのは、ある画面が送った通知を、その画面がそのまま受信するケースがあります。
そして、他画面からの通知と自分の画面からの通知とを区別してハンドリングする必要があるケースもあるからです(①のコードでvc === wselfと記述しているのがそのケースです)。


Eventsでジェネリクスを用いて安全に、迷わないように。。

さて、つらつらと書いてまいりましたが、これまで書いた中で大事だったのはEventsでジェネリクスを用いたことだと思っています。
前述の通り、Eventsの「T」はアクションに対応した通知に必要なパラメータです。
NotificationManagerの中で各アクションに対応するEventsオブジェクトを定義する際に「T」の型もきっちりと定義しておくことで、

  • それぞれのアクションを行った後に通知をpostする時、いちいち通知の仕様(パラメータの正確な変数名など)に気を配る必要がある
  • 通知を受け取る各画面で、通知のパラメータの仕様(パラメータの変数名や型)に気を配る必要がある

といった、前述したヒヤヒヤ感がなくなりました。


以上になります。長文失礼致しました。うーぬ、blogって大変ですね、また次回書く時も頑張ります。。。。


この記事を書いた人:buscemi

ルームクリップ株式会社のiOSエンジニアです。 Steve Buscemiに似てます。


外部サービスを利用した機能実装の際に注意しないといけないこと。

こんにちは。Tunnelにてエンジニアをしてます阿南です。
最近は、WebやAPIまわりを中心にやっております。

RoomClipの技術ブログのリリース早々に自分の順番が回ってきたので、拙筆ではありますが、技術ブログを書かせていただきます。



RoomClipではユーザーは投稿した自分の写真に含まれる商品検索して、アイテムタグとしてその写真にタグ付けすることができる機能があります。

この商品検索の際、各ショップが提供しているAPIを利用して商品検索を行っているわけなのですが、外部から提供されているサービスを利用する時って、自分が実装した部分の範疇にないところで問題が起きたり、サービスの定める仕様に左右されたりと何かと問題があるんですよね。

このアイテム検索の機能拡張でも同様で、直面したのは以下の問題でした。

  • 外部のサービスのシステム障害やメンテナンスモードなどにより、自社のサービス(アイテム検索機能)の一部が利用できなくなるケース

  • 外部のサービスが定めるリクエストの制限数に達してしまい、一定時間リクエストが受理されないようなケース

このようなケースの解決策として、サーキット・ブレーカーなるアイデアがあるらしく、それを元にしてアーキテクチャを構成することで、外部APIを利用した際に発生した上記のような問題を、回避・解決した話をしたいと思います。

サーキット・ブレーカーとは?


簡単にまとめますと

  • クライアントとサーバーの間に入り、クライアント→サーバーへのリクエストや、サーバー→クライアントへのレスポンスを監視。

  • 障害など発生し、一時的に利用ができなくなってしまったサービス(今回の件なら、外部API)を自社のシステムから切り離すことで、自社のサービス全体を停止せずに運用するための構成。

のことを言うそうです。

主に以下のような挙動をしています。

  1. クライアントから外部サーバーへのリクエストや、外部サーバーからクライアントへのレスポンスはサーキット・ブレーカーを介して行なわれる。(closed state)

  2. 外部サーバーに対して、リクエストを送る際に、リクエストに失敗してしまうと、サーキット・ブレーカーが落ちる。(open state)

  3. ブレーカーが落ちている間は、クライアントからのすべてのリクエストが、外部サーバーからのレスポンスを待つことなく、サーキット・ブレーカーがすぐにリクエスト失敗のレスポンスをクライアントに返す。

  4. 一定時間経過後に、サーキット・ブレーカーは外部サーバーへリクエストが可能な状態になる。(half-open state)

商品検索APIを利用するような今回のケースを例にしてみますと、

  1. 平常時(closed state)

平常時(closed state)

  1. リクエスト失敗(この時点で、ブレーカーが落ちる)

リクエスト失敗

  1. ブレーカーが落ちている間(open state)

ブレーカーが落ちている間(open state)

  1. 一定時間経過(half-open state)

一定時間経過(half-open state)


のような構成となり、4.の時点で外部のサーバーへのリクエストが成功すれば、1.のclosed stateに戻り、失敗すれば、3.のopen stateに戻ります。

回避・解決方法


今回のケースにおいてサーキット・ブレーカーが、監視するのは以下の2つです。

  1. 各外部サーバーに障害が発生していないかの監視

    • 使用不可サーバーの判別が常について状態にする目的
    • 障害発覚時点で自動的にブレーカーを落とす(open state)
  2. 各外部APIの上限リクエスト数が超えないかを監視

    • 各外部APIの秒間リクエスト数の上限を超えないようにする目的
    • リクエスト制限を超えた時点で自動的にブレーカーを落とす(open state)

また2つの役割を1つのクラスが担うのでなく、別々のクラスがそれぞれの役割を担うような実装してます

"外部サーバーに障害が発生していないかの監視"について

  1. ユーザーからのリクエストとは別に定期的に外部のサーバーへのリクエストを送信。

  2. レスポンス内容を判断(400,500番系のエラー検出や200番で返ってきたときのレスポンス内容で判断)し、外部サーバー側での障害が見られるようなら、各外部サーバーごとにブレーカーを落とす。

  3. その後即座にhalf-openになり、ユーザーからのリクエストとは別に定期的にリクエストを送信し続けるが、ユーザーからのリクエストに対しては、ブレーカーが落ちている状態(open state)になっている。


以下のようなイメージです。
ユーザーからのリクエストに対してはopen stateであるので、

open state

のようにユーザーからは見えていますが、実際はhalf-open stateになっています。

half-open state


また外部のサーバーごとにブレーカーが落ちたり元に戻ったりしますが、この外部のサーバーごとのブレーカーの状態(すなわち外部サーバーがリクエストを受理できるかどうかの状態)はMySQLで管理しています。

"各外部APIの上限リクエスト数が超えないかを監視"について

RoomClipで利用している外部の商品検索APIのリクエスト制限は、だいたい秒間1~10リクエストでした。
ですので、各APIのごとに1秒間で何回のリクエストがあるのかをカウントするようにします。

以下のように動作します。

  1. 1秒で揮発するように設定したRedisが各APIごとに、ユーザーからの叩かれたリクエスト回数をカウントした結果を保持し、カウンターが上限数に達したら、外部サーバーへリクエストを送る前に、APIごとにブレーカーが落ちる。

  2. 数秒(2~3秒程度)待機し、half-openに移行。再度同様のリクエストを送信する。

カウンターが上限に達していた場合

Redisを1秒で揮発するように設定しておくことで1秒経つと、その前の1秒間に送られたリクエスト数がリセットされます。

ちょうどリクエストの上限に引っかかってしまったら数秒待機となってしまうので、商品検索の結果のレスポンスが遅れ得てしまいますが、リクエスト制限に引っかかって検索結果が返ってこないような自体になるよりはよいのではないでしょうか。


こんな感じで僕の初の技術ブログ執筆は終わりですが、 やっぱりなにかを書くのって難しいですね。

今後もTunnelの諸先輩エンジニアの元でたくさん学ばせていただきまして、もっと面白い記事が書ければなと思います。

それでは。


参考


この記事を書いた人:アナン トモロウ

ルームクリップ株式会社でエンジニアしております。 最近はエンジニアリングに限らず いろいろやってます