진행 중인 리멤버미 프로젝트에서는 특정 시간(아침 9시, 오후 6시)마다 전체 사용자에게 추억 회상 push 알림을 보냅니다. 이때 FCM을 사용하는 과정을 정리해보았습니다.
FCM으로 push 알림 보내기
우선 아래 이유들로 FCM을 push 서버로 사용하기로 했습니다.
FCM을 사용하는 이유?
- 교차 플랫폼 가능
- 해당 프로젝트에서는 클라이언트 앱으로 안드로이드/ios 둘 다 하고 있기 때문에 교차 플랫폼 메시징 서비스를 사용하기로 했습니다.
- 백 그라운드일 때도 알림 가능
- FCM을 사용하지 않고 서버에서 직접 알림 기능을 준다면, 앱이 서버와 연결된 포그라운드 상황이어야 합니다.
- FCM을 사용하면, 클라이언트가 포그라운드로 실행 중이지 않더라도, FCM 서버에서 알림을 줄 수 있습니다.
참고로 포그라운드 : 앱을 현재 사용중인 상태이고, 백그라운드는 앱을 켰지만 현재 창에서 실행 중이지는 않은 상황을 뜻합니다.
FCM 작동 방식
FCM 작동 방식은 다음과 같습니다.
- 우선 디바이스를 FCM에 등록하고, 토큰을 발급받습니다.
- 디바이스는 앱 서버(Spring Boot)에 각자의 토큰을 전송하고, 앱서버는 이를 저장합니다.
- 앱 서버는 푸시 서버(FCM)에 알림을 요청합니다.
- (앱서버도 FCM에 등록)
- 푸시 서버(FCM)은 디바이스에 푸시 알림을 보냅니다.
참고로 해당 프로젝트에서는 개인별로 다른 알림을 주기 위해 Token을 사용하였는데, 특정 사용자 집합에 똑같은 알림을 보낼때는 Topic을 사용해줄 수 있습니다. (개인의 추억으로 알림을 주어야 하기 때문에 개인마다 다른 알림을 주어야 했습니다)
FCM의 HTTP는 기본적으로 동기방식
FCM은 XMPP와 HTTP 두가지 프로토콜로 사용할 수 있습니다. XMPP는 업스트림/다운 스트림 둘다 가능하지만 HTTP는 다운스트림만 가능합니다. 해당 프로젝트에서는 다운 스트림만 필요하기 때문에 HTTP를 사용해주었습니다. 참고로 FCM의 예전 HTTP 방식이 아닌 최신 FCM HTTP v1 API을 사용합니다!
- 업스트림 : 디바이스 → 앱 서버로 알림
- 다운스트림 : 앱 서버 → 디바이스로 알림
이떄 FCM의 HTTP 프로토콜은 기본적으로 동기 방식으로 구현됩니다. 즉, push 알림을 보내고 사용자의 정상 응답을 기다립니다. 정상 응답이 확인 되기 전까지는 다른 사용자에게 push 알림을 보내지 않습니다. (이전에 정리한 동기 방식을 비교한 글 참고)
- A 사용자에게 보내고
- A 사용자가 정상 수신하면 (정상 수신했는지 확인할때까지 앱 서버는 기다림)
- B 사용자에게 보내고
- B 사용자가 정상 수신하면 (또 앱 서버는 기다림)
- C 사용자에게 보내고
- … 반복
즉, 사용자의 수신 확인을 할때까지 앱 서버는 일을 하지 않고 대기하게 됩니다. 사용자 전체에게 푸시 알림을 일괄적으로 보내는데 각 사용자들의 응답을 기다리는 건 굉장히 비효율적입니다. 따라서 FCM 알림을 비동기 방식으로 구현해주겠습니다.
FCM 구현하기 - 비동기로!
1. Firebase 프로젝트 생성하고 키값 Spring Boot에 등록해주기 / 의존성 추가해주기
Firebase 프로젝트 생성 후 키값을 등록해줍니다. 이때 주의사항‼은 Firebase 키를 public한 레포지토리에 올리면 구글이 추적(?)해서 키값을 오픈된 곳에 올리지 말라고 메일이 옵니다..! 그리고 해당 프로젝트가 막힙니다. 그러니 키는 올리지 말고 private하게 관리해주어야 합니다.
의존성은 다음을 추가해주면 됩니다. 참고로 firebase 패키지 6.X.X 버전은 Notification 객체에 image를 넣어줄 수 없습니다. 7.X.X은 image를 추가해줄 수 있어서, 알림 시 원하는 앱 아이콘을 넣고 싶으면 7버전을 사용해주어야 합니다.
implementation group: 'com.google.firebase', name: 'firebase-admin'
2. FCM 기본 설정해주기
코드에 대한 간단한 설명을 정리해보면,
- @PostConstruct : 서버 실행하면 다른 호출 없어도 실행
- (이때 필요한 의존성 주입이 있다면, 의존성 주입후 실행)
- 이를 통해 서버 작동 시, Firebase 어플리케이션을 딱 한번만 초기화해줍니다.
- GoogleCredentials 생성 : 발급받은 FCM 키와 FCM 사용 범위를 통해 GoogleCredentials을 생성해줍니다.
- FirebaseOptions 생성: GoogleCredentials를 통해 FirebaseOptions을 생성해줍니다. 그리고 이 옵션 객체를 통해 FirebaseApp를 생성하고 FirebaseMessaging 객체를 생성합니다. (FirebaseMessaging는 필드로 선언해주었습니다.)
3. Message
/ Notification
객체 생성
다음은 오전 9시에 모든 사용자들에게 일괄적으로 전송하는 로직입니다.
Notification
: 기본 알림 템플릿으로, 어플리케이션에 표시되는 알림 정보들을 위한 객체입니다.- title : 푸시 알림 타이틀(굷은 글씨)
- body : 푸시 알림 내용(타이틀 아래 작은 글씨)
- image : 푸시 알림 시 앱 아이콘
Message
: 기본 알림 템플릿Notification
외에도 모바일 디바이스에 전송할 데이터가 있을 경우, Message에 데이터를 더 추가해줄 수 있습니다. 해당 프로젝트에서는 MemoryId만 추가해주었습니다.
만약 사용자가 해당 알림을 스왑하면 추억 상세 조회 화면으로 넘어갑니다. 이때를 위해 MemoryId를 Message
에 추가해주었습니다.
추억 알림을 스왑하지 않으면 굳이 Memory의 모든 데이터를 받을 필요가 없으므로, MemoryId값만 알림으로 우선 전송해줍니다. (많은 데이터를 FCM를 통해 매번 전송하는 건 낭비라고 생각했습니다)
만약 사용자가 알림을 스왑하여 Memory 모든 데이터가 필요할 때(추억 상세 화면)만 해당 id로 Memory를 다시 조회하게끔 구현했습니다.
자세한 내용은 공식 문서(https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?hl=ko)를 참고했습니다.
4. 이때, send()가 아니라 sendAsync()로 구현해주기
참고로 이때 공식 문서 혹은 메소드 정보 찾아보면 send(동기)랑 sendAsync(비동기)일때의 리턴값이 다릅니다. 그래서 코드 구현할 때
firebaseMessaging.send(message);
// 동기 방식firebaseMessaging.sendAsync(message).get();
// 비동기 방식은 get()으로 객체 한번 꺼내주어야 정상 작동합니다!
- SendAsync() 공식 문서 : https://firebase.google.com/docs/reference/admin/dotnet/class/firebase-admin/messaging/firebase-messaging#sendasync_1
이렇게 FCM을 사용하여 사용자에게 알림 보내는 기능을 구현해보았습니다. 비동기 방식으로 구현하는 방법 자체는 간단하지만, 동기 vs 비동기에 대한 내용을 평소에 고민해봐야 적용할 생각을 할 수 있을 것 같습니다.
진행 중인 프로젝트는 다음 링크에서 확인할 수 있습니다.