1️⃣ 파이어베이스 서버와 스프링부트 서버 연결
2️⃣ 파이어베이스에 보낼 메시지
형식을 DTO로 만들어서 커스텀하고 관리하자
3️⃣ 메시지 만들고, 파이어베이스 서버로 전송
! → 이걸 호출하는 시점이 푸시알림을 보내고 싶은 서비스 로직 상에 있으면 됨
[전제] User 테이블 안에 있는 1명의 유저를 대상으로 푸시 알림 테스트를 진행한다.
스프링부트 프로젝트에 미리 ‘fcm_token’ 필드를 가지고 있는 User 테이블을 생성해두었습니다! 저는 회원가입 시 전달받은 FCM 토큰을 유저 DB에 한번에 저장해두고 유저 아이디와 각각 매핑되는 방식으로 구현해보겠습니다
Firebase Console > ‘프로젝트 만들기’
그냥 ‘계속’ 을 쭉쭉 누르시고 생성하세요~
그럼 이렇게 내가 지정한 프로젝트명의 콘솔로 들어오게 됩니다
✅ 여기서 ‘엡에 Firebase를 추가하여 시작하기’ 부분이 클라분들(안드,아요)이 등록해주셔야 하는 부분입니다!!
왼쪽 상단에 톱니바퀴를 누르고, [프로젝트 설정]을 클릭
[서비스 계정] 탭 > [새 비공개 키 생성] 버튼 클릭
비공개 키(*.json)를 다운 받은 후, 프로젝트의 src/main/resources
디렉토리 안에 파일을 넣어줍시다!
🚨 .gitignore
에 미리 파일명을 추가해두고, 절대절대절대 Github에 올리지 않도록 주의합니다. (저는 잘못 올렸다가 커밋 기록 삭제해달라고 깃헙에 문의드린 경험이 있습니다 ,,허허)
*파일명이 엄청 길텐데 얘는 그냥 임의로 바꿔주셔도 됩니다!
build.gradle
에 FCM 설정에 필요한 의존성 라이브러리 추가
// FCM
implementation 'com.google.firebase:firebase-admin:9.1.1'
implementation 'com.squareup.okhttp3:okhttp:4.10.0' // Firebase 서버로 푸시 메시지 전송 시 필요
application.yml
설정 추가
fcm:
key:
path: firebase-adminsdk.json
scope: https://www.googleapis.com/auth/cloud-platform
path
: 위에서 추가한 비공개 키 파일의 경로와 파일명을 명시 *지금은 같은 resources/
하위에 있으므로 그냥 파일명만 적어주자!scope
: 권한의 범위를 설정 → Google Cloud 데이터 확인, 수정, 구성, 삭제 및 Google 계정 이메일 주소 확인 에 대한 권한을 부여하도록 명시 *참고 - https://developers.google.com/identity/protocols/oauth2/scopes?hl=ko#fcmFCMConfig
에서 기본 설정에 대한 내용 추가
// common/config/FCMConfig.java
@Slf4j
@Configuration
public class FCMConfig {
@Value("${fcm.key.path}")
private String SERViCE_ACCOUNT_JSON;
@PostConstruct
public void init() {
try {
ClassPathResource resource = new ClassPathResource(SERViCE_ACCOUNT_JSON);
InputStream serviceAccount = resource.getInputStream();
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build();
FirebaseApp.initializeApp(options);
log.info("파이어베이스 서버와의 연결에 성공했습니다.");
} catch (IOException e) {
log.error("파이어베이스 서버와의 연결에 실패했습니다.");
}
}
}
@@Value
어노테이션으로 application.yml
에서 지정해준 파일명을 SERVICE_ACCOUNT_JSON 변수에 넣어준다.InputStream
클래스의 fromStream()
메서드를 이용해 파일의 내용을 읽는다.// Android
public AndroidConfig TokenAndroidConfig(FCMPushRequestDto request) {
return AndroidConfig.builder()
.setCollapseKey(request.getCollapseKey())
.setNotification(AndroidNotification.builder()
.setTitle(request.getTitle())
.setBody(request.getBody())
.build())
.build();
}
// Apple
public ApnsConfig TokenApnsConfig(FCMPushRequestDto request) {
return ApnsConfig.builder()
.setAps(Aps.builder()
.setAlert(
ApsAlert.builder()
.setTitle(request.getTitle())
.setBody(request.getBody())
.setLaunchImage(request.getImgUrl())
.build()
)
.setCategory(request.getCollapseKey())
.setSound("default")
.build())
.build();
}
공식 문서와 동일한 메시지 형식에 따라 FCMMessage
클래스 생성
@Builder
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class FCMMessage {
**private boolean validateOnly;
private Message message;**
@Builder
@AllArgsConstructor
@Getter
public static class Message {
**private Notification notification;** // 모든 모바일 OS에 통합으로 사용할 수 있는 Notification
private String token; // 특정 디바이스(클라이언트)에 알림을 보내기 위한 토큰
private String topic; // 주제 구독 시 사용
}
@Builder
@AllArgsConstructor
@Getter
**public static class Notification {
private String title;
private String body;
private String image;
}**
}
* - Request
* {
* **"validate_only": boolean,
* "message": {
* object (Message)
* }**
* }
*
*
* - Message
* {
* **"name": string,
* "data": {
* string: string,
* ...
* },
* "notification": { ✅모든 플랫폼에서 사용할 기본 알림 템플릿
* object (Notification)
* },**
* "android": { FCM 연결 서버를 통해 전송된 메시지에 대한 Android 전용 옵션
* object (AndroidConfig)
* },
* "webpush": { Web 푸시 알림을 위한 webpush 프로토콘 옵션
* object (WebpushConfig)
* },
* "apns": { Apple 푸시 알림 서비스 특정 옵션
* object (ApnsConfig)
* },
* "fcm_options": { 모든 플랫폼에서 사용할 FCM SDK 기능 옵션용 템플릿
* object (FcmOptions)
* },
*
* // Union field target can be only one of the following:
* "token": string, 메시지를 보낼 등록 토큰 (특정 클라이언트 대상)
* "topic": string, Topic 발행의 경우, 사용
* "condition": string
* // End of list of possible types for union field target.
* }
→ 우리는 Android, iOS 공통으로 사용할 것이므로 Notification 부분만 참고하면 된다!커스텀 알림 DTO 생성
→ title, body로 구성
→ title, body, image로 구성
앱에서 수신하는 푸시알림의 형태를 살펴보면 다음과 같이 제목, 바디의 형태로 구성되어 있다는 공통점을 발견할 수 있는데요, (앱의 아이콘과 시간 정보는 저희 서버에서 처리해주는 것이 아닙니다!)
이를 커스텀하기 위한 DTO를 만들어서 알림을 보내는 서비스 로직에서 공통적으로 적용시켜줍시다
→ 이 DTO 필드의 값들은 위 Notification 클래스의 title, body, Message 클래스의 token에 매핑됩니다.
@Slf4j
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class FCMPushRequestDto {
private String targetToken;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String title;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String body;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
JSON 문자열로 요청, 응답값이 보내질 때 null 인 경우에 해당 필드는 아예 제외하고 반환하게 하는 어노테이션입니다. *이 어노테이션이 없다면 값이 null인 경우에 아래와 같이 표시되지만, 이를 아예 제외시켜버리기 위해서 해당 어노테이션을 붙여주었습니다!{
"targetToken" : "fcm-token-unique-value",
"title": null,
"body": "제목이 없는 푸시알림이군요"
}
application.yml
내용 추가
특정 파이어베이스 서버(=우리가 생성한 프로젝트)에 전송하기 위해 API 엔드포인트를 추가해줘야 하는데요, API 엔드포인트 형식은 공식문서에 따르면 아래와 같습니다.
POST https://fcm.googleapis.com/v1/projects/{firebase-project-ID}/messages:send
fcm:
key:
path: firebase-adminsdk.json
scope: https://www.googleapis.com/auth/cloud-platform
api:
url: **https://fcm.googleapis.com/v1/projects/{fcm-project-id}/messages:send**
topic:
"sopt-topic"
IntelliJ에서 환경변수 관리하기
[Edit] > [Modify Options] > [Environment variables]
key=value
의 형태로 작성하고(따옴표, 괄호, 공백 없음), 코드에서 환경변수를 사용할 때는 키 값으로 적어주면 됩니다!
이제 FCMService에서 Firebase에 보낼 메시지 객체를 생성하고 보내봅시다!
먼저 의존성 주입은 얘 정도만.. DTO → json 문자열로 매핑해서 String 형태로 반환하기 위해 사용할 거예요
// service/FCMService.java
private final ObjectMapper objectMapper; // FCM의 body 형태에 따라 생성한 값을 문자열로 저장하기 위한 Mapper 클래스
위 설정파일에 내용을 추가해줬으니, 어딘가 사용이 되겠죠?
바로 지금입니다.
// service/FCMService.java
@Value("${fcm.key.path}")
private String SERVICE_ACCOUNT_JSON;
@Value("${fcm.api.url}")
private String FCM_API_URL;
@Value("${fcm.topic}")
private String topic;
@Value
어노테이션으로 application.yml의 내용을 가져와서 변수에 값을 넣어준다.
topic
: 어플리케이션 내에서 관리하는 주제 (유저들이 구독 가능한)를 가져오면, 해당 주제를 구독하는 유저에게 일괄적으로 푸시 메시지 전송 가능
SERVICE_ACCOUNT_JSON
: 초기 설정과 마찬가지로 해당 파일 안에 있는 비공개 키를 읽어서 헤더에 실어야 요청을 정상적으로 보낼 수 있다!
FCM_API_URL
: 요청을 어디에 보낼 것인지 지정 → 파이어베이스 특정 서버 (미리 생성해둔 파이어베이스 프로젝트가 되겠쥬?)
얘네들은 어디서 쓰이냐구요?
// 실제 파이어베이스 서버로 푸시 메시지를 전송하는 메서드
private void sendPushMessage(String message) {
try {
OkHttpClient client = new OkHttpClient();
RequestBody requestBody = RequestBody.create(message, MediaType.get("application/json; charset=utf-8"));
Request httpRequest = new Request.Builder()
.url(**FCM_API_URL**) // 요청을 보낼 위치 (to 파이어베이스 서버)
.post(requestBody)
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + getAccessToken())
.addHeader(HttpHeaders.CONTENT_TYPE, "application/json; UTF-8")
.build();
Response response = client.newCall(httpRequest).execute();
log.info("단일 기기 알림 전송 성공 ! successCount: 1 messages were sent successfully");
log.info("알림 전송: {}", response.body().string());
} catch (IOException e) {
throw new IllegalArgumentException("파일을 읽는 데 실패했습니다.");
}
}
// Firebase에서 Access Token 가져오기
private String getAccessToken() {
try {
GoogleCredentials googleCredentials = GoogleCredentials
.fromStream(new ClassPathResource(**SERVICE_ACCOUNT_JSON**).getInputStream())
.createScoped(List.of("https://www.googleapis.com/auth/cloud-platform"));
googleCredentials.refreshIfExpired();
log.info("getAccessToken() - googleCredentials: {} ", googleCredentials.getAccessToken().getTokenValue());
return googleCredentials.getAccessToken().getTokenValue();
} catch (IOException e) {
throw new IllegalArgumentException("파일을 읽는 데 실패했습니다.");
}
}
그리구, 요청 바디에는 정해진 형식대로 반드시 지켜서 보내야 한다고 했었죠!
json 문자열을 만들어주기 위해 아까 만들어둔 FCMMessage
클래스를 이용할 것입니다
// service/FCMService.java
// 요청 파라미터를 FCM의 body 형태로 만들어주는 메서드 [단일 기기]
private String makeSingleMessage(FCMPushRequestDto request) throws JsonProcessingException {
try {
FCMMessage fcmMessage = FCMMessage.builder()
.message(FCMMessage.Message.builder()
.token(request.getTargetToken()) // 1:1 전송 시 반드시 필요한 대상 토큰 설정
.notification(FCMMessage.Notification.builder()
.title(request.getTitle())
.body(request.getBody())
.image(request.getImage())
.build())
.build()
).validateOnly(false)
.build();
return objectMapper.writeValueAsString(fcmMessage);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("JSON 처리 도중에 예외가 발생했습니다.");
}
}
// 요청 파라미터를 FCM의 body 형태로 만들어주는 메서드 [주제 구독]
private String makeTopicMessage(FCMPushRequestDto request) throws JsonProcessingException {
try {
FCMMessage fcmMessage = FCMMessage.builder()
.message(FCMMessage.Message.builder()
.topic(topic) // 토픽 구독에서 반드시 필요한 설정 (token 지정 x)
.notification(FCMMessage.Notification.builder()
.title(request.getTitle())
.body(request.getBody())
.image(request.getImage())
.build())
.build()
).validateOnly(false)
.build();
return objectMapper.writeValueAsString(fcmMessage);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("JSON 처리 도중에 예외가 발생했습니다.");
}
}
// 요청 파라미터를 FCM의 body 형태로 만들어주는 메서드 [다수 기기]
private static MulticastMessage makeMultipleMessage(FCMPushRequestDto request, List<String> tokenList) {
MulticastMessage message = MulticastMessage.builder()
.setNotification(Notification.builder()
.setTitle(request.getTitle())
.setBody(request.getBody())
.setImage(request.getImage())
.build())
.addAllTokens(tokenList)
.build();
log.info("message: {}", request.getTitle() +" "+ request.getBody());
return message;
}
타겟팅에 따라 3가지 방식으로 구현이 가능하다고 했었는데요..! 3가지 방식의 작동하는 구현을 모두 소개하고, 실제 테스트는 단일 기기만 해보겠습니다 (시험 끝나고 또 나머지도 추가할게요,,)
→ [참고][**타겟팅에 따른 구현**](https://www.notion.so/2fdec67c96a145db9ea914b8ae73998b?pvs=21)
/**
단일 기기
Firebase에 메시지를 수신하는 함수 (헤더와 바디 직접 만들기)
*/
@Transactional
public String pushAlarm(FCMPushRequestDto request) throws IOException {
String message = **makeSingleMessage**(request);
sendPushMessage(message);
return "알림을 성공적으로 전송했습니다. targetUserId = " + request.getTargetToken();
}
### 다수 기기 (특정 이벤트 발생 시, 2명 이상의 기기 대상)
```java
/**
다수 기기
Firebase에 메시지를 수신하는 함수 (동일한 메시지를 2명 이상의 유저에게 발송)
*/
public String multipleSendByToken(FCMPushRequestDto request, List userList) {
// User 리스트에서 FCM 토큰만 꺼내와서 리스트로 저장
List<String> tokenList = userList.stream()
.map(User::getFcmToken).toList();
// 2명만 있다고 가정
log.info("tokenList: {}🌈, {}🌈",tokenList.get(0), tokenList.get(1));
MulticastMessage message = makeMultipleMessage(request, tokenList);
try {
BatchResponse response = FirebaseMessaging.getInstance().sendMulticast(message);
log.info("다수 기기 알림 전송 성공 ! successCount: " + response.getSuccessCount() + " messages were sent successfully");
log.info("알림 전송: {}", response.getResponses().toString());
return "알림을 성공적으로 전송했습니다. \ntargetUserId = 1." + tokenList.get(0) + ", \n\n2." + tokenList.get(1);
} catch (FirebaseMessagingException e) {
log.error("다수기기 푸시메시지 전송 실패 - FirebaseMessagingException: {}", e.getMessage());
throw new IllegalArgumentException(ErrorType.FAIL_TO_SEND_PUSH_ALARM.getMessage());
}
}
### 주제(topic) 구독 (유저의 이벤트와 무관하게 사전에 구독한 알림 일괄 발송)
```java
/**
주제 구독 등록 및 취소
String message = **makeTopicMessage(request)**;
sendPushMessage(message);
return "알림을 성공적으로 전송했습니다. targetUserId = " + request.getTargetToken();
}
주제를 구독한 유저들이 존재하려면, 구독을 등록하거나 취소하는 로직도 필요하겠죠!
```java
// Topic 구독 설정 - application.yml에서 topic명 관리
// 단일 요청으로 최대 1000개의 기기를 Topic에 구독 등록 및 취소할 수 있다.
public void subscribe() throws FirebaseMessagingException {
// These registration tokens come from the client FCM SDKs.
List<String> registrationTokens = Arrays.asList(
"YOUR_REGISTRATION_TOKEN_1",
// ...
"YOUR_REGISTRATION_TOKEN_n"
);
// Subscribe the devices corresponding to the registration tokens to the topic.
TopicManagementResponse response = FirebaseMessaging.getInstance().subscribeToTopic(
registrationTokens, topic);
log.info(response.getSuccessCount() + " tokens were subscribed successfully");
}
// Topic 구독 취소
public void unsubscribe() throws FirebaseMessagingException {
// These registration tokens come from the client FCM SDKs.
List<String> registrationTokens = Arrays.asList(
"YOUR_REGISTRATION_TOKEN_1",
// ...
"YOUR_REGISTRATION_TOKEN_n"
);
// Unsubscribe the devices corresponding to the registration tokens from the topic.
TopicManagementResponse response = FirebaseMessaging.getInstance().unsubscribeFromTopic(
registrationTokens, topic);
log.info(response.getSuccessCount() + " tokens were unsubscribed successfully");
}
FCMService
클래스 코드 ```java
package jjun.server.pushalarm.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.messaging.BatchResponse;
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.google.firebase.messaging.TopicManagementResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import jjun.server.pushalarm.common.exception.ErrorType;
import jjun.server.pushalarm.domain.User;
import jjun.server.pushalarm.dto.fcm.FCMMessage;
import jjun.server.pushalarm.dto.fcm.FCMPushRequestDto;
import jjun.server.pushalarm.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class FCMService {
private final ObjectMapper objectMapper; // FCM의 body 형태에 따라 생성한 값을 문자열로 저장하기 위한 Mapper 클래스
@Value("${fcm.key.path}")
private String SERVICE_ACCOUNT_JSON;
@Value("${fcm.api.url}")
private String FCM_API_URL;
@Value("${fcm.topic}")
private String topic;
/**
* 단일 기기
* - Firebase에 메시지를 수신하는 함수 (헤더와 바디 직접 만들기)
*/
@Transactional
public String pushAlarm(FCMPushRequestDto request) throws IOException {
String message = makeSingleMessage(request);
sendPushMessage(message);
return "알림을 성공적으로 전송했습니다. targetUserId = " + request.getTargetToken();
}
/**
* 다수 기기
* - Firebase에 메시지를 수신하는 함수 (동일한 메시지를 2명 이상의 유저에게 발송)
*/
public String multipleSendByToken(FCMPushRequestDto request, List<User> userList) {
// User 리스트에서 FCM 토큰만 꺼내와서 리스트로 저장
List<String> tokenList = userList.stream()
.map(User::getFcmToken).toList();
// 2명만 있다고 가정
log.info("tokenList: {}🌈, {}🌈",tokenList.get(0), tokenList.get(1));
MulticastMessage message = makeMultipleMessage(request, tokenList);
try {
BatchResponse response = FirebaseMessaging.getInstance().sendMulticast(message);
log.info("다수 기기 알림 전송 성공 ! successCount: " + response.getSuccessCount() + " messages were sent successfully");
log.info("알림 전송: {}", response.getResponses().toString());
return "알림을 성공적으로 전송했습니다. \ntargetUserId = 1." + tokenList.get(0) + ", \n\n2." + tokenList.get(1);
} catch (FirebaseMessagingException e) {
log.error("다수기기 푸시메시지 전송 실패 - FirebaseMessagingException: {}", e.getMessage());
throw new IllegalArgumentException(ErrorType.FAIL_TO_SEND_PUSH_ALARM.getMessage());
}
}
/**
* 주제 구독 등록 및 취소
* - 특정 타깃 토큰 없이 해당 주제를 구독한 모든 유저에 푸시 전송
*/
@Transactional
public String pushTopicAlarm(FCMPushRequestDto request) throws IOException {
String message = makeTopicMessage(request);
sendPushMessage(message);
return "알림을 성공적으로 전송했습니다. targetUserId = " + request.getTargetToken();
}
// Topic 구독 설정 - application.yml에서 topic명 관리
// 단일 요청으로 최대 1000개의 기기를 Topic에 구독 등록 및 취소할 수 있다.
public void subscribe() throws FirebaseMessagingException {
// These registration tokens come from the client FCM SDKs.
List<String> registrationTokens = Arrays.asList(
"YOUR_REGISTRATION_TOKEN_1",
// ...
"YOUR_REGISTRATION_TOKEN_n"
);
// Subscribe the devices corresponding to the registration tokens to the topic.
TopicManagementResponse response = FirebaseMessaging.getInstance().subscribeToTopic(
registrationTokens, topic);
log.info(response.getSuccessCount() + " tokens were subscribed successfully");
}
// Topic 구독 취소
public void unsubscribe() throws FirebaseMessagingException {
// These registration tokens come from the client FCM SDKs.
List<String> registrationTokens = Arrays.asList(
"YOUR_REGISTRATION_TOKEN_1",
// ...
"YOUR_REGISTRATION_TOKEN_n"
);
// Unsubscribe the devices corresponding to the registration tokens from the topic.
TopicManagementResponse response = FirebaseMessaging.getInstance().unsubscribeFromTopic(
registrationTokens, topic);
log.info(response.getSuccessCount() + " tokens were unsubscribed successfully");
}
// 요청 파라미터를 FCM의 body 형태로 만들어주는 메서드 [단일 기기]
private String makeSingleMessage(FCMPushRequestDto request) throws JsonProcessingException {
FCMMessage fcmMessage = FCMMessage.builder()
.message(FCMMessage.Message.builder()
.token(request.getTargetToken()) // 1:1 전송 시 반드시 필요한 대상 토큰 설정
.notification(FCMMessage.Notification.builder()
.title(request.getTitle())
.body(request.getBody())
.image(request.getImage())
.build())
.build()
).validateOnly(false)
.build();
return objectMapper.writeValueAsString(fcmMessage);
}
// 요청 파라미터를 FCM의 body 형태로 만들어주는 메서드 [주제 구독]
private String makeTopicMessage(FCMPushRequestDto request) throws JsonProcessingException {
FCMMessage fcmMessage = FCMMessage.builder()
.message(FCMMessage.Message.builder()
.topic(topic) // 토픽 구독에서 반드시 필요한 설정 (token 지정 x)
.notification(FCMMessage.Notification.builder()
.title(request.getTitle())
.body(request.getBody())
.image(request.getImage())
.build())
.build()
).validateOnly(false)
.build();
return objectMapper.writeValueAsString(fcmMessage);
}
// 요청 파라미터를 FCM의 body 형태로 만들어주는 메서드 [다수 기기]
private static MulticastMessage makeMultipleMessage(FCMPushRequestDto request, List<String> tokenList) {
MulticastMessage message = MulticastMessage.builder()
.setNotification(Notification.builder()
.setTitle(request.getTitle())
.setBody(request.getBody())
.setImage(request.getImage())
.build())
.addAllTokens(tokenList)
.build();
log.info("message: {}", request.getTitle() +" "+ request.getBody());
return message;
}
// 실제 파이어베이스 서버로 푸시 메시지를 전송하는 메서드
private void sendPushMessage(String message) throws IOException {
OkHttpClient client = new OkHttpClient();
RequestBody requestBody = RequestBody.create(message, MediaType.get("application/json; charset=utf-8"));
Request httpRequest = new Request.Builder()
.url(FCM_API_URL)
.post(requestBody)
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + getAccessToken())
.addHeader(HttpHeaders.CONTENT_TYPE, "application/json; UTF-8")
.build();
Response response = client.newCall(httpRequest).execute();
log.info("단일 기기 알림 전송 성공 ! successCount: 1 messages were sent successfully");
log.info("알림 전송: {}", response.body().string());
}
// Firebase에서 Access Token 가져오기
private String getAccessToken() throws IOException {
GoogleCredentials googleCredentials = GoogleCredentials
.fromStream(new ClassPathResource(SERVICE_ACCOUNT_JSON).getInputStream())
.createScoped(List.of("https://www.googleapis.com/auth/cloud-platform"));
googleCredentials.refreshIfExpired();
log.info("getAccessToken() - googleCredentials: {} ", googleCredentials.getAccessToken().getTokenValue());
return googleCredentials.getAccessToken().getTokenValue();
}
}
```
⭐ 우리가 서비스에 위 로직을 적용하려면, ①특정 이벤트가 발생한 경우 또는 ②주기적으로 알림을 보낼 경우 등을 생각해볼 수 있겠죠!?
푸시알림을 이때 보내야겠다! 하는 그 시점에서 위 메서드를 호출하는 거라고 이해하시면 됩니다!
자 이제 파이어베이스에 보내는 부분까지 구현했으니, 테스트를 해볼까요?
[👨👩👧👦엄빠도 어렸다] 실제 적용 사례
SOPT 32기 앱잼 ‘엄빠도 어렸다’에서는 위 알림 템플릿을 Enum 으로 관리하여 적용했습니다!
public static FCMPushRequestDto sendTodayQna(String targetToken, String section, String topic) {
return FCMPushRequestDto.builder()
.type(MessageType.FIREBASE)
.targetToken(targetToken)
.title("📞" + section + PushMessage.TODAY_QNA.getTitle())
.body("'" + topic + PushMessage.TODAY_QNA.getBody())
.build();
}
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum PushMessage {
// 새로운 주제가 도착했을 때
TODAY_QNA("로부터 교신이 도착했어요",
"'에 대한 질문에 답변하고 추억을 나눠보세요 ☺️(수신거부 : 설정 - 푸시알림 off)"),
private String title;
private String body;
}
그래서 ! 우리는 간단한 테스트를 해볼 거니까 테스트용으로 만들어 보겠습니다
FCMPushRequestDto
클래스에 static 메서드를 정의해서 푸시알림을 보내는 시점에 바로 메시지가 들어간 객체로 메서드 호출을 할 수 있도록 합시다! (ENUM은 따로 구현하지 않겠습니다!)
public static FCMPushRequestDto sendTestPush(String targetToken) {
return FCMPushRequestDto.builder()
.targetToken(targetToken)
.title("💚DO SOPT SERVER💚")
.body("서팟 앱잼 화이팅! FCM 화이팅!")
.build();
}
API 호출을 통해 테스트하기 위해서, FCMController
클래스를 생성해줍시다!
@RestController
@RequestMapping("/alarm")
@RequiredArgsConstructor
public class FCMController {
private final FCMService fcmService;
/**
* 헤더와 바디를 직접 만들어 알림을 전송하는 테스트용 API (상대 답변 알람 전송에 사용)
*/
@PostMapping
@ResponseStatus(HttpStatus.OK)
public ResponseEntity<String> sendNotificationByToken(@RequestBody FCMPushRequestDto request) throws IOException {
fcmService.pushAlarm(request);
return ResponseEntity.ok().body("푸시알림 전송에 성공했습니다!");
}
}
또, 테스트용으로 만들어둔 유저 조회 API 호출 시마다 위 Push 메시지를 전송하는 코드를 추가해보겠습니다!
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
// 푸시알림 활성화가 필요한 로직이라면, FCMService를 주입!
private final FCMService fcmService;
public GetUserResponse getUserById(Long userId) {
User user = userRepository.findById(userId).orElseThrow(
() -> new EntityNotFoundException("존재하지 않는 회원의 아이디입니다.")
);
//== User 조회 API 호출 시 푸시 알림 전송! ==//
fcmService.pushAlarm(FCMPushRequestDto.sendTestPush(user.getFcmToken()));
return GetUserResponse.of(user);
}
}
PostMan에서 POST http://localhost:8080/alarm
로 테스트해보면, 아래와 같이 전송에 성공했다는 로그를 볼 수 있어요! 클라이언트 측에서 앱을 해당 파이어베이스 프로젝트에 등록한다면 실제 기기에서 테스트 결과를 확인할 수 있습니다 !!!
→ 이 부분은 FCM 토큰 값이 유효하지 않기 때문에(아직 유저가 앱을 설치할 수 없으니까요!) 발생하는 문제이고, 여기까지 나오면 서버 측에서는 성공입니다 😁
전체 실습코드는 제 깃헙에 올려뒀으니 참고해주실 분들은 참고해주세요 !!!
https://github.com/jun02160/fcm-practice.git
위 실습은 정말 일부분만 다루며 , 더 다양하고 좋은 레퍼런스들이 많아요..! 처음 접하시는 분들을 위해 최대한 빠르고 쉽게 구현할 수 있도록 준비한 자료이니 많은 도움 되셨으면 좋겠습니다 :>
궁금한 점, 오류사항 지적, 같이 토론하고 싶은 부분 모두 다 대 환 영 입니다 !!!!
아웅 야무져~