티스토리 뷰

최근 어플리케이션의 사용자가 많아지면서 대충 만들었던 notification 에 대한 안정성을 평가하고, 코드 리팩토링을 진행하게 되었습니다.

expo sdk 를 이용하여 하이브리드 어플리케이션을 제작하고

spring boot 를 이용하여 api server를 만드는 경우 사용하게 되는 expo-server-sdk-java 의 사용법에 대해서 알아보려고 합니다.


0. expo push notification

expo-notifications 을 이용하면 어플리케이션에서 ExponentPushToken[...] 형식의 expo push token 을 받아 올 수 있습니다.

어플리케이션에서 전달받은 expo push token 을 database 에 저장하고 백엔드 언어에 맞는 expo-server-sdk-(java or python..) 를 이용하면 ios/android 구분없이 하나의 로직을 이용하여 손쉽게 push notification 을 보낼 수 있습니다.

아래 그림은 expo 공식 document 에서 제공하는 모식도 입니다.

출처: https://docs.expo.dev/push-notifications/sending-notifications/

  1. 실제 디바이스에 설치된 어플리케이션에서 expo backend 에 push_token 발급을 요청하면 expo backend 에서 해당 디바이스에 대한 정보를 저장합니다
  2. expo-server-sdk 와 1에서 발급받은 토큰을 이용하여 백엔드 서버에서 push 알림을 요청합니다.
  3. expo backend 에서 push token 과 메세지를 받고, push token 에 저장된 정보를 분석하여 Firebase, APNs 에 push 알림을 요청합니다.

1. TroubleShooting & FAQ

APNS & Firebase 에 따라서 각각 다른 코드를 관리하는 것이 아닌 expo 를 통해서 동일한 코드로 두개의 플랫폼을 관리할 수 있는 것은 생산성과 관리측면에서 큰 이점이 있습니다. 하지만 알림을 발송할때 expo server 를 거쳐야 한다는 점에서 expo server 의 성능을 어느정도로 신뢰할 수 있는지, 특별하게 다른 조건은 없는지 따져봐야 했습니다.

https://docs.expo.dev/push-notifications/faq/ 에 나온 내용을 정리해 봤습니다.

  1. 일단은 무료, EAS Notify (Emergency Alert System) 기능은 일부 유료
  2. 초당 수백회 이상의 알림을 보내지 않는 이상 감당할 수 있는 규모. 하지만 expo-server-sdk 를 이용하여 쓰로틀링을 구현하는 것이 추천된다.
  3. Expo 대신 APNs, Firebase 를 사용할 수 있다. 대신 직접 구현해야하는 부분이 많아짐
  4. ExpoPushToken 은 applicationId 혹은 experienceId 가 변하지 않는 이상 모든 경우에 유지된다. 
  5. ExpoPushToken 은 절대 만료되지 않기 때문에 만약 DeviceNotRegistered 에러가 발생하면 expoPushToken 사용을 중단하자
  6. Expo 는 메세지를 저장하거나 읽거나 공유하지 않고, 암호화하여 전달한다.
  7. Android 의 경우 priority에 의해 드물게 메세지가 전달되지 않을때도 있다

2. Remark

expo notificaiton 을 사용하게 되는 경우, push notificaiton 을 요청하게 되는 우리의 백엔드 서버는 APNS, Firebase 와 직접 통신하는 대신 expo-server 에 요청을 보내고 응답을 받습니다. 따라서 상황에 따른 expo server 의 응답에 대해 잘 파악할 필요가 있습니다.

우선 https://docs.expo.dev/push-notifications/sending-notifications/ 을 중심으로 정리했습니다. 앞으로 포스팅을 진행하면서 중요하게 고려해야할 부분들이 나오면 업데이트 하겠습니다.

 

 

1. 알림을 잔뜩 보내는 경우를 대비해서 동시성에 제한을 걸자

const DEFAULT_CONCURRENT_REQUEST_LIMIT = 6;
...
private async requestAsync(url: string, options: RequestOptions): Promise<any> {
    ...
    const response = await this.limitConcurrentRequests(() =>
      fetch(url, {
        method: options.httpMethod,
        body: requestBody,
        headers: requestHeaders,
        agent: this.httpAgent,
      })
    )
    ...
}

Node 로 작성된 expo-server-sdk 를 확인해보면 DEFAULT_CONCURRENT_REQUEST_LIMIT 와 promise_limit 을 이용하여 동시에 처리하는 요청에 제한을 두는 것을 확인할 수 있습니다. 기본값을 6개로 최대 6개의 알림요청을 동시에 처리할 수 있습니다. 이는 절대적인 알림수와는 다른 요청 제한 개수입니다. 한번의 요청에 여러개의 알림을 보낼 수 있습니다. 

 

2. 한번의 요청에 보내야하는 알림이 많은 경우 적절한 Chunk 로 분리하여 요청하라

// main 에서 사용법
let chunks = expo.chunkPushNotifications(messages);

// 실제 코드
const PUSH_NOTIFICATION_CHUNK_LIMIT = 100;
chunkPushNotifications(messages: ExpoPushMessage[]): ExpoPushMessage[][] {
    ... // messages: ExpoPushMessage[] 를 ExpoPushMessage[][] 로 분할
    return chunks;
  }

Node 로 작성된 코드를 확인해보면 chunk size 를 100개로 두고 그 이상의 메세지가 한번의 요청에 들어오는 경우 여러개의 작은 요청으로 분할하는 것을 확인할 수 있습니다. 추가로 receipt 의 chunk size 는 300개로 설정되어있습니다.

 

3. Retry for Failure

push notification 을 보낼때 우선적으로 backend server 에서 expo server 로 ExpoToken 과 함께 메세지 요청을 보냅니다. 이렇게 받은 요청을 expo server 는 queue 에 저장하였다가 Google(FCM) Apple(APNs) 로 전송합니다. expo server 가 요청을 받아 queue 에 담아두는 과정을 첫번째 단계 라고 할때, 다양한 이유에 의해서 실패할 수 있습니다. 주로 발생할 수 있는 에러가 네트워크 에러와 malformed 에러입니다. network 에러 발생시 429, 500 상태코드, 잘못된 형식의 메세지를 보내는 경우는 400 에러를 반환합니다. 

각각의 에러 상황에 대응되는 적절한 로직을 작성해야 하며 특히 429, 500 등의 네트워크 에러가 발생시 재요청을 하도록 코드를 작성해야합니다. expo 도큐먼트에서는 exponential jitter 을 이용한 backoff 를 사용하는 것을 추천합니다. 

https://aws.amazon.com/ko/blogs/architecture/exponential-backoff-and-jitter/

 

Exponential Backoff And Jitter | Amazon Web Services

Introducing OCC Optimistic concurrency control (OCC) is a time-honored way for multiple writers to safely modify a single object without losing writes. OCC has three nice properties: it will always make progress as long as the underlying store is available

aws.amazon.com

expo server 가 원인이 되어 에러가 발생했을 경우, expo server 가 안정되기를 기다렸다가 재요청을 보내야하는데 반복적으로 에러가 발생하는 경우 더 오랜시간을 기다리는 방법이 exponential backoff 입니다. 처음 에러발생했을때는 2초, 다음은 4초, 8초, 16초 이런식으로 점진적으로 시간을 늘려가며 expo server 가 안정되어 요청을 받을 수 있는 상태가 되도록 기다립니다.

 

4. Push ticket & Push receipt

expo server 에서 성공적으로 요청을 받으면 push ticket을 발급합니다. push ticket 을 이용하여 push receipt 을 조회할 수 있으며, push receipt 에는 알림이 사용자에서 정상적으로 전달됐는지에 대한 정보가 들어있습니다. 

// push ticket
{
    "data": [
        { "status": "ok", "id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" },
        { "status": "ok", "id": "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY" },
        { "status": "ok", "id": "ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ" },
        {
          "status": "error",
          "message": "\\\"ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]\\\" is not a registered push notification recipient",
          "details": {
            "error": "DeviceNotRegistered"
          }
        },
        { "status": "ok", "id": "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA" }
      ]
    },
    "errors" :[]
}

push ticket 의 응답은 위와 같은 json 이며 순서는 보낸 메세지의 순서와 같습니다. id 값은 push receipt 의 아이디를 의미합니다.

만약 보낸 모든 요청에 에러가 발생한다면 errors field만 채워진채로 결과가 반환됩니다. 또한 이때 http status code 400 ~ 500 중 하나를 가집니다. 

 

위에서 발급받은 receipt 아이디를 이용하여 receipt 를 조회할 수 있습니다. 

{
  "data": {
    Receipt ID: {
      "status": "error" | "ok",
      // if status === "error"
      "message": string,
      "details": JSON
    },
    ...
  },
  // only populated if there was an error with the entire request
  "errors": [{
    "code": string,
    "message": string
  }]
}

receipt 결과는 위의 형태와 같습니다. tickets 과 거의 유사한 형태로 마찬가지로 요청자체에 에러가 있는 경우 errors 필드만 채워지게 됩니다. receipt 는 expo server 에서 FCM 과 APNs 에 요청을 보낸 이후에 만들어지기 때문에 tickets 을 발급받고 어느정도의 시간이 지난 이후에 확인하는 것이 권장됩니다. expo 에서는 15분을 추천하고 있습니다.


다음 포스트부터 위의 내용을 중심으로 expo-server-sdk java 의 코드를 확인하고 필요한 부분을 추가해서 spring project 안에서 expo push notification service 를 작성해보겠습니다.

 

출처

node expo-server-sdk : https://github.com/expo/expo-server-sdk-node

expo-notification: https://docs.expo.dev/push-notifications/sending-notifications/#push-receipt-response-format

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함