[SpringBoot] FCM을 이용해 Push API 만들기

둥그냥·2022년 1월 21일
6

STOVE DEV CAMP

목록 보기
4/5

용어 정리

Notification Server

  • mobile 기기에 Push 알림을 전송하는 서버
  • FCM(Firebase Cloud Messaging) 사용 예정

Client App

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

Provider

  • Client App을 위한 서버
  • 필요시 Notification Server에 요청을 전송하여, Client App에 알림을 보냄
  • 나의 push server

FCM 푸시 대상 종류

단일 (token)

  • 특정 기기의 Token 값을 이용하여 해당 기기에만 알림을 전송

여러명 (topic)

  • 특정 topic을 구독한 여러 기기에 메시지를 보내는 방법
    - 예시) 날씨와 같이 공개적으로 제공되는 정보

  • 클라이언트인 앱에서 기존 topic을 구독하거나 새 topic을 만들 수 있음
    - Firebase 프로젝트에 토픽 구독

    • 없는 토픽을 구독 요청할 경우 새 topic이 만들어 짐
  • Firebase Admin SDK를 사용하면 서버 측에서 기본적인 topic 관리 작업을 수행할 수 있음

  • 등록 토큰을 알고 있으면 서버 로직을 사용하여 클라이언트 앱 인스턴스를 일괄 구독하거나 구독 취소할 수 있음

  • 속도보다 처리량을 위주로 최적화되어 있는 기능

  • 빠르고 안전하게 전송하고 싶다면 다른 방법을 사용하라고 권고됨

  • 단일 요청으로 기기를 최대 1,000대까지 구독하거나 구독 취소할 수 있음

여러명 (기기그룹)

  • 그룹에 속한 기기에만 알림을 보내는 방법입니다.

  • 기기 그룹 메시징은 앱 내에서 관리하는 것이 아니라 서버에서 기기 그룹을 관리함

  • 예를 들어, 기기 모델에 따라서 다른 메시지를 보내려면 서버에서 알맞은 그룹에 등록/삭제하여 각 그룹에 적절한 메시지를 보냄

  • 하지만 알림키 하나 그룹에 최대 20명까지만 속할 수 있음


FCM의 전송방식

  1. TOKEN 방식
    1. 첫 APP을 시작하면 기기마다 고유의 TOKEN을 Firebase Cloud에서 할당받음
    2. 기기의 Token값으로 FCM에 PUSH 요청을 함
    3. FireBase에서 해당 Token에 해당하는 기기로 알람을 보내줌
  2. TOPIC 방식
    1. 모바일에서 원하는 주제(TOPIC)를 FireBase에 등록
      • ex) 카카오톡 채팅방
    2. 서버에서 FireBase에게 해당 주제(topic)에 해당되는 푸시를 전송함
    3. FireBase 에서는 해당 Topic을 등록한 기기들에게 알람을 보내줌

즉 기기의 Token or Topic 관리를 해야 함
클라이언트 -> Token or Topic 값을 계속 등록 및 갱신해 줘야 함
서버 -> 해당 사용자의 Token or Topic을 저장해야 함


Spring Boot에서 push notification 사용하기

  1. FCM(Firebase Cloud Messaging)에 스마트폰 application과 백엔드 서버를 등록
  1. Firebase로부터 AccessToken을 가져온 뒤, 그것을 RestAPI를 이용해 FCM에 Push 요청을 보낼때, Header에 설정해야 함
public class FCMService {
    private String getAccessToken() throws IOException {
        String firebaseConfigPath = "firebase/cocotalk_firebase_service_key.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();
    }
}
  1. FCM에 요청할 DTO를 만든다.
@Builder
@AllArgsConstructor
@Getter
public class FCMMessage {
    private boolean validate_only;
    private Message message;

    @Builder
    @AllArgsConstructor
    @Getter
    public static class Message {
        private Notification notification; // 모든 mobile os를 아우를수 있는 Notification
        private String token; // 특정 device에 알림을 보내기위해 사용
    }

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

}
  1. FCMService에 메시지를 수신하는 함수를 만든다.
@Service
@RequiredArgsConstructor
@Slf4j
public class FCMService {

    private 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();

        log.info(response.body().string());
    }

    // 파라미터를 FCM이 요구하는 body 형태로 만들어준다.
    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 {...}
    
}
  1. controller 기쁘게 사용한다!

DEVICE TOKEN 관리

  • 앱을 Firebase와 동기화했다면 기기들은 각각의 고유한 Instance ID(Instance ID가 FCM Token을 발행)
  • Firebase 서버는 FCM Token을 통해 각각의 기기에 Push Notification을 보낼 수 있다.

FCM TOKEN이 갱신되는 시점

  • 앱이 인스턴스 ID를 삭제

  • 앱이 새 기기에서 복원

  • 사용자가 앱을 제거 및 재설치

  • 사용자가 앱 데이터를 지움

  • TOKEN 생성 모니터링
    onTokenRefreshcallback은 새 토큰이 생성 될 때마다 발생하므로 컨텍스트에서 getToken을 호출하면 현재 사용 가능한 토큰이 등록되어있는지 확인할 수 있다.

    @Override
    public void onTokenRefresh() {
       // Get updated InstanceID token.
       String refreshedToken = FirebaseInstanceId.getInstance().getToken();
       Log.d(TAG, "Refreshed token: " + refreshedToken);
    
       // If you want to send messages to this application instance or
       // manage this apps subscriptions on the server side, send the
       // Instance ID token to your app server.
       sendRegistrationToServer(refreshedToken);
    }
    

    입장 로직

  • [Cleint] Backend Server에 자신이 특정 채팅방에 입장하겠다는 요청을 보냅니다.

  • [Server]
    1. [채팅 서버] 해당되는 채팅방 그룹에, 해당 사용자의 id를 등록해둡니다.
    2. [푸시 서버] Device Table에서 해당 Client의 FCM Device Token을 가져옵니다.
    3. [푸시 서버] 이 token을 특정 Messaging Group(동창모임)에 추가해달라는 요청을 FCM에 보냅니다.

    채팅 푸시 로직

    룸 id -> token list 찾기 -> 디바이스 그룹

서버에서 토큰을 갱신하는 시점

    • 로그인 시에
    • 앱 실행 시에
    • 로그인 시에
    • 웹 접속 시에
  • 서버에서 토큰을 삭제하는 시점
  • 푸시 요청 시 토큰이 만료되어 에러 메시지가 오는 경우 삭제

본문 외 기타 참고

FCM TOKEN 설명
FCM을 도입할 때 고려할 것들
push notification
FCM 푸시 알림 대상

0개의 댓글