[Capstone] FCM을 통해 Push 알림 서버 구축하기

홍정완·2022년 7월 5일
1

Capstone Project

목록 보기
2/7
post-thumbnail

Spring BootFirebase FCM을 이용해, PushNotification Server 구축을 해보자.
현재 Front는 Android 측에서 구현이 되어있기 때문에, 이번 포스팅에서는 BackEnd 구축만 다루겠다.




Push Notification 동작


  • 유저의 device에 push 알림을 보내기 위한 구현은 모바일 os마다 다르겠지만 큰 틀은 비슷하다.

  • 우선 용어를 정리하자



용어

  • Notification Server

    • Mobile 기기에 Push 알림을 전송하는 서버

  • Client App

    • 사용자 Mobile 기기에 설치된 app
    • Push 알림을 받는 역할

  • Provider

    • Client App을 위한 서버
    • Notification Server에 요청 전송
      • Client App에 알림을 보낸다



과정

  • Client App을 Notification Server에 등록

  • Client App을 켜면 각각의 Client App을 구분하는 Token을 Notification Server에서 발급

  • Client App에서 Token을 Provider로 전송

  • Provider는 Token 저장

  • Client App에 알림 전송 필요시, 토큰 값과 함께 Notification Server에 요청

  • Client App에서 Push 알림 수신



빠른 구축을 위해 Token을 저장하는 과정을 생략하고, Front(Android)에서 생성된 토큰 값을 console에 출력하여,
그 토큰을 바로 사용하겠다.




Firebase AppServe 인증 및 개발 환경 준비


  • Firebase 프로젝트 개요 (톱니바퀴 클릭) 👉 서비스 계정 👉 새 비공개 키 생성 클릭

    • 비공개 키를 다운 받는다.



  • 프로젝트 resources/firebase/ 하위에 비공개 키를 넣자.



의존성 추가 (maven)

	<!--firebase sdk-->
        <dependency>
            <groupId>com.google.firebase</groupId>
            <artifactId>firebase-admin</artifactId>
            <version>8.1.0</version>
        </dependency>

        <!--okhttp-->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.9.0</version>
        </dependency>
  • pom.xml 파일에 Firebase sdk, okhttp 의존성을 추가하자.



구현


FirebaseCloudMessageService

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.net.HttpHeaders;
import lombok.RequiredArgsConstructor;
import okhttp3.*;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.List;

@Component
@RequiredArgsConstructor
public class FirebaseCloudMessageService {

    private final String API_URL = "https://fcm.googleapis.com/v1/projects/본인 프로젝트 ID/messages:send";
    private final ObjectMapper objectMapper;

    public void sendMessageTo(String targetToken, String title, String body) throws IOException {
        String message = makeMessage(targetToken, title, body);

        OkHttpClient client = new OkHttpClient();
        RequestBody requestBody = RequestBody.create(message, MediaType.get("application/json; charset=utf-8"));
        Request request = new Request.Builder()
                .url(API_URL)
                .post(requestBody)
                .addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + getAccessToken())
                .addHeader(HttpHeaders.CONTENT_TYPE, "application/json; UTF-8")
                .build();

        Response response = client.newCall(request)
                .execute();

        System.out.println(response.body().string());
    }

    private String makeMessage(String targetToken, String title, String body) throws JsonProcessingException {
        FcmMessage fcmMessage = FcmMessage.builder()
                .message(FcmMessage.Message.builder()
                        .token(targetToken)
                        .notification(FcmMessage.Notification.builder()
                                .title(title)
                                .body(body)
                                .image(null)
                                .build()
                        )
                        .build()
                )
                .validate_only(false)
                .build();

        return objectMapper.writeValueAsString(fcmMessage);
    }

    private String getAccessToken() throws IOException {
        String firebaseConfigPath = "/firebase/다운 받은 비공개키.json";

        GoogleCredentials googleCredentials = GoogleCredentials
                .fromStream(new ClassPathResource(firebaseConfigPath).getInputStream())
                .createScoped(List.of("https://www.googleapis.com/auth/cloud-platform"));

        googleCredentials.refreshIfExpired();
        return googleCredentials.getAccessToken().getTokenValue();
    }

}

AcessToken 발급


GoogleCredentials

  • GoogleApi를 사용하기 위해서 oauth2를 이용해 인증한 대상을 나타내는 객체

  • GoogleCredentials의 static 메서드인 fromStream()

    • 앞서 다운받은 비공개 키.json의 inputStream을 넣어주면 인스턴스를 얻을 수 있다.

권한 지정

  • 이는 다시 반환된 GoogleCredentials 인스턴스의 createScoped를 통해 설정


googleCredentials.refreshIfExpired();
return googleCredentials.getAccessToken().getTokenValue();

권한 부여된 Oauth2 AccessToken 받기

  • refreshIfExpired() 메서드 호출 👉 accessToken 생성
    👉 googleCredentials.getAccessToken().getTokenValue() 👉 토큰 값 받기

  • 받은 AccessToken은 RestAPI FCM Push 요청 시 사용

    • Header에 설정하여 인증을 사용



HTTP POST Request


sendMessageTo()

  • 매개변수로 전달받은 targetToken에 해당되는 device로 FCM push 전송 요청

    • targetToken은 front 측에서 받아온다.

makeMessage()

  • FcmMessage 구현 후, ObjectMapper를 이용해 String 변환 후 반환



알림 요청 메시지 - FcmMessage

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@Builder
@AllArgsConstructor
@Getter
public class FcmMessage {
    private boolean validate_only;
    private Message message;

    @Builder
    @AllArgsConstructor
    @Getter
    public static class Message {
        private Notification notification;
        private String token;
    }

    @Builder
    @AllArgsConstructor
    @Getter
    public static class Notification {
        private String title;
        private String body;
        private String image;
    }

}



RequestDTO

import lombok.AllArgsConstructor;
import lombok.Data;

@AllArgsConstructor
@Data
public class RequestDTO {
    private String title;
    private String body;
    private String targetToken;
}



FcmController

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
@RequiredArgsConstructor
public class FcmController {

    private final FirebaseCloudMessageService firebaseCloudMessageService;

    @PostMapping("/api/fcm")
    public ResponseEntity pushMessage(@RequestBody RequestDTO requestDTO) throws IOException {
        System.out.println(requestDTO.getTargetToken() + " "
                + requestDTO.getTitle() + " " + requestDTO.getBody());

        firebaseCloudMessageService.sendMessageTo(
                requestDTO.getTargetToken(),
                requestDTO.getTitle(),
                requestDTO.getBody());
        return ResponseEntity.ok().build();
    }

}



결과


  • targetToken, title, body를 작성해서 firebase 서버에 전송

    • App 실행 시, Log에 남겨진 디바이스 토큰 사용
  • 서버는 firebase 서버에 메시지 푸시 요청

  • 토큰 확인 시, firebase 서버는 클라이언트(Android)에게 메시지 푸시



정상 동작!

profile
습관이 전부다.

1개의 댓글

comment-user-thumbnail
2023년 6월 8일

https://firebase.google.com/docs/admin/setup?hl=ko
admin-sdk사용하는 것과 이렇게 직접 api 호출중 뭐가 나은지 혹시 알고 계신가요? 구글문서에서는 admin sdk만 있는 것 같더라구요

답글 달기