[Spring] 나만의 게시판 만들기 6 - FCM 설정

최진민·2022년 2월 25일
0

게시판 만들기

목록 보기
6/9
post-thumbnail

FCM 를 활용하여 댓글 알림 기능을 구현하고자 했습니다.

FCM ?


< 참고 > https://firebase.google.com/docs/cloud-messaging?hl=ko

  • 정의 : FCM (Firebase Cloud Messaging) : Firebase의 API를 활용하여 클라이언트에게 메시지를 전송합니다.
  • 주요 기능
    • 알림 또는 데이터 메시지 전송
      • 알림 메시지를 보내기도 하며, 애플리케이션 내에서 수행되는 비즈니스 로직을 거친 데이터를 전송할 수도 있습니다.
    • 다양한 메시지 Targeting
      • 3가지 방법으로 클라이언트에게 메시지를 전송할 수 있습니다.
        • 1) 단일 기기 (token) - 현재의 프로젝트에서 사용할 방법
          • 특정 기기 한 대(한 클라이언트)에 메시지를 전송
        • 2) 여러 기기 (tokens)
          • 여러 기기(여러 클라이언트)에 메시지를 전송
        • 3) 주제 구독 기기 (topic)
          • 주제를 선정해 해당 주제를 구독한 기기(클라이언트)들에게 전송
    • 클라이언트 앱에서 메시지 전송
      • 클라이언트의 기기에서 서버로 전송도 가능합니다.
  • 메시지 송수신을 위한 2가지 구성 요소
    • 메시지 작성, 타켓팅, 전송할 환경(앱 서버, Firebase용 Cloud Functions)
    • 메시지를 수신할 수 있는 앱(Android, IOS, 웹(js))

구현 (서버 사이드)

< 참고 > https://firebase.google.com/docs/cloud-messaging/server?hl=ko


프로젝트 생성 및 앱 등록

  • https://console.firebase.google.com/?hl=ko 에서 프로젝트를 생성하여 앱(IOS, Android, Web)을 등록할 수 있습니다.
  • 프로젝트를 생성한 후 프로젝트 설정서비스 계정 탭에서 새로운 비공개 키 생성으로 .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"
    }
    • private 관련된 내용은 숨김 필수!!

설정

< 참고 > https://firebase.google.com/docs/cloud-messaging/migrate-v1?hl=ko

Firebase 서비스에 대한 서버 요청을 승인하기 위해서 아래 방법들을 조합하여 사용할 수 있습니다.

  • Google 애플리케이션 기본 사용자 인증 정보(ADC)
  • 서비스 계정 JSON 파일
  • 서비스 계정에서 생성된 수명이 짧은 OAuth 2.0 액세스 토큰
  • FCM의 설정 파일입니다.
    @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)합니다.
    • 위의 설정 파일은 ADC를 사용한 사용자의 인증 정보를 제공하도록 합니다.
      • 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 객체를 생성하도록 클래스를 작성했습니다.
  • FCM을 사용하기 위한 서비스 클래스 (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();
        }
    }
    • 위의 코드에서는 tokenFirebase 서버를 사용합니다. (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()의 구현체는 FirebaseCloudMessageServicesendMessageTo()입니다.
profile
열심히 해보자9999

0개의 댓글