FCM 를 활용하여 댓글 알림 기능을 구현하고자 했습니다.
< 참고 > https://firebase.google.com/docs/cloud-messaging?hl=ko
token
) - 현재의 프로젝트에서 사용할 방법tokens
)topic
)< 참고 > https://firebase.google.com/docs/cloud-messaging/server?hl=ko
프로젝트 설정
→ 서비스 계정
탭에서 새로운 비공개 키 생성으로 .json
파일을 다운로드 할 수 있습니다.jinminboard-firebase-adminsdk-9ataf-770210e8eb.json
{
"type": "service_account",
"project_id": "jinminboard",
"private_key_id": "{private_key_id}",
"private_key": "{private_key}",
"client_email": "firebase-adminsdk-9ataf@jinminboard.iam.gserviceaccount.com",
"client_id": "108834495148772815532",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-9ataf%40jinminboard.iam.gserviceaccount.com"
}
< 참고 > https://firebase.google.com/docs/cloud-messaging/migrate-v1?hl=ko
Firebase 서비스에 대한 서버 요청을 승인하기 위해서 아래 방법들을 조합하여 사용할 수 있습니다.
- Google 애플리케이션 기본 사용자 인증 정보(ADC)
- 서비스 계정 JSON 파일
- 서비스 계정에서 생성된 수명이 짧은 OAuth 2.0 액세스 토큰
@Configuration
public class FirebaseConfig {
@Bean
public GoogleCredentials getGoogleCredentials() throws IOException {
return GoogleCredentials
.fromStream(new ClassPathResource("firebase/jinminboard-firebase-adminsdk-9ataf-770210e8eb.json").getInputStream())
.createScoped(Arrays.asList("https://www.googleapis.com/auth/cloud-platform"));
}
@Bean
public FirebaseApp firebaseApp() throws IOException {
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(getGoogleCredentials())
.build();
return FirebaseApp.initializeApp(options);
}
//비동기 통신을 위함
@Bean
public ListeningExecutorService firebaseAppExecutor() {
return MoreExecutors.newDirectExecutorService();
}
@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient();
}
}
@Configuration
: 클래스를 설정 파일로 저장(in Bean Factory
)합니다.getGoogleCredentials()
: Google의 인증 라이브러리와 Firebase 사용자 인증 정보를 사용하여 인증 정보를 반환하도록 합니다.firebaseApp()
: 반환된 Google의 인증 정보를 통해 Firebase의 설정을 초기화 합니다.@Bean
: 메서드를 빈 타입으로 팩토리에 정의합니다. (싱글톤)< 참고 > https://firebase.google.com/docs/cloud-messaging/send-message?hl=ko
메시지를 다음과 같은 타겟 유형으로 전송할 수 있습니다.
- 주제 이름
- 조건
- 기기 등록 토큰
- 기기 그룹 이름(기존 프로토콜 및 Node.js용 Firebase Admin SDK만 해당)
FcmMessage
@Getter
public class FcmMessage {
@Key("validate_only")
@JsonIgnore
private boolean validateOnly;
// Message 에 해당하는 데이터는 많지만,
// 사용할 데이터는 notification(Notification), token(String) 뿐
// Notification => title(String), body(String)
@Key("message")
private Message message;
@Builder
public FcmMessage(boolean validateOnly, Message message) {
this.validateOnly = validateOnly;
this.message = message;
}
}
Message
클래스에는 수많은 데이터가 담겨 있지만, 필요한 데이터만 사용하기 위해 FcmMessage
객체를 생성하도록 클래스를 작성했습니다.FirebaseCloudMessageService
)@Slf4j
@Service
@RequiredArgsConstructor
public class FirebaseCloudMessageService {
private static final String API_URI = "https://fcm.googleapis.com/v1/projects/jinminboard/messages:send";
private final GoogleCredentials googleCredentials;
private final OkHttpClient client;
private final ObjectMapper objectMapper;
/**
* `targetToken`에 해당하는 기기로 푸시 알림 전송 요청
* (targetToken 은 프론트 사이드에서 얻기!)
*/
public void sendMessageTo(String targetToken, String title, String body) throws FirebaseMessagingException, IOException, ExecutionException, InterruptedException {
//Request + Response 사용
/*RequestBody requestBody = getRequestBody(makeFcmMessage(targetToken, title, body));
Request request = getRequest(requestBody);
Response response = getResponse(request);
log.info(response.body().string());*/
//FirebaseMessaging 사용
Message message = makeMessage(targetToken, title, body);
String response = FirebaseMessaging.getInstance().send(message);
log.info(response);
//비동기
String asyncMessage = FirebaseMessaging.getInstance().sendAsync(message).get();
}
private Response getResponse(Request request) throws IOException {
return client.newCall(request).execute();
}
private Request getRequest(RequestBody requestBody) {
return new Request.Builder()
.url(API_URI)
.post(requestBody)
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + googleCredentials.getAccessToken().getTokenValue())
.addHeader(HttpHeaders.CONTENT_TYPE, "application/json; UTF-8")
.build();
}
private RequestBody getRequestBody(String message) {
return RequestBody.create(message, MediaType.get("application/json; charset=utf-8"));
}
private String makeFcmMessage(String targetToken, String title, String body) throws JsonProcessingException {
FcmMessage fcmMessage = FcmMessage.builder()
.message(Message.builder()
.setToken(targetToken)
.setNotification(new Notification(title, body))
.build())
.validateOnly(false)
.build();
//log.info("message 변환(Object -> String) \n" + objectMapper.writeValueAsString(fcmMessage));
return objectMapper.writeValueAsString(fcmMessage);
}
private Message makeMessage(String targetToken, String title, String body) {
FcmMessage fcmMessage = FcmMessage.builder()
.message(Message.builder()
.setToken(targetToken)
.setNotification(new Notification(title, body))
.build())
.validateOnly(false)
.build();
return fcmMessage.getMessage();
}
}
token
과 Firebase 서버
를 사용합니다. (Request, Response X)sendMessageTo(token, title, body)
: Token(기기, 클라이언트)에게 메시지를 전송하는 메서드입니다.makeMessage(token, title, body)
: 메시지를 생성합니다.FcmMessage
객체와 Builder
를 활용하여 토큰과 알림 내용을 설정하여 생성합니다.CommentWriteService
@Slf4j
@Service
@RequiredArgsConstructor
public class CommentWriteService {
private final CommentRepository commentRepository;
private final UserFindService userFindService;
private final BoardFindService boardFindService;
private final FirebaseCloudMessageService messageService;
@Transactional
public Long writeComment(Long userId, Long boardId, CommentWriteRequest commentWriteRequest) throws FirebaseMessagingException, IOException, ExecutionException, InterruptedException {
Board board = boardFindService.findById(boardId);
User user = userFindService.findById(userId);
Comment comment = Comment.builder()
.content(commentWriteRequest.getContent())
.writer(user.getName())
.board(board)
.build();
Comment savedComment = commentRepository.save(comment);
user.writeComment(savedComment);
String targetToken = board.getUser().getDeviceToken();
sendMessageToBoardWriter(targetToken, "Comment Notification!", comment.getWriter(), comment.getContent());
return savedComment.getComment_id();
}
private void sendMessageToBoardWriter(String targetToken, String title, String writer, String content) throws FirebaseMessagingException, IOException, ExecutionException, InterruptedException {
messageService.sendMessageTo(targetToken, title, "[" + writer + "]" + "가 댓글 : <" + content + ">을 작성했습니다.");
}
}
sendMessageToBoardWriter()
의 구현체는 FirebaseCloudMessageService
의 sendMessageTo()
입니다.