배경
앱용 API를 개발하던 중 App push 기능이 필요하여 FCM을 이용하여 구현하게 되었습니다.
선행 학습
FCM (Firebase Cloud Messaging)
•
Firebase 클라우드 메시징(FCM)은 메시지를 안정적으로 무료 전송할 수 있는 크로스 플랫폼 메시징 솔루션입니다.
•
FCM을 사용하면 새 이메일이나 기타 데이터를 동기화할 수 있음을 클라이언트 앱에 알릴 수 있습니다.
•
이렇게 알림 메시지를 전송하여 사용자를 유지하고 재참여를 유도할 수 있습니다.
•
채팅 메시지와 같은 사용 사례에서는 메시지로 최대 4,000바이트의 페이로드를 클라이언트 앱에 전송할 수 있습니다.
주요 기능
•
알림 메시지 또는 데이터 메시지 전송
◦
사용자에게 표시되는 알림 메시지를 전송합니다. 또는 데이터 메시지를 전송하고 애플리케이션 코드에서 임의로 처리합니다.
•
다양한 메시지 타겟팅
◦
단일 기기, 기기 그룹, 주제를 구독한 기기 등 3가지 방식으로 클라이언트 앱에 메시지를 배포할 수 있습니다.
•
클라이언트 앱에서 메시지 전송
◦
FCM의 신뢰성 높고 배터리 효율적인 연결 채널을 통해 기기에서 다시 서버로 확인, 채팅, 기타 메시지를 보낼 수 있습니다.
기본 원리
•
FCM Server
◦
App Server에서는 FCM Server에 메시지 요청을 보내고, FCM Server는 사용자 기기에서 실행되는 클라이언트 앱에 메시지를 보냅니다.
•
App Server
◦
메시지를 작성, 타겟팅, 전송할 수 있는 신뢰할 수 있는 환경
•
Client
◦
메시지를 수신하는 Apple, Android 또는 웹(자바스크립트) 클라이언트 앱
통신 흐름
•
FCM으로 보낸 모든 메시지는 ACK 또는 NACK 응답을 받습니다. 이러한 응답을 받지 못한 메시지는 대기 중인 것으로 간주됩니다. 대기 중인 메시지 수가 100개에 도달하면 앱 서버에서 메시지를 새로 보내는 것을 중지하고 그림에 나와 있는 것처럼 FCM이 대기 중인 기존 메시지 중 일부를 확인할 때까지 기다려야 합니다.
•
반대로, 확인되지 않은 메시지가 너무 많으면 앱 서버에 과부하가 발생하지 않도록 FCM이 전송을 중지합니다. 따라서 FCM을 통해 클라이언트 애플리케이션으로부터 수신한 업스트림 메시지를 앱 서버에서 최대한 빠르게 'ACK' 처리하여 수신 메시지의 흐름을 일정하게 유지해야 합니다. 이러한 ACK에는 앞서 설명한 대기 중인 메시지의 한도가 적용되지 않습니다. 대기 중인 메시지 수가 100개에 도달해도 새로운 업스트림 메시지의 전송이 차단되지 않도록 앱 서버가 FCM에서 수신한 메시지에 대해 계속 ACK를 보내야 합니다.
•
ACK는 연결 1개 내에서만 유효합니다. 메시지를 ACK 처리하기 전에 연결이 종료되면 다시 ACK 처리하기 전에 FCM에서 업스트림 메시지를 다시 보낼 때까지 앱 서버에서 대기해야 합니다. 마찬가지로 연결이 종료되기 전에 FCM에서 ACK 또는 NACK가 수신되지 않은 대기 중인 모든 메시지도 다시 보내야 합니다.
Firebase Admin SDK
•
Admin FCM API는 백엔드 인증을 처리하고 메시지 보내기와 주제 구독 관리를 지원합니다.
•
한 번에 최대 1,000대의 기기 등록 토큰을 구독하거나 구독을 취소할 수 있습니다.
•
Firebase Admin SDK를 사용하면 다음을 수행할 수 있습니다.
◦
개별 기기에 메시지 보내기
◦
주제 및 하나 이상의 일치하는 조건문에 메시지 보내기
◦
기기에서 주제 구독 및 구독 취소
◦
다양한 타겟 플랫폼에 맞는 메시지 페이로드 구성
서버 옵션
•
FCM 서버와 상호작용하는 방법을 결정해야 합니다. Firebase Admin SDK 또는 원시 프로토콜을 사용할 수 있습니다.
◦
Firebase Admin SDK
▪
Node, 자바, Python, C#, Go 지원
◦
FCM HTTP v1 API
▪
가장 최신 프로토콜로서 보다 안전한 승인과 유연한 크로스 플랫폼 메시징 기능 제공(Firebase Admin SDK는 이 프로토콜을 기반으로 하며 모든 고유 이점을 제공함). 새 기능은 일반적으로 HTTP v1 API에만 추가되므로 대부분의 사용 사례에 이 API를 사용하는 것이 좋습니다.
◦
기존 HTTP 프로토콜
▪
새 프로젝트에서는 기존 프로토콜 대신 FCM v1 HTTP API를 사용하는 것이 좋습니다.
◦
기존 XMPP 서버 프로토콜
▪
새 프로젝트에서는 기존 프로토콜 대신 FCM v1 HTTP API를 사용하는 것이 좋습니다.
메시지 포맷
•
name
◦
출력 전용의 projects/*/messages/{message_id} 형식으로 전송된 메시지 식별자
•
data
◦
URL Scheme을 가진 딥링크 등의 앱에게 무언가 제공해주어야 하는 데이터를 담는 객체
◦
키는 예약어("from", "message_type" 또는 "google" 또는 "gcm")로 시작하는 단어 사용 불가
•
notification
◦
입력만 가능한 모든 플랫폼에서 사용할 기본 알림 템플릿
•
token
◦
메시지를 받을 기기의 등록 토큰
•
topic
◦
특정 집단의 기기에 메시지를 보낼 주제
결론
위 사진은 대략적인 앱 → 백엔드 API → FCM 백엔드 API → 유저 디바이스 의 흐름입니다.
현재 안드로이드와 IOS 두 가지 플랫폼에 대한 App Push를 구현해야 하므로 크로스 플랫폼이 가능한 Notification을 사용하고
FCM HTTP v1 API의 기능이 모두 포함되면서 사용하기 쉬운 Admin SDK를 사용하도록 하겠습니다.
개발
프로젝트 생성
Firebase에 이미 프로젝트가 생성되어 있어 생성하는 부분은 생략하겠습니다.
라이브러리 추가
//Firebase Admin SDK
implementation 'com.google.firebase:firebase-admin:9.1.1'
Java
복사
build.gradle에 firebase admin sdk 라이브러리를 추가해줍니다.
FirebaseApp 초기화
Firebase 콘솔 - 프로젝트 설정 - 서비스 계정에서 비공개 키를 생성하여 json파일을 받아서 프로젝트에 추가합니다.
APN 인증서나 Google Cloud의 권한 설정도 잊지않고 해줍니다.
저는 Github Action의 Secrets로 이런 private한 파일들을 관리하는데 json들을 Secrets로 사용하면 손상되는 이슈가 있어서 아래와 같은 라이브러리를 사용하여 추가해주었습니다.
- name: Copy JSON
id: create-json
uses: jsdaniell/create-json@v1.2.2
with:
name: "serviceAccountKey.json"
json: ${{ secrets.SERVICE_ACCOUNT_KEY }}
dir: 'resources/key/'
YAML
복사
FirebaseApp을 초기화하는 코드를 작성해줍니다.
저는 FirebaseConfig 클래스를 만들어서 @Configuration 어노테이션을 달아 빈으로 등록되게끔 했습니다.
//FirebaseConfig.java
@Bean
public FirebaseApp createFireBaseApp() throws IOException {
try(InputStream inputStream = new ClassPathResource(keyPath).getInputStream()) {
FirebaseOptions options = new FirebaseOptions.Builder()
.setCredentials(GoogleCredentials
.fromStream(inputStream)
.createScoped(scope))
.build();
return FirebaseApp.initializeApp(options);
}
}
Java
복사
비공개 키와 GoogleCredentials를 이용해 AccessToken을 받아오고
해당 AccessToken을 FirebaseOptions의 Credentials로 설정하여 FirebaseApp을 초기화해줍니다.
DTO 작성
//FcmMessageDTO.java
@Getter @Setter
public class FcmMessageDTO {
private String token;
private String topic;
private Notification notification;
@Builder
public FcmMessageDTO(String token, String topic, Notification notification) {
this.token = token;
this.topic = topic;
this.notification = notification;
}
@Getter @Setter
public static class Notification {
private String title;
private String body;
@Builder
public Notification(String title, String body) {
this.title = title;
this.body = body;
}
}
public Message toMessage() {
return Message.builder()
.setToken(this.token)
.setTopic(this.topic)
.setNotification(com.google.firebase.messaging.Notification.builder()
.setTitle(this.notification.title)
.setBody(this.notification.body)
.build())
.build();
}
}
Java
복사
간단한 알림 기능만 필요하기 때문에 알림을 받을 기기의 등록 토큰, 토픽, 제목, 내용만을 받는 DTO를 작성합니다.
toMessage 메서드를 만들어 Admin SDK에서 사용하는 Message 객체로 변환해줍니다.
Service 작성
//FcmService.java
@Slf4j
@Service
@RequiredArgsConstructor
public class FcmService {
public ResponseDTO sendMessage(FcmMessageDTO fcmMessageDTO) {
Message message = fcmMessageDTO.toMessage();
try {
FirebaseMessaging.getInstance().send(message);
return ResponseDTO.builder()
.status(200)
.statusName("OK")
.build();
} catch (FirebaseMessagingException e) {
throw new CustomException(ErrorCode.FAIL_SEND_NOTIFICATION);
}
}
}
Java
복사
Admin SDK를 사용하여 FirebaseMessaging 객체로 메시지, 알림 전송의 모든 기능을 사용할 수 있습니다.
테스트
•
Postman
Header에 키를 ‘Authorization’ 값을 ‘Bearer 토큰’ 으로 넣은 후
https://fcm.googleapis.com/v1/projects/{프로젝트ID}/messages:send 로 POST 요청을 보내면 테스트할 수 있습니다.
정상적으로 앱 푸쉬가 오는 것을 확인할 수 있었습니다.