본문 바로가기

[Project] 프로젝트 삽질기28 (feat 푸시 메시지 구성)

어가며

사이드 프로젝트에서 푸시 알림을 활용한 서비스를 개발하고 있습니다. 푸시 서버를 개발하면서, 참고할 수 있는 자료들이 많이 없었습니다. 이번에 푸시 서버를 개발하고, 제가 했던 고민을 공유한다면 많은 분들에게 도움이 될 수 있지 않을까 생각했습니다. 제가 어떤 문제를 겪었는지, 그리고 그 문제는 왜 나타났으며 문제를 해결하기 위해 했던 고민들은 무엇이었는지 공유하고자 합니다. 그 과정에서 생각하고 배웠던 점들을 하나씩 작성하고자 합니다. 

 

 

 


 

 

Push 로직 구성

제가 개발하고 있는 서비스 POG는 즐겨찾기 한 소환사의 전적이 갱신되면 푸시 알림을 받을 수 있고, 앱에서 바로 확인할 수 있는 서비스입니다. 그러다 보니, 소환사의 전적이 갱신되면 푸시 알림을 보내야 하는데, 이 기능을 개발할 때 많은 고민을 할 수 있었습니다. 

 

 

 

1. 한 명의 소환사를 여러 명이 즐겨찾기 한다면?

첫 번째 고민은, 한 명의 소환사가 여러 명이 즐겨찾기 한다면, 어떻게 푸시를 보낼 수 있을까입니다. 예를 들어 페이커라는 소환사를 1만 명이 즐겨찾기 했다고 했을 때, FCM에 대해 제대로 몰랐을 때는 페이커를 즐겨찾기 한 소환사 1만 명의 FCM 토큰을 찾아와서, 한 명씩 푸시 알림을 보내야 한다고 생각했습니다. 이는 상당한 비효율이라 생각했습니다. 심지어 FCM으로 푸시를 보낼 때, 동기적으로 동작한다는 것이 큰 장애 포인트가 될 수 있다고 판단했습니다. 정리하면 아래와 같이 푸시 메시지를 전송해야 했습니다.

 

 

  • 전적이 변경된 특정 소환사를 즐겨찾기 하는 사용자가 갖고 있는 토큰을 조회한다.
  • 조회한 토큰과 푸시 메시지를 FCM 서버로 전송한다.
  • FCM 서버에서 응답이 정상적으로 올 때까지 기다린다.
  • 다음 토큰을 조회하고 푸시 메시지 정보를 FCM 서버로 전송한다.
  • 이를 반복한다.

 

위와 같은 방식은, 사용자가 많지 않다면 그나마 서버가 무리가 가지 않을 수 있지만, 사용자가 조금이라도 늘어나게 된다면 

 

 

 

출처 : https://deveric.tistory.com/75

 

 

 

FCM 서버로 전송하고 응답을 받을 때까지의 시간이 점점 성능적인 부분에서 문제를 발생시킬 것입니다. 요청과 응답 사이 해당 스레드는 IO Blocking이 걸리기 때문에 스레드가 작동을 멈추기 때문입니다. Blocking이 걸린 동안 다른 일을 하지 않으니 요청과 응답이 많아질수록 성능은 기하급수적으로 떨어지게 됩니다. 심지어 node.js의 경우 싱글 스레드이기 때문에 블로킹이 걸리는 동안, 처리해야 할 작업이 쌓이다 보면, 서버는 버티지 못할 것입니다. 

 

이 문제를 해결하기 위해 FCM 공식문서를 모두 뒤져보면서 topic이란 개념을 알 수 있었습니다. 예를 들어, 특정 사용자가 특정 topic을 구독하면, topic을 대상으로 푸시 메시지를 보낼 경우 topic을 구독하는 사용자에게 푸시 메시지가 가는 좋은 기능이었습니다. 이를 이용하면, 아래의 단계로 처리를 진행할 수 있습니다. 

 

 

  • 전적이 변경된 특정 소환사의 topic을 조회한다.
  • topic과 푸시 메시지 정보를 FCM 서버로 전송한다.
  • FCM 서버에서 응답이 정상적으로 올 때까지 기다린다.
  • 이를 반복한다.

 

여기서 특정 사용자의 FCM 토큰을 조회하지 않아도, 소환사의 topic만으로도 푸시 알림을 보낼 수 있기에, 푸시 메시지를 보내는 과정에서의 비효율을 크게 줄일 수 있었습니다.

 

 

 

 

2. 즐겨찾기에 추가된 소환사가 여러 명이라면?

하지만 아직 동기적으로 동작하는 부분에서 나올 수 있는 문제는 해결되지 않았습니다. 즐겨찾기에 추가된 소환사가 여러 명일 때, 소환사가 모두 전적이 변경됐으면 여러 topic에 푸시 메시지를 보내야 합니다. 이때 Node.js에서 블로킹 문제가 발생하는 것을 비동기 처리로 변경한다면 해결방법이 될 수 있지 않을까 생각했습니다.

 

Node.js는 멀티 스레드로 운영할 수 없기에, Queue를 활용하여 비동기 처리를 진행한다면 어떨까 생각했습니다. Queue에 작업을 담아두고, Queue에 담긴 작업을 하나씩 빼서 처리한다면, 동기적으로 처리됨으로써 스레드가 멈춰있어야 하는 문제를 해결할 수 있다고 생각했습니다. 이를 위해 Redis 기반의 bull.js를 Queue로 활용했습니다. 

 

Queue를 활용한 방식으로 변경하여 더 이상 해당 스레드의 IO Blocking 기간 동안 응답을 기다리지 않아도 되었습니다. 이를 통해 싱글 스레드로 동기적으로 동작하는 것을 비동기적으로 동작하게 만들어서 작업의 효율성을 올렸습니다. 

 

Queue를 활용하면 다양한 이점을 누릴 수 있습니다. 먼저 큐에 푸시 관련 작업을 넣어두고(프로듀서), 빼내는(워커) 과정에서, 큐에 많은 데이터가 쌓이게 됩니다. 이 때 서버에서 큐를 꺼내서 처리하는 시간이 오래 걸린다면 서버의 성능을 올려서 큐에 쌓여있는 작업을 처리할 수 있도록 할 수 있습니다. 또한 푸시 알림 뿐 아니라 SMS, 메일 알림 등을 추가적으로 보내야한다고 했을 때 큐를 사용하지 않았다면 SMS를 보낼 때 장애가 날 경우 메일, 푸시 알림 등의 기능에도 장애가 전파될 수 있지만 큐를 활용한다면 장애가 전파되지 않게 막을 수 있다고 생각합니다. 또한 데이터 손실 문제를 막을 수 있습니다. 스레드에 작업해야 하는 내용이 담겨있을 때, 만약 과부하로 인해 서버가 터져버리는 경우, 누구에게 어떤 알림을 보내야 하는지 알 수 있는 방법은 없습니다. 이 문제를 큐를 활용하면 알림 데이터를 큐에 저장하기 때문에 서버가 갑작스럽게 종료되더라도 무리없이 알림을 전송할 수 있습니다. 비동기적으로 처리하는 목적도 있지만, 다양한 부분에 있어서 큐는 큰 이점이 있습니다. 그럼 큐를 어떻게 활용했는지 알아보겠습니다. 

 

 

 

 

 


 

변경 방법

먼저 적용한 푸시 로직을 살펴보겠습니다.

 

@Interval('pushCronTask', 180000)
  async addMessageQueue(): Promise<void> {
    const summonerIds = await this.redisClient.smembers('summonerId');

    summonerIds.map(async summonerId => {
      const riotApiResponse = await this.riotApiJobService.soloRankResult(
        summonerId,
      );
 
      const redisResponse = await this.redisClient.summonerRecordMget(
        summonerId,
      );

      const isChangeRecord = await this.compareRecord(
        riotApiResponse,
        redisResponse,
      );

      if (isChangeRecord) {
        await this.addPushQueue(summonerId, riotApiResponse[0].summonerName);
        await this.redisClient.pushChangeRecord(riotApiResponse, summonerId);
      }
    });
  }

 

 

특정 시간마다 Redis의 summonerId라는 키 값에 set 자료구조로 저장된 값을 모두 불러옵니다. 여기서 summonerId를 파라미터로 받는 Riot의 전적 조회 API를 호출하여 특정 소환사의 전적을 받아오고, redis에 저장된 소환사 전적을 불러와서, 두 전적을 비교합니다. 만약 전적이 변경됐다면 addPushQueue 메서드에 일정 데이터를 전달합니다. 그 후 redis에 소환사 정보를 저장합니다.

 

 

private async addPushQueue(summonerId: string, summonerName: string) {
    return await this.bullService.createJob(
      this.tasks.addPushQueue,
      {
        summonerId,
        summonerName,
      },
      { delay: 10000, removeOnComplete: true },
    );
  }

 

 

addPushQueue 메서드에서는 bull를 활용하여 생성한 addPushQueue에 데이터를 전달합니다.

 

 

@Task({ name: 'addPushQueue' })
  async addPushQueue(job: Bull.Job, done: Bull.DoneCallback): Promise<void> {
    await this.pushJobService.send(
      job.data['summonerId'],
      job.data['summonerName'],
    );
    this.logger.log(
      `${job.data['summonerName']}의 ${job.data['summonerId']} topic 푸시를 전송했습니다.`,
    );
    done(null);
  }

 

 

Queue에 전달된 데이터를 활용하여 푸시 메시지를 전송합니다. 

 

 

async send(summonerId: string, summonerName: string) {
    const message = {
      notification: {
        title: `${summonerName} 전적 갱신`,
        body: `${summonerName}의 전적이 갱신됐어요.`,
      },
      topic: summonerId,
    };
    return Promise.all([await admin.messaging().send(message)]);
  }

 

 

푸시는 다음의 형태로 전송됩니다. 이렇게 푸시 메시지를 비동기적으로 처리하기 위해 Queue를 활용하는 방법에 대해 알아봤습니다. 처음 서비스를 기획하면서 이 서비스를 만드는 게 가능한 것일까 의구심도 들었지만, 이 프로젝트를 개발하면서 많은 고민을 거듭한 결과 훌륭한 공부를 할 수 있었다고 생각합니다. 위와 같은 방식이 훌륭한 방식은 아닐 수 있기에, 현재의 상황에서 어떤 코드가 부족한지 찾아내고 개선점을 찾아낼 수 있는 개발자가 되고 싶습니다. 이상한 점이나, 부족한 점이 있다면 언제든 댓글 혹은 PR 혹은 issue 남겨주시면 개선하도록 하겠습니다. 감사합니다.  

 

 

 

 

 


 

 

 

 

 

 

마치며

앞으로도 팀의 발전을 돕는 개발자가 되기 위해 노력하려 합니다. 팀에 필요한 부분이 무엇일지 고민하면서, 팀에 도움이 된다면, 열심히 공부해서 실무에 적용할 수 있는 개발자가 되기 위해 노력하고 싶습니다. 팀의 성장에 기여할 수 있는 개발자가 되겠습니다. 

 

 

 

 


 

 

 

 

 

참고 및 출처

 

회원시스템 이벤트기반 아키텍처 구축하기 | 우아한형제들 기술블로그

{{item.name}} 최초의 배달의민족은 하나의 프로젝트로 만들어졌습니다. 배달의민족의 주문수는 J 커브를 그리는 빠른 속도로 성장했고, 주문수가 커지면서 자연스럽게 트래픽 또한 매우 커졌습니

techblog.woowahan.com

 

[Project] 푸시 알림 프로젝트 삽질기1 (feat FCM 공식문서)

들어가며 사이드 프로젝트에서 푸시 알림을 활용한 서비스를 개발하고 있습니다. 그 과정에서 생각하고 배웠던 점들을 하나씩 작성하고자 합니다. 먼저 푸시 알림을 보내기 위해서는 파이어베

overcome-the-limits.tistory.com

 

[Project] 푸시 알림 프로젝트 삽질기9 (feat Queue, bull)

들어가며 사이드 프로젝트에서 푸시 알림을 활용한 서비스를 개발하고 있습니다. 그 과정에서 생각하고 배웠던 점들을 하나씩 작성하고자 합니다. 먼저 푸시 알림 서비스를 구축하려면, Queue를

overcome-the-limits.tistory.com

 

[Project] 푸시 알림 프로젝트 삽질기10 (feat bull 공식문서 정리)

들어가며 사이드 프로젝트에서 푸시 알림을 활용한 서비스를 개발하고 있습니다. 그 과정에서 생각하고 배웠던 점들을 하나씩 작성하고자 합니다. 저번 글에서는 Queue에 대해 알아봤고, Queue 중

overcome-the-limits.tistory.com

 

[이슈 #10] 푸시 메세지를 비동기로 처리하여 성능 개선하기

개요 DelFood에 드디어 푸시 메세지 전송 기능이 추가되었습니다. Firebase Cloud Messaging(이하 FCM)을 기반으로 앱, 웹으로 사용자에게 푸시 메세지 전송 기능을 제작하였습니다. 여러 사용자에게 순차

deveric.tistory.com