FCM 이란?
FCM 적용 방법
FCM 이란?
- Firebase 클라우드 메시징(FCM)은 비용 없이 안정적으로 메시지를 보낼 수 있는 플랫폼 간 메시징 솔루션입니다.
라고 공식문서에서는 말한다. 좀 딱딱해 보이니 더 간단하게 한 줄로 요약하면
Web, Android, iOS 등 여러 플랫폼에 메시지 보내주는 서비스이다.
FCM의 구조는 다음과 같다.
저 그림이 좀 쉽게 이해가 되지 않아서 글로 간단하게 나타내자면 다음과 같다.
Web, Android, iOS (이하 클라이언트)에서 Backend와 소통하는 중에 Backend에게 본인이 원하는 대상들에게 어떤 메세지를 보내고 싶다고 요청을한다.
Backend는 이 요청을 내부적으로 처리하고 FCM에게 요청을 이관한다.
FCM 은 등록된 클라이언트에게 Backend에게 이관받은 메세지를 뿌려준다.
그러면 메세지는 어떻게 생겼는가?
아래와 같은 json형식으로 메세지를 만들면된다.
1
2
3
4
5
6
7
8
9
{
"message": {
"token": "bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
"notification": {
"title": "Portugal vs. Denmark",
"body": "great match!"
}
}
}
여기서 token은 클라이언트 플랫폼에서 각 기기마다의 정보를 바탕으로 고유한 token을 발급받아야 하는 것이다.
notification은 title, body로 이루어져 있어서 말 그대로 메세지의 제목, 내용을 담아 보내면 되는 것이다.
FCM 토큰 사용하기
기본 모범 사례
서버에 등록 토큰을 저장한다.
서버의 역할은 각 클라이언트의 토큰을 추적하고 업데이트된 활성 토큰 목록을 유지하는 것이다. (DB에 담거나 캐시에 담거나 등 서버에서 토큰을 관리해야한다.)
코드와 서버에 토큰 타임스탬프를 구현하고 정기적으로 이 타임스탬프를 업데이트하는 것이 좋다.
오래되어 저장된 토큰을 제거해야한다.
잘못된 토큰 응답의 명백한 경우 토큰을 제거하는 것 외에, 토큰이 만료된 상황 또한 고려해야한다.
등록된 토큰 검색 및 저장
앱을 처음 시작할 때 FCM SDK는 클라이언트 앱 인스턴스에 대한 등록 토큰을 생성한다.
이는 API의 대상 전송 요청에 포함하거나 주제 대상 지정을 위해 주제 구독에 추가해야 하는 AccessToken 이다.
앱은 초기 시작 시 이 토큰을 검색하고 타임스탬프와 함께 앱 서버에 저장해야 한다.
이 타임스탬프는 FCM SDK에서 제공하지 않으므로 코드와 서버에서 구현해야 한다.
또한 토큰을 서버에 저장하고 다음과 같이 변경될 때마다 타임스탬프를 업데이트해야한다.
- 앱이 새 기기에서 복원되는 상황
- 사용자가 앱을 제거 -> 재설치 하는 상황
- 사용자가 앱 데이터를 지우는 상황
코드로 적용해보기
0. FCM 전역 설정정보 추가
YML 파일 추가
1
2
3
fcm:
key: firebase_key.json
project-id: ~~~
1
2
3
4
5
6
7
spring:
profiles:
include:
- jpa
- jwt
- aws
- fcm
resourced 폴더에 FCM 사용자 key file 넣어주기
key file은 json 타입으로 다음과 유사하게 생긴 내용이 담긴 파일이다.
1
2
3
4
5
6
7
8
9
10
11
12
{
"type": "service_account",
"project_id": "~~~",
"private_key_id": "~~~",
"private_key": "~~~",
"client_email": "~~~",
"client_id": "~~~",
"auth_uri": "~~~",
"token_uri": "~~~",
"auth_provider_x509_cert_url": "~~~",
"client_x509_cert_url": "~~~"
}
1. 의존성 추가
1
2
3
4
5
6
7
8
9
10
dependencies {
...
implementation 'com.google.firebase:firebase-admin:6.8.1'
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.2.2'
...
}
2. Fcm 설정정보 등록하기
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
32
33
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
@Configuration
public class FcmConfig {
@Value("${fcm.key}")
private String secretKey;
@Value("${fcm.project-id}")
private String projectId;
@Bean
public FirebaseApp firebaseApp() {
final ClassPathResource resource = new ClassPathResource(secretKey);
try (InputStream stream = resource.getInputStream()) {
final FirebaseOptions firebaseOptions = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(stream))
.setProjectId(projectId)
.build();
return FirebaseApp.initializeApp(firebaseOptions);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
3. FCM 메세지 폼 지정하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.usw.sugo.domain.user.user.User;
import lombok.Getter;
@Getter
public class FcmMessage {
private final User user;
private final String title;
private final String body;
public FcmMessage(User user, String title, String body) {
this.user = user;
this.title = title;
this.body = body;
}
}
4. FCM 푸쉬 서비스
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import static com.usw.sugo.global.exception.ExceptionType.INTERNAL_PUSH_SERVER_EXCEPTION;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.MulticastMessage;
import com.google.firebase.messaging.Notification;
import com.usw.sugo.domain.user.user.User;
import com.usw.sugo.global.exception.CustomException;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class FcmPushService {
private final FirebaseApp firebaseApp;
@Value("${fcm.key}")
private String secretKey;
@Value("${fcm.project-id}")
private String projectId;
private final String CONFIG_PATH = secretKey; // 토큰 발급 URL
private final String AUTH_URL = "https://www.googleapis.com/auth/cloud-platform"; // 엔드포인트 URL
private final String SEND_URL =
"https://fcm.googleapis.com/v1/projects/" + projectId + "/messages:send";
private String getAccessToken() throws IOException {
GoogleCredentials googleCredentials = GoogleCredentials.fromStream(
new ClassPathResource(CONFIG_PATH).getInputStream()).createScoped(List.of(AUTH_URL));
googleCredentials.refreshIfExpired();
return googleCredentials.getAccessToken().getTokenValue();
}
@Async
public void sendPushNotification(FcmMessage fcmMessage) {
try {
extractUserTokenByPushAlarmAllowed(fcmMessage.getUser());
} catch (NullPointerException exception) {
return;
// throw new CustomException(INTERNAL_PUSH_SERVER_EXCEPTION_BY_TOKEN_NOT_ACCESSIBLE);
}
MulticastMessage multicastMessage = MulticastMessage.builder()
.setNotification(new Notification(fcmMessage.getTitle(), fcmMessage.getBody()))
.addAllTokens(
Collections.singletonList(extractUserTokenByPushAlarmAllowed(fcmMessage.getUser())))
.build();
try {
FirebaseMessaging.getInstance(firebaseApp).sendMulticast(multicastMessage);
} catch (FirebaseMessagingException e) {
throw new CustomException(INTERNAL_PUSH_SERVER_EXCEPTION);
}
}
private String extractUserTokenByPushAlarmAllowed(User user) {
if (user.getPushAlarmStatus()) {
return user.getFcmToken();
}
return null;
}
}
5. PushService 적용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Transactional
public String executeSendNoteContent(
Note note, String message, Long senderId, Long receiverId) {
User sender = userServiceUtility.loadUserById(senderId);
User receiver = userServiceUtility.loadUserById(receiverId);
saveNoteContentByText(note, message, sender, receiver);
note.updateRecentContent(message);
note.updateUserUnreadCountBySendMessage(sender);
fcmPushService.sendPushNotification(new FcmMessage(receiver, fixedPushAlarmTitle, message));
return message;
}
임시로 완성되지 않은 iOS 애뮬레이터에서 AccessToken을 발급받고 DB에 삽입한 후
위 코드를 실행시키는 API를 요청했을 때 정상적으로 push 알림이 가는 것을 확인했다.
이제 남은 것은 클라이언트에게 각 유저마다의 FCM AccessToken을 발급받아 서버에서 관리하는 내용만 만들면 된다.
이 부분은 아직 클라이언트 팀원들과 다루지 않은 내용이므로 실제로 적용하는 방법을 추가할 예정이다.