大規模ネイティブアプリへのプッシュ通知機能導入にあたって考えたこと

<div class="alert alert-info text-center"><img src="https://qiita-image-store.s3.amazonaws.com/0/25109/d193e0b9-ca5c-3ba2-d5b9-34ee0456c356.png" width="16" height="18" alt=""> この記事は <a href="http://qiita.com/advent-calendar/2016/dwango">「ドワンゴ Advent Calendar 2016」</a>23日目の記事です。</div>

　こんばんは。[ニコニコ静画](http://seiga.nicovideo.jp/)のバックエンドや[ニコニコ漫画 iOSアプリ](http://info.nicovideo.jp/nicomanga/)の開発などのお仕事を、もしかしたらしているかもしれない @gomi_ningen と申します。

　この記事は、比較的規模の大きなサービスのネイティブアプリにプッシュ通知機能を導入する風景を淡々と描いたものです。過度な期待はしないでください。なお、一応お約束として書いておくと、この記事は個人の見解であり、所属する組織の公式見解ではありません。

# はじめに

　**[Amazon SNS](https://aws.amazon.com/jp/sns/)** や **[Firebase](https://firebase.google.com/)** などのプッシュ通知配信基盤のおかげで、iOS/Android 端末へのモバイルプッシュ通知の実装自体は簡単にできるようになってきました。これらのサービス基盤の利用にはサーバーサイドの知識はほぼ必要なく、普段ネイティブアプリ開発しかやっていないような方でも簡単に利用することができる作りになっています。しかしながら、実際に比較的大きなサービスにモバイルプッシュ通知機能を導入・運用していくにあたっては、地味に考慮しなければならない点がいくつも見つかりました。

　Amazon SNS を利用したプッシュ通知の実装方法自体については[公式のドキュメント](http://docs.aws.amazon.com/sns/latest/dg/SNSMobilePush.html)をはじめとして、Qiita にも記事がゴロゴロ転がっているのでお好きなものを参照していただくとして、この記事では大規模サービスのネイティブアプリにプッシュ通知機能を導入する際に考えなければならないことをまとめていくことにします。



# どんなプッシュ通知機能が必要なのか

　**プッシュ通知機能は諸刃の剣です。**ユーザーに対してサービス側から好きなタイミングでメッセージを送ることができる一方で、不要な通知はユーザーの気分を害します。そしてそれは、通知をオフにされたり、アンインストールされたりといったことに繋がるでしょう。

　ネイティブアプリにプッシュ通知機能を導入する際にまず考えなければならないことは、**「なんのためにどんなプッシュ通知を送るのか」**を決めることです。非常にマジレス的でつまらない、当たり前の内容ですね。

　しかし、世の中には意味不明なプッシュ通知を大量に送ってくるアプリがあり、私も日頃アンインストールしまくっているので、こうした当たり前のことを検討していないか、ユーザーへのいやがらせのためにプッシュ通知を送って楽しんでいる悪趣味な方がたくさんいるような雰囲気を感じます。



## 機能要件の想定とプッシュ通知のパターン

　さて、とあるところに作品を投稿できるサービスがあり、ネイティブアプリから作品が閲覧できるシステムがあったと**仮定**しましょう。ユーザーはきっと、**気に入った作品が更新されたら通知が欲しい**のではないでしょうか。また、**サービス内の突発的なイベントや面白い作品の情報などについても通知が欲しい**というケースがあると思います。

![event.png](https://qiita-image-store.s3.amazonaws.com/0/56771/295d7815-0319-7476-d2e6-c173f3221bc8.png)


### プッシュ通知パターンの分類

ここで登場した2つの通知パターンについて詳しくみていくと、特徴が違います。

1. 気に入った作品の更新情報通知（**個別通知**）
2. イベントなどの情報通知（**全体通知**）

　前者は、ある作品をお気に入り登録しているユーザーにだけ通知がいく**個別通知**になります。一方後者は、任意のタイミングで全ユーザーに通知がいく**全体通知**になります。そして、個別通知は比較的定形のメッセージ、全体通知は自由なメッセージを送りたいケースが多いのではないでしょうか。まとめると以下の表のようになります。

| 種別 | 内容 | 送信先 | 通知発生タイミング | メッセージ形式 |
|:--:|:--:|:--:|:--:|:--:|
| 個別通知 | お気に入り作品の更新情報など | 個別のユーザー | サービス運営者による制御が難しい | ほぼ定形 |
| 全体通知 | イベントやお知らせなど | 全ユーザー | サービス運営者により制御できる | 自由 |

　世の中にたくさんのアプリが存在しますが、実はどんなアプリも通知のパターンとしては、だいたいこのようなものに落ち着くのではないでしょうか。



### 通知パターンの分類から検討事項を洗い出す

送りたいプッシュ通知の内容を分類した結果をみると、いくつか検討が必要な項目が見当たります。それぞれについて見ていきましょう。



#### 検討1：個別通知の発火タイミング

　ユーザーが作品を投稿できるようなサービスの場合、その**イベントの発生時刻をサービス運営者が制御することは不可能**といって良いでしょう。これは、真夜中にユーザーのデバイスに通知が行ってしまう可能性があるということを意味します。

　こうした場合、ユーザーのデバイスにプッシュ通知が到達したあと、すぐにメッセージを表示せずに溜めておき（**サイレントプッシュ通知**）、定時にユーザーに知らせるという手法を検討すると良いでしょう。

一般的な方はアプリを

1. 通勤・通学中（7:00〜9:00）
2. お昼休み（12:00〜13:00）
3. 学校や会社終わり（15:00〜18:00）
4. 寝る前（21:00〜24:00）

などに使うことが多いのではないでしょうか。このあたりは各アプリのユーザーの特性によって大きくかわる部分ではありますが、いずれにせよユーザーがいつ通知を欲しがるかを検討することは必要かと思います。

　そして必要であれば、サービス運営者が制御できないイベント通知に関してサイレントプッシュ通知を利用し情報をデバイスにスタックしたのちに、ユーザーへ定時にメッセージを出すことを検討してみてください。

#### 検討2：全体通知メッセージの内容や遷移先のテスト

　全体通知として、サービス運営者がメッセージを自由に入力し、アプリ内での遷移先を自由に定めることができる仕組みを作った場合に考慮しなければならないことは、**オペレーティングミス**です。人間が自由に操作できる部分が大きければ大きいほど、この手のミスは起こりやすいでしょう。また、**プッシュ通知は一度送信してしまうと、取り消しは基本的にはできない**ため、ミスに対する取り返しはほぼつきません。手が滑って適当なメッセージを送ってしまえば、炎上の要因にもなりうるでしょう。

　こうしたことは、**送信予定のメッセージをあらかじめサービス運営者のみに送信できる仕組み**を作れば、ある程度防ぐことができるのではないでしょうか。サービスの規模や特性などにもよりますが、プッシュ通知の事故のリスクとテストメッセージ送信の仕組みを天秤にかけ、必要かどうか判断することになるかと思います。



## 機能要件まとめ

以上のことを検討した結果、次のような機能を実装する必要があると仮定しましょう。

* 特定ユーザーへイベントごとの定形メッセージを通知する機能（個別通知）
  * アプリ側でメッセージの出現タイミングを制御する機能（サイレントプッシュ+定時通知）
* 全ユーザーに対し、自由なメッセージや遷移先を指定した通知を送る機能（全体通知）
  * 事前にサービス運営者にのみにテスト送信・動作確認できる機能（通知テスト機能）

以降はこれらの機能を Amazon SNS を用いて構成していくことについて考えていきます。



# プッシュ通知機能をどのように実装するのか

　いよいよプッシュ通知システムの実装方法について検討していきます。ここまで技術的な話があまりなかったので、ただのポエムに成り下がるところでした。危ない...。


## Amazon SNS モバイルプッシュ通知概論

　Amazon SNS を用いて iOS/Android デバイスにプッシュ通知を送るためには次のような手順を踏む必要があります。初見の方にはよく分からない単語が出てくるかと思いますが、ひとまず流れだけを見てみてください。

![prepare (4).png](https://qiita-image-store.s3.amazonaws.com/0/56771/3eedfcd4-f6e8-42a6-1e4b-420335e813d8.png)

### SNS を利用したモバイルプッシュ通知のための準備

　図を見ながら順を追って説明していくと、まずアプリ起動時に iOS アプリであれば APNS に、 Android アプリであれば GCM にデバイストークンを払い出させます。これは（各アプリごとに）デバイスを一意に特定するためのトークンです。

　続いてそのトークンをサービスのAPIサーバーなどへ送出します。この部分についてはアプリにAWS SDKを入れてしまい、APIサーバーを通さない実装もできるかと思います。しかし個人的には、プッシュ配信基盤をいつでも差し替えられるようにしておきたいので、デバイストークンをどう処理するかについてサーバー側が握れる仕組みの方が好みです。

　APIサーバーは、受け取ったデバイストークンを Amazon SNS 上に作った `application` へ登録し、払い出された `endpoint_arn` を受け取ります。**`application` はプッシュ証明書等と紐付いた概念**であるため、iOS/Android でそれぞれ別のものを作成する必要があります。また、`application` には通知を送りたい対象のデバイストークンを登録する必要があります。登録を行うと `endpoint_arn` というものが発行されます。これはデバイストークンと1対1に紐付いているものになります。デバイストークンのフォーマットは iOS/Android で異なるため、 AWS 側で扱いやすいように統一的な名前を発行しているのではないかと思います（※個人の感想です）。こうして払い出された `endpoint_arn` と デバイストークンはユーザー情報と紐付けて永続化しておきます。

　個別通知を送るだけであれば、`application` にデバイストークンを登録し、`endpoint_arn` を払い出すところまでで十分なのですが、全体通知をするために、今回は Amazon SNS のトピックという機能を使うこととしましょう。トピックは `subscriber` を要素として保持しており、トピック宛てにメッセージを送ると `subscriber` 全員にメッセージを配信してくれるという機能を持っています。したがって作成したトピックを、払い出されたすべての `endpoint_arn` に `subscribe` させれば、全体通知を実現することができます。

### SNS を利用したモバイルプッシュ通知の流れ

　以上が Amazon SNS を利用したモバイルプッシュ通知のための下準備になります。続いて実際に通知を送信するときの流れについてざっくりと見ていきましょう。

![prepare (5).png](https://qiita-image-store.s3.amazonaws.com/0/56771/cc778c2b-3fa7-c9c8-7d2b-a619021cd792.png)

　図のように、個別通知の場合は DB からメッセージを送りたい対象ユーザーの `endpoint_arn` を拾い、そこ宛てにメッセージを通知します。全体通知の場合はトピックに対してメッセージを通知します。すると Amazon SNS はAPNSやGCMなどの各プラットフォームの通知配信サービスへとメッセージを届けてくれます。そこから先は APNS や GCM がよしなに各ユーザーのデバイスへの通知を届けてくれるでしょう。

　ひとつ注意が必要なのは、デバイストークンは一定期間を経ると無効になるということです。無効になるタイミングは APNS/GCM 次第であるため、アプリ起動時には毎回デバイストークンをサーバーサイドに送り、`endpoint_arn` を払い出すようにする必要があります。また無効になったデバイストークンに紐づく `endpoint_arn` へメッセージをを送ると当然通知は失敗するのですが、その際に `endpoint_arn`が持つ属性である `Enabled` が `false` になった状態で、 `application` に残り続けます。放置し続けてもよいのですが、たまり続けると実装方法によっては個別通知のパフォーマンスが低下したりなど問題が起こりうるので、なんらかの削除機構を作るのがよいのではないでしょうか。

以上が Amazon SNS を利用したモバイルプッシュ通知のざっくりとした流れになります。

## 実装するシステムの全体像

　Amazon SNS を用いたモバイルプッシュに関する基礎知識を確認したところで、前節で想定した次のような機能要件を実現するシステム構成の全体像を見てみましょう。ただし、サービスは基本的にオンプレミスで動いていることを前提とします。

> 機能要件
>
> * 特定ユーザーへイベントごとの定形メッセージを通知する機能（個別通知）
>   * アプリ側でメッセージの出現タイミングを制御する機能（サイレントプッシュ+定時通知）
>* 全ユーザーに対し、自由なメッセージや遷移先を指定した通知を送る機能（全体通知）
>   * 事前にサービス運営者にのみにテスト送信・動作確認できる機能（通知テスト機能）

![user (3).png](https://qiita-image-store.s3.amazonaws.com/0/56771/9a2ad099-92bc-e7ef-0377-8a3fff508195.png)

　ご覧のように、結構複雑になります。すべてAWS上に乗っかっていたり、Direct Connectなどを利用しているのであれば、もう少しシンプルな作りにはできる部分はあるかと思います。あと、図中には登場していないのですが、ネイティブアプリ側のプッシュ通知処理実装はもちろん必須になります。

## サーバーサイド各モジュールの実装

図中に登場した 6 つのモジュールの実装についてそれぞれ見ていきます。

### 1. デバイストークン受け取り用のエンドポイント作成

　ネイティブアプリからデバイストークンを受け取るエンドポイントを作成します。適当なパスを切り `PUT` で受け取るような形になるかと思います。サーバーはデバイストークンを受けとったあと、その処理は他のジョブに任せてさっさとレスポンスを返してあげましょう。そのためにキューにデバイストークンとユーザーIDとプラットフォームタイプ（iOS/Androidなど）の情報を含んだジョブを登録します。

　オンプレ環境にすでにジョブキューがあるのであればそれを使い、なければ Amazon SQS の料金が激安なので利用してみるのも手かと思います。

### 2. デバイストークン処理用のバッチジョブ作成

　ユーザーから受け取ったデバイストークンの処理を受け持つバッチジョブを作ります。このジョブがしなければならないことは以下の4つになります。

1. 各プラットフォーム（iOS/Android）に対応する `application` にデバイストークンを登録し、`endpoint_arn` を払い出させる
2. 全体通知用トピック を `endpoint_arn` に `subscribe` させる
3. もし、全体通知テストユーザーだったらテスト用トピックも `subscribe` させる
4. `device_token` と `endpoint_arn` をユーザー情報に紐付けて永続化させる

　`1.` の手順については Amazon SNS の [`CreatePlatformEndpoint`](http://docs.aws.amazon.com/sns/latest/api/API_CreatePlatformEndpoint.html) という操作を行うことになります。また、`2.` と `3.` の手順については [`Subscribe`](http://docs.aws.amazon.com/sns/latest/api/API_Subscribe.html) という操作を行うことになります。詳細についてはドキュメントを確認してみてください。

### 3. 作品更新時にジョブをキューに積む

　既存の稼働しているサービスへの作品更新時に、プッシュ通知送信ジョブを作成し、キューに積んであげるように変更をいれましょう。この部分はきっとほんの少しの実装で済むのではないでしょうか。

### 4. 作品更新プッシュ通知ジョブ作成

　このジョブは、作品をお気に入りに登録しているユーザーの `endpoint_arn` のリストを取得し、ペイロードを並列で publish します。これには Amazon SNS の [`Publish`](http://docs.aws.amazon.com/sns/latest/api/API_Publish.html) という操作を行います。個人的にはこの `Publish` の宛先である `endpoint_arn` をリストで指定できるように AWS 側に機能追加してほしさはあるのですが、いまのところ 1 回の publish で 1 つの `endpoint_arn` しか指定できません。今後に期待しています...。


### 5. 全体通知送信フォームをサービス管理ツールに追加する

　ある程度の規模以上のサービスであればきっと、サービスを管理するツールは存在すると思います。これまで見てきたように作品更新通知についてはバッチジョブ連携により自動的に送信される仕組みになっています。しかし、イベント情報などを知らせる全体通知に関しては、サービス運営者の手によってメッセージや遷移先の指定をする必要があるため、サービス管理ツールにフォームを追加する必要があるでしょう。

　オペミスを防ぐために、最初にテスト通知を送り、確認したものしか本番送信できないような仕組みづくりなど、工夫次第で手間のかかりぐあいはだいぶかわる箇所でもあります。

　また通知の即時送信の仕組みしかないと、サービス運営者は決まった時間に送信ボタンを押さなければならず、運用コストは大きくなりそうです。必要であれば通知を予約する仕組みなどを作ると良いのではないでしょうか。

### 6. disabled なエンドポイントを削除するジョブ

　最後に無効になったエンドポイントを削除するジョブを実装しておきましょう。Amazon SNS の `application` が持つ `endpoint_arn` には、その属性が変化したときに SNS トピックに通知してくれる機能があります。 `application` の設定画面の以下のような部分に通知先の SNS トピックの ARN を設定する項目があるはずなので、そこを開いて disabled なエンドポイント削除トリガー用トピックのARNを指定してあげてください。

<img width="630" alt="0e022af5-654a-24ad-f2d1-b5c7e9a92258.png" src="https://qiita-image-store.s3.amazonaws.com/0/56771/a71c30b0-eb2e-dc17-91fc-97ef34257f40.png">

　続いて実際に削除処理を行うラムダ関数を用意しておきましょう。こちらはラムダ関数に飛んできたデータの中から `endpoint_arn` を抜き出し [`DeleteEndpoint`](http://docs.aws.amazon.com/sns/latest/api/API_DeleteEndpoint.html) という操作をすればOKですので、適当にラムダ関数を実装してください。

　ラムダ関数の実装が終わったら、 disabled なエンドポイント削除トリガー用トピックに、作ったラムダ関数を `subscribe` させれば、 disabled になった瞬間にラムダ関数が走り、エンドポイントが削除されます。大きなサービスで全ユーザーにプッシュ通知を送ったりすると、当然無効なエンドポイントもたくさん生まれるので、もしかすると AWS Lambda の同時実行制限に引っかかってしまうかもしれません。そのあたりは調整して、場合によっては上限緩和申請を行うとよいでしょう。

# おわりに

　クラウドサービスが手頃な値段で誰でも簡単に利用できるようになってきた今、「アプリ開発エンジニア」や「サーバーサイドエンジニア」といった肩書きは意味をなさなくなってきています。思い立ったらすぐに iOS/Android アプリ開発者が API やプッシュ通知などサーバーサイドの機能に手を加え、運用していくことができるようになってきました。

　しかしながら、それなりの規模のサービスに導入しようとなると、まだまだ検討しなくてはいけないことが多いということも事実でした。今後ますますクラウドサービスが発展していっても、人間が考える余地は残り続けるはずなので、クライアント/サーバーの知識をバランスよく摂取して、ユーザーに快適に利用していただけるサービスづくりをこれからも進めていきたいですね。

　なお、今回検討した内容の一部は、この秋開催されたAWSのカンファレンス [Re:invent](https://reinvent.awsevents.com/) にて発表された [Amazon Pinpoint](https://aws.amazon.com/jp/pinpoint/) という新機能を用いるとよりきれいに実装できたり、もっと高機能にできる可能性があります。このあたりについては引き続き調査を行い、今後の改善につなげていきたいと考えています。

# 謝辞

　偉そうに記事を書いてますが、以上のことは自分一人で考えたわけではないので、一緒にお仕事をしたチームの方々に感謝の気持ちを記しておきます。今年も一年間、楽しい開発ライフをありがとうございました。

以上、[ドワンゴ Advent Calendar 2016](http://qiita.com/gomi_ningen/private/95278f260a36da9d981d) 23日目の記事でした。明日は @kwappa さんの記事です。楽しみにしております。

# ニコニコ静画広告

　ニコニコ漫画は、少年、青年、少女、４コマなど、幅広いジャンルの作品が毎日更新され、その数は国内最大級です。ここから商業デビューを果たした作品も多数あり、魅力的な作品が満載です。ぜひ、使ってみてください！

> **ニコニコ漫画について詳しくは [こちら](http://info.nicovideo.jp/nicomanga/)**
> **アプリのダウンロードはこちら：[【Android版】](https://play.google.com/store/apps/details?id=jp.co.dwango.seiga.manga.android) / [【iOS版】](https://itunes.apple.com/app/id1045938821)**

[![](https://qiita-image-store.s3.amazonaws.com/0/56771/70693a3d-1a25-a675-421b-c83c6ca37084.png)](http://seiga.nicovideo.jp/)

　つい先日には、コミックマーケット91の新刊をチェックできる機能、[ニコニコ漫画＠C91](http://seiga.nicovideo.jp/manga/expo/c91)もオープンしました。是非のぞいてみてください。