Post

알림 구현기

알림 구현기

배경

최근에 우리 명지대학교 학생식당과 학생들간에 정보를 빠르게 확인하고 소통할 수 있는 플랫폼이 있으면 좋을 것 같다는 생각에 프로젝트를 진행하였다.

그동안에도 다른 식단 정보를 알려주는 어플이 있었지만 다들 개발자가 직접 학교 홈페이지를 참고해서 업로드하기 때문에 정보의 반영이 늦거나 메뉴 품절/변경 등 유동적인 공지를 따로 해줄 수는 없었고 현재는 모두 운영이 중단된 상태이다.

따라서 관리자가 직접 정보를 등록하고 유저는 이를 편리하게 확인할 수 있는 그런 플랫폼을 만들고자 했다. 그렇기 때문에 정보를 항상 찾아서 확인해야 하는 것이 아닌 푸시 알림을 통해 공지 기능이 매우 중요했다.

이 글에서는 이 푸시 알림을 어떻게 구현했는지에 대해서 설명하고자 한다.

구현한 알림 사진


알림 구현

먼저 우리 프로젝트에서의 알림에 관한 요구 사항을 정리하면 대략 다음과 같다.

  1. 알림 항목에는 5종류가 있다. 1) 오늘의 식단 알림 2) 품절 알림 3) 주간 식단 업로드 알림 4) 식단 사진 업로드 알림 5) 일반 알림
  2. 알림 받을 식당은 3개가 있다. 편의상 A,B,C라 하겠다.
  3. 사용자는 어떤 식당의 정보를 받을지와 어떤 종류의 알림을 받을지를 각각 정할 수 있다.
  4. 오늘의 식단 알림은 매일 오전 10시에 알림 받을 식당의 중식 정보를 같이 알려준다. 예를 들어 유저X는 A식당과 오늘의 식당 알림을 ON하였고 유저 X는 A,B 식당과 오늘의 식당 알림을 ON하였다. 그러면 X는 A식당의 식단 정보만 받고 B는 A,B 식당의 식단 정보를 합쳐서 받는 것이다.
  5. 수신한 알림은 알림센터에 저장된다. …

백문이불여일견이니 아래 사진을 보면 이해가 빠를 듯 싶다. 구현한 알림 항목 화면

오늘의 식단 알림 예시


본론으로 돌아와서, 우리는 FCM을 이용하여 원격 알림을 구현하였다.

Firebase Cloud Messaging(FCM)

FCM 아키텍처

간단하게 설명을 하자면

  1. 아이폰, 갤럭시, 웹에서 Firebase SDK를 이용하여 FCM 서버에 고유 기기를 식별하는 기기토큰을 등록한다.
  2. FCM Backend에 지정된 형식으로 Notification이 포함된 Message를 만들고 보낸다.
  3. FCM에서 이 Message를 읽어 메시지 수신 대상 기기에게 메시지를 보낸다. (수신 대상은 Message를 만들 때 지정)

이러한 방식으로 푸시 알림이 앱 사용자에게 보내지는 것이다.

FCM 공식문서에 가이드가 친절하게 나와있는 편이기 때문에 보고 잘 따라한다면 크게 어려울 것은 없을 것이다.

수신 알림 저장

그런데 이제 우리의 요구사항에는 알림센터에 수신받은 알림이 동시에 저장이 되어야 한다.

따라서 클라이언트가 알림을 수신을 받을 때 받은 알림을 저장하는 API를 호출하도록 하였다.

여기서 잠깐 알림과 사용자의 관계를 살펴보자면 하나의 알림은 여러 사용자에게 전달 될 수 있고, 한 명의 사용자 역시 여러 알림을 수신받을 수 있기 때문에 다대다 관계임을 확인할 수 있다.

따라서 알림 메시지에 NotificationID를 주어 알림을 저장하는 API를 호출할 때 해당 ID와 유저의 ID를 통해 중간 테이블에 인스턴스를 만들어 알림 메시지를 관리 하였다. DB 스키마DB 스키마 일부


ios 백그라운드 처리 이슈

이렇게 구현을 하여 문제가 없을 줄 알았는데, IOS의 백그라운드 환경에서 메시지를 수신할 시에 보안적인 문제 때문에 외부 API를 호출할 수 없어, 수신받은 알림이 제대로 저장되지 않는 문제가 발생하였다.

(다시 찾아보니 아예 불가능한 것은 아니고 background mode를 잘 설정해주면 된다고 한다, 여튼 수신 받을 때마다 모든 기기가 API를 호출하는 것도 비효율적이라고 판단)

그래서 우리는 수신을 받을 때 알림을 저장하는 API를 호출하는 것에서 서버가 직접 수신받을 기기(유저)를 식별하여 중간 테이블에 인스턴스를 만들어주는 것으로 변경하기로 하였다.

여기서 또 한가지 문제점이 발생하는데, FCM Message의 수신자를 지정할 때는 크게 두 가지 방법이 있다.

  1. 기기토큰을 기반으로 하는 Multicast 방법
  2. 구독을 기반으로 하는 Multicast 방법

여기서 2번 방법을 살펴보자. 기기토큰으로 식별한 기기는 ‘topic’을 구독할 수 있고, Message를 보낼 때 topic을 명시하면 따로 기기토큰을 전달하지 않고도 FCM 서버 내에 저장된 구독 정보를 바탕으로 여러 기기에 알림을 전달한다.

1
String condition = "'" + cafeteriaName + "' in topics && '" + type + "' in topics";

이런 식으로 구독 조건을 설정하고

1
2
3
4
return Message.builder()
                .setCondition(condition)
   				...
                .build();

메시지 빌드할 때 설정한 조건 추가를 하는 방식

기존에는 2번 방법을 사용하여 메시지를 전달하고 있었다. 따라서 알림을 수신받을 사용자 정보를 따로 가져오지 않았기 때문에 알림받을 유저를 식별하여 받은 알림을 저장할 수 없었다.

그래서 알림 설정을 기반으로 메시지를 수신받을 유저 리스트를 가져오는 과정이 필요 했는데, 이 방법을 사용할 경우 그냥 사용자와 일대일로 관계를 맺고 있는 NotificationSet에 접근하여 유저를 조회 해야했다.

그런데 막상 이렇게 유저를 조회하다 보니 같은 테이블인 NotificationSet에 저장된 기기토큰까지 얻어와서 Multicast를 하는 방법이 더 좋아 보였다.

혹여나 구독 정보와 실제 알림 설정 정보가 다를 경우에 잘못된 알림이 갈 수도 있기 때문이다. 애초에 구독정보가 내 DB와 FCM Server에** 중복으로 관리하는 것은 비효율적이다. **정보의 불일치가 생길 수 있기 때문에 이런 중복은 제거해주어야 한다.

결론적으로 FCM에 저장된 구독 기반의 메시지 송신 방법이 아닌 내 서버에 저장된 유저별 알림 구독을 기반으로 메시지를 보내는 방법으로 변경 하였다.


메시지 송신 방법 변경

변경된 메시지 송신 로직을 요약해보자

  1. 메시지의 제목과 본문을 작성한다.
  2. 알림 설정에 따라 유저 리스트를 가져와 수신받을 기기토큰 리스트를 등록 한다.
  3. 유저들과 알림 사이 중간테이블에 인스턴스를 생성한다.

여기서 2번에 주목하자 아까 알림 설정을 봤듯이 알림은 식당이 3개 항목이 5개이다. Spring Data JPA에서 이 모든 경우의 수를 다 고려하여 메서드를 만드는 것은 매우매우 비효율적이다.

“단순히 어떤 알림 설정이 ON이면 보낸다” 라면 8개 정도로 그냥저냥 사용할만 했겠지만 어떤 식당에 대해 어떤 항목이 ON OFF인지를 판단해야 하고(여기서만 3*5=15), 심지어는 나중에 더 자세히 설명하겠지만 어떤 식당은 구독을 했고, 어떤 식당은 구독을 안했는지에 따라서도 다른 알림을 보내야 하는 경우도 있다.

1
2
@Query("SELECT u FROM users u WHERE u.notificationSet.todayDiet = true AND u.notificationSet.myeongJin = true AND u.notificationSet.hakGwan = false")
    List<User> findByAcceptedTodayDietAndOnlyMyeongJin();

_이런 쿼리 메서드가 18개나 있었다.. _ 이러한 경우를 고려해볼때 유연하게 동적으로 쿼리를 만드는 것이 필요하다고 판단하여 QueryDSL을 도입하였다.

QueryDSL을 도입하는 과정은 다음 글에서 얘기해보고자 한다.


마치며

처음 알림을 구현해보는 만큼 이런저런 우여곡절이 많았다. 특히 IOS와 Android간 차이점도 있어서 테스트가 까다로웠다._ 둘이 좀 친해지면 안되냐..?_

여하튼 만약 알림 기능을 구현해야 하는 경우, 단순한 알림의 경우 FCM의 구독을 이용해도 괜찮을 것 같고

알림 메시지를 저장하여 관리하고, 구독 조건에 따라 유연하게 알림을 보내고 싶다면 처음부터 서버에서 구독정보를 저장하여 처리하는 것이 좋은 것 같다.

그리고 친구와 얘기를 하다가 나온 아이디어인데 어차피 이런 구독기반의 서비스를 중간 FCM을 이용하여 중간의 오버헤드를 늘리는 것 보단

직접 DB Connection을 유지한 상태로 Redis의 Pub/Sub 기능을 잘 활용하면 훨씬 빠른 속도로 알림을 보내고, 유연하게 관리할 수 있지 않을까 하는 얘기가 나왔었는데.. 이는 모바일 기기의 알림 구조를 잘 모르고 했던 말이었다.

찾아 보니 안드로이드든 ios이든 구조는 비슷하다. 푸시 서버를 중간에 두고, 앱에서 토큰을 등록하면 클라이언트와 persistent connection을 유지 한다.

그리고 FCM이나 APNs와 같은 서비스를 통해 개발자에게 메시지를 편하게 보낼 수 있는 API를 제공하는 것이다.

이런 서비스는 알림 송신에 있어 최적화된 방법인 XMPP를 기반으로 한 persistent connection 프로토콜을 이용하고, 또 무료인데 굳이 머리를 싸매며 새로운 방법을 고안할 필요가 없다고 생각이 들었다. 공학도는 장인이 아니다.

따라서 처음 생각했던 Redis Pub/Sub 방법은 애초에 단순히 데이터를 전달하는게 아닌 ‘알림’에 있어서는 적절하지 않은 방법이었다.

출처

https://firebase.google.com/docs/cloud-messaging/fcm-architecture?hl=ko (FCM 공식문서)

https://developer.apple.com/documentation/usernotifications#//apple_ref/doc/uid/TP40008194-CH8-SW1 (애플 공식문서)

https://stackoverflow.com/questions/41511475/android-push-notification-without-firebase(stack overflow 문답)

탐식당 서버 깃허브 주소: https://github.com/Team-Fiveguys/TAM-Cafeteria-Server

This post is licensed under CC BY 4.0 by the author.