SchooアプリのPush通知基盤を刷新しました


こんにちは。Schooアプリでは最近、ユーザ毎により柔軟な通知を送りたいという要求に応えるため、Push通知の基盤を大幅に刷新して運用し始めました。
今回は構築した基盤の概要や使用した技術/チャレンジなどを紹介したいと思います。

# 以前のpush通知基盤
  ![以前の基盤の全体構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8970/bdf32755-78b7-6a9e-ffe5-65d465f93995.png)

### 困ってたとこ:weary:
###### 送信対象の制御がSchoo側からできない
Schoo側から指定できる送信対象の制御がtopic単位しかないのでSchoo上のユーザ情報をもとに柔軟に送信対象を抽出して通知を送るといったことが困難でした。

例えば、特定の授業を見逃したユーザに対して、Push通知で見逃した授業の録画受講を促す施策を打ちたい場合、この構成だと

 - クライアント端末側で見逃したかどうかの判定を行い（アプリ/サービスの起動が必要）
 - 見逃していた場合は特定のTOPIC（授業毎）を購読
 - サーバー側では上記TOPICに対して通知を配信する
 
といったふうにクライアントの実装に依存してしまい、新たに通知の種類や条件を変更したり、追加したりすることが難しくなってしまっていました。


### 刷新に際しての要件
 -  ユーザの状態に合わせて細かく通知の送り分けができること（通知対象の抽出ロジックをSchoo側に持つ）
 -  ユーザ側で通知の許諾を制御ができる

#刷新後の全体構成
![基盤の全体構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8970/4cb76f48-65f2-ad6b-d716-888ec60eac7b.png)



####構成要素

###### :new:1. GRPCサーバー
今回notificationb関連を司るマイクロサービスとして新たにnotificationサーバーを立てました。
これまでアプリの通知周りは主にクライアント<->firebase間での直接Subsacribe/UnsunubscribeしておりSchooサーバ側では管理されていませんでした。今後Schoo側で諸々の購読や配信の管理を担うために別サーバーとしてマイクロサービスとして分離することにしました。

Schooのアプリのバックエンド構成は以前書かれた[Qiita記事](https://qiita.com/xlune/items/c0a113ec9764782096c7)のようにgrpcによるマイクロサービス群により支えられています。

今回このサーバーには主に以下の２つの機能が実装されました。
 - クライアントからのFCMTokenを受け取りSchooのUser情報と紐付けて保持します。これによりユーザ情報から柔軟に送信対象のTokenを抽出することができるようになりました。
 - クライアントとSchooサーバの間で通知の許可フラグを同期します。このflagを送信対象の抽出条件に加えることでクライアント側での通知の許諾設定を反映します。

ここで少しnotificationサーバーで使っているgrpc/protocol buffersについて詳しく説明します。

grpcではまず.protoというIDLをつかってサーバー/クライアント間のインターフェイスを定義します。
この.protoをサーバー側とクライアント側で共有します。
サーバー側・クライアント側それぞれでprotoファイルをコンパイルすることでそれぞれの環境でのスタブが生成されます。
あとはそれぞれの環境の実装者がその中に実装を書いていく感じです。

今回はまずサーバ側の実装担当とクライアント（iOS/Android）担当で一緒に仕様を詰めながら.protoを作成しました。詳細は略しますが↑の２つ機能のために以下のようなprotoができました。

<details><summary>notification.proto</summary><div>

```proto:notification.proto
// ==== サービス:RPCの定義 ====

service Notification {

  // FCMトークンを追加する
  rpc PostFcmRegistrationToken(PostFcmRegistrationTokenRequest) returns (PostFcmRegistrationTokenResponse) {}

  // FCMトークンを取得する
  rpc GetFcmRegistrationToken(GetFcmRegistrationTokenRequest) returns (GetFcmRegistrationTokenResponse) {}

  // push通知許諾フラグを取得する
  rpc GetPermissionFlags(GetPermissionFlagsRequest) returns (GetPermissionFlagsResponse) {}

  // push通知許諾フラグを更新する
  rpc UpdatePermissionFlag(UpdatePermissionFlagRequest) returns (UpdatePermissionFlagResponse) {}
}

// === メッセージ：やり取りされるメッセージを定義/リクエストとレスポンスの仕様等 ===== //
// FCMトークンを保存する
message PostFcmRegistrationTokenRequest {}
message PostFcmRegistrationTokenResponse {}

// FCMトークンを取得する
message GetFcmRegistrationTokenRequest {}
message GetFcmRegistrationTokenResponse {}

// 通知許諾を取得する
message GetPermissionFlagsRequest {}
message GetPermissionFlagsResponse {}
// 通知許諾を更新する
message UpdatePermissionFlagRequest {}
message UpdatePermissionFlagResponse {}

// 通知許諾データ
message PermissionData {}
```

</div></details>

.protoが固まった段階でそれぞれの環境でprotoをコンパイルします。
以下のようにserver向けのinterface(など)が生成されるのでそちらを実装します。

<details><summary>notification.pb.go</summary><div>

```go;notification.opb.go
//略

// NotificationServer is the server API for Notification service.
type NotificationServer interface {
	// FCMトークンを追加する
	PostFcmRegistrationToken(context.Context, *PostFcmRegistrationTokenRequest) (*PostFcmRegistrationTokenResponse, error)
	// FCMトークンを取得する
	GetFcmRegistrationToken(context.Context, *GetFcmRegistrationTokenRequest) (*GetFcmRegistrationTokenResponse, error)
	// push通知許諾フラグを取得する
	GetPermissionFlags(context.Context, *GetPermissionFlagsRequest) (*GetPermissionFlagsResponse, error)
	// push通知許諾フラグを更新する
	UpdatePermissionFlag(context.Context, *UpdatePermissionFlagRequest) (*UpdatePermissionFlagResponse, error)
}

```

</div></details>

あとは、それぞれ実装が完了したらテスト環境にdeploy動作確認しました。


###### 2. Client(android/iOS)
 - 前述のnotificationサーバーとのFCMtokenおよび配信可否フラグの送受信を追加実装しました。
 - 前述のように、grpcではサーバ側と同様.protoをもとにjava(android)/swift(iOS)のクライアントを自動で生成してくれます。一度コンパイルされれば、リクエストObjectやレスポンスObject、メソッド名の補完も効くのでAPIのインターフェイスで迷ったりタイポしたりといったミスが起こらずスムーズに実装を進められます。


###### 3. AWS SQS
 - Schooには既にSQSを使ったメール配信基盤が存在していたので同様な配信システム上に乗せました。
 - Firebase admin SDKでのbulk送信は上限が1000件となっているので通知の内容と通知対象を1000件ごとにバンドルしてQueueingします。

###### 4. AWS Lambda
 - [lambdaのトリガーにsqsのキューが使えるようになっていた](https://aws.amazon.com/jp/blogs/aws/aws-lambda-adds-amazon-simple-queue-service-to-supported-event-sources/) ので、前述のSQSのキューを直接トリガーにしてキューから通知対象と内容を取り出し、Firebase Admin SDK(Node.js)を使って通知を送る処理を行います。
実装自体はqueueから取り出したメッセージをそのままSDKでsendMulticast()するだけです。

<details><summary>notification.js</summary><div>

```js:notification.js
// Firebase Adminを追加する
const admin = require('firebase-admin');

// 認証情報を追加する
const serviceAccount = require('../serviceAccountKey.json');

// Firebase Admin SDKを初期化する
admin.initializeApp({
    credential: admin.credential.cert(serviceAccount)
  });

const main = async (event, context) => {
  try {
    await Promise.all(event.Records.map(async record => {
      const message = JSON.parse(record.body.replace(/(\r\n)/g, '\n'));

      await admin.messaging().sendMulticast(message)
        .then((response) => {
          if (response.failureCount > 0) {
            const failedTokens = [];
            response.responses.forEach((res,indx) => {
              if (!res.success) {
                console.log ("失敗");
                failedTokens.push(registrationTokens[indx]);
              }
            });
          }
          console.log(response.successCount + '回' + '：FCM送信成功');
          return response;
        })
        .catch((error) => {
          console.log(error + 'FCM送信失敗');
          return error
        });
    }));
  } catch(e) {
    console.log('失敗', e);
  }
};


export default main;
```

</div></details>

###### 5. Firebase
 - 端末に実際にpush通知を送ってくれます。
 - 以前の実装では各クライアントはFirebase上の特定のtopicを購読し、Schooサーバー側からはそのTopicに対して通知を送信するという構成になっていました。
 - 今回の実装ではtopic単位ではなくclient毎に発行されるtokenを直接指定して通知を送る構成に変更しました。
 - これにより通知対象の制御をClient側の購読処理ではなくSchooサーバー側にもつことができ、より柔軟に通知対象の制御ができるようになりました。

##さいごに
実は今回Goによるnotificationサーバの実装担当したメンバーはGo実装に関してはまだ学び始めたばかりでした。大小の壁にぶち当たりながらも、ほかメンバーのサポートや本人のモチベーションでなんとか実装・リリースをすることができました。
というわけで、今回の通知基盤刷新では、push通知をより柔軟に運用したいという要求に答えつつも、通知周りのサービスをマイクロサービス化して新しいgrpcサーバを立てて分離するという開発チームとしての技術的なチャレンジも行い、メンバーのサーバサイドの知見も大いに高まったプロジェクトとなりました。

しかし、Schooではまだまだエンジニア（特にサーバーサイド）のリソースが足りていません。
というわけで
[エンジニアを超絶募集](https://www.wantedly.com/projects/353365)していますー。
よろしくおねがいします！:v_tone5: