스프링- 푸시 알림 구현예제(FCM 사용)

이진우·2023년 8월 15일
1

스프링 학습

목록 보기
10/41
post-thumbnail

사전 지식 얻기

푸시 알림 구현할 때 서버가 필요한가?

나는 푸시 알림 구현을 공부하기 전에는 왜 필요한지 이해하지 못했다.
아니 내가 8시마다 똑같은 공지를 받는 것을 서버가 뭘 해줘야 해?
그냥 클라이언트 분들이 알아서 8시 되면 공지를 띄우면 안되나?

그렇게 할 수도 있다. 하지만 클라이언트에서만 관리한다면
1)공지 내용 변경
2)사용자 별 맞춤으로 공지 해 주기
3)시간 동기화 문제

가 발생한다. 여기서 시간 동기화 문제란 핸드폰의 시간을 임의적으로 조작해서 공지를 자기만의 시간에 받게 하거나 할 수 도 있다는 말이다. 즉 서버를 통해서 공지를 만드는 게 확장성과 시간 동기화 문제로부터 보다 자유롭다.

그렇다면 스프링에서 직접 구현 하면 안되나

당연히 해도 된다. 그렇지만 직접 만드는 것보다 구글에서 지원하는 메시징 서비스를 사용하는 것이 훨씬 편리하다.

용어 및 그림

FCM:Firebase Cloud Messaging 의 줄임말이다.


출처:https://maejing.tistory.com/entry/Android-FCM%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-Push-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

위에서는 안드로이드에서 어떻게 코드를 짜야하는지 알 수 있었다.

1) 먼저 저기서 얘기하는 토큰이란 Client App을 켜면 각각의 Client App을 구분하는 Token 을 의미한다. 어떤 디바이스에 정보를 보낼지 구분하는 토큰이라고 한다. 클라이언트는 그 토큰을 Firebase 로부터 발급받아서

2) 클라이언트는 그 토큰을 서버에 DTO를 통하던가 어떻게든 전달을 한다.

3)서버는 DB에 저장을 하든 Redis를 통해 저장을 하든 Member에 칼럼을 두든 어떻게든 저장을 한다.

4)메시지를 전달해야 하는 상황이 오면 서버는 Token을 이용해 Message를 만들어 Firebase에 전달한다.

5)그럼 이게 정상 Token인지 Firebase가 판단한 후 맞으면 모바일 디바이스에 메시지를 전달하는 식이다.

FCM과 서버가 상호작용하는 옵션

아래는 공식 문서의 일부분을 발췌한 것이다.

1)Admin FCM API는 백엔드 인증을 처리하고 메시지 보내기와 주제 구독 관리를 지원합니다. Firebase Admin SDK를 사용하면 다음을 수행할 수 있습니다.

개별 기기에 메시지 보내기
주제 및 하나 이상의 일치하는 조건문에 메시지 보내기
기기에서 주제 구독 및 구독 취소
다양한 타겟 플랫폼에 맞는 메시지 페이로드 구성

2)FCM 서버 프로토콜
현재 FCM은 다음과 같은 원시 서버 프로토콜을 제공합니다.

FCM HTTP v1 API
기존 HTTP 프로토콜
기존 XMPP 프로토콜
앱 서버는 이러한 프로토콜을 별도로 사용하거나 동시에 사용할 수 있습니다. 가능하면 여러 플랫폼에 메시지를 보낼 수 있는 유연한 최신 프로토콜인 FCM HTTP v1 API를 사용하는 것이 좋습니다. 기기에서 서버로 업스트림 메시지를 보낼 필요가 있는 경우에는 XMPP 프로토콜을 구현해야 합니다.

공식 문서

https://firebase.google.com/docs/cloud-messaging?hl=ko
공식 문서는 위 사이트이다. 왜 토큰을 DB에 저장해야 하는지, 관리는 어떻게 하면 좋은지 각각의 언어마다 어떻게 코드를 작성해야 하는지 알려준다.

세팅

FCM을 활용하기 때문에 즉 Firebase기능을 사용하기 때문에 안드로이드 및 클라이언트와 백앤드 모두 세팅을 해주어야 한다. 아래 글들은 백앤드를 위주로 썼다.

1)https://firebase.google.com/?hl=ko 에 접속한다. 그럼 아래와 같은 화면이 나온다.

2)시작하기 버튼을 누르면 아래와 같은 화면이 나온다.

3)프로젝트 추가를 누른다.

프로젝트 이름을 이렇게 세팅해 주었다.

4)계속 누르다가 아래 화면에서

이렇게 설정하고 프로젝트 만들기를 눌러준다.

5)잠깐의 시간을 기다린 후 아래 화면을 본다.

6)프론트 쪽을 불러서 저 버튼을 누르게 시킨다.

7)저 위에 프론트를 위한 사이트를 보고 안드로이드 세팅을 완료한다.

8)백앤드는

위 사진에서 프로젝트 설정을 누르고

9)이 화면으로 간다.

이걸 누르면 JSON 형식으로 파일이 나오는데 이 파일을 잘 보관해 둔다.

코드 구현

일단 아까 받은 JSON 형식의 데이터를 먼저 처리하자.

이렇게 Resources 하위에 firebase_service_key.json 을 추가한 후에 아까 받은 JSON 을 통째로 붙여 넣기한다.


이렇게 말이다.

또한 build.gradle에

implementation 'com.google.firebase:firebase-admin:6.8.1'
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.2.2'

okhttp는 요청을 보내기 편리하기 위해서 추가한다. 우리는 역으로 클라이언트의 입장이 되어서 Firebase에 요청을 보내야 하기 때문이다.

Dto

FcmMessage

일단 Dto라기 애매한 감이 많이 있지만 그냥 일단 FcmMessage를 Dto패키지 안에 꾸겨 넣었다.


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

@Builder
@AllArgsConstructor
@Getter
public class FcmMessage {
    private boolean validateOnly;
    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;
    }
}

위 코드는 Fcm에 메시지를 보내는 뼈대가 되는 구조이다.
image는 선택사항이고
validateOnly는 boolean 타입으로, 메시지를 실제로 보내는 대신 유효성 검사만 실행하려면 true로 설정한다. 따라서 false로 고정하는 경우가 많은듯 하다.

RequestDto
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class RequestDTO {
    private String targetToken;
    private String title;
    private String body;
}

순전히 테스트 용도이므로 이렇게 DTO를 작성한다. 프론트엔드 분들이 디바이스 토큰과 title 과 body를 넘겨 주는 걸로 되어 있는데 실제로는 미리 토큰을 Redis나 DB나 컬럼에 저장을 해두는 것이 좋다. 또한 title 과 body또한 고정되어 있다면 스프링 코드에 직접 작성해도 된다. 단지 테스트 용도이므로 토큰과 title,body를 작성해두도록 한다.

Service

FirebaseCloudMessageService
@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 JsonParseException, JsonProcessingException {
        FcmMessage fcmMessage = FcmMessage.builder()
                .message(FcmMessage.Message.builder()
                        .token(targetToken)
                        .notification(FcmMessage.Notification.builder()
                                .title(title)
                                .body(body)
                                .image(null)
                                .build()
                        ).build()).validateOnly(false).build();

        return objectMapper.writeValueAsString(fcmMessage);
    }

    private String getAccessToken() throws IOException {
        String firebaseConfigPath = "firebase/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)API_URL 부분:

여기서 프로젝트 ID 부분을 고대로 붙여 넣기만 해주면 된다.

2)ObjectMapper: 우리는 아까 만들었던 FcmMessage를 Json으로 바꾸기 위해서 추가 했다.

3)sendMessage메서드 : 토큰과 title과 body를 이용 만들어진 메서드를 이용해서 Firebase에 request를 보내는 작업이다.

4)makeMessage 메서드: 아까 우리가 만든 FcmMessage를 이용해서 Json형식으로 바꾸어 준다. 당연하지만 title,body에 직접 입력해도 되고 ,validate는 실제로 메시지를 보낼 거기 때문에 false 로 고정한다.

5)getAccessToken (): 우리는 파이어베이스가 우리는 정당히 메시지를 보낼 권리가 있다는것을 알릴 필요가 있다. 여기서 토큰은 디바이스 토큰이 아니다.

Controller

MainController
import com.FCM.notification.dto.RequestDTO;
import com.FCM.notification.service.FirebaseCloudMessageService;
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 MainController {

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

PostMan을 통한 테스트


이것은 안드로이드 환경에서 볼 수 있는 token이다. 이러한 token을 서버에 주면 그것은 디바이스 토큰을 식별할 수 있는 토큰이다.

원래 서버에서 디바이스 토큰을 저장하고 이용하지만 이것은 테스트 용도이므로 postman을 통해서 title과 body,그리고 토큰을 함께 주도록 하겠다.

이렇게 보내보면

FCM을 통해 안드로이드 스튜디오에

무사히 도착하는 것을 볼 수 있다.

profile
기록을 통해 실력을 쌓아가자

0개의 댓글