[밍글] Spring Boot + FCM 을 이용한 푸시알림 로직 개선기

KIM TAEHYUN·2023년 6월 6일
0
post-thumbnail

ㄴ> 밍글 알림 예시

커뮤니티 앱에서 필수적인 기능인 푸시알림을 Firebase Cloud Messaging을 이용해 구현해보았다.

하지만 댓글 작성 시 푸시알림을 보낼 때 알림이 중복되어 보내진다는 문제가 있었다.

알림을 보낼 경우는 1. 댓글 작성자에 의해 댓글이 작성될 때,
2. 게시물 작성자에게, 대댓글이라면 3. 부모 댓글 작성자와 4. 멘션 된 유저에게까지 알림을 보내야한다. 또한 자기 자신에게 답글을 다는 경우도 있을 것이다.

만약 이 4명의 사용자가 각기 다 다른 사용자라면 문제가 없겠지만, 만일 겹치는 사용자가 있다면 그 사용자에게는 알림이 중복되어 갈 것이다.

그래서 댓글이 작성될 때, 게시물 작성자, (대댓글일 시)부모 댓글 작성자, 멘션 된 댓글 작성자, 그리고 댓글은 다는 작성자를 모두 고려해 겹치는 유저가 있다면 그 유저에게는 푸시알림을 한번만 보내야한다.

이에 대한 경우의 수는 총 12가지로 이 모든 경우를 if문으로 구현하기엔 너무 비효율적이고 코드 가독성이 떨어질거라 판단해 고민한 결과, HashMap 자료구조를 활용해 문제를 해결할 수 있었다.


댓글 알림을 보낼 때 고려해야하는 유저들

  • 게시물 작성자
  • 부모 댓글 작성자
    • null 이 될 수 있음 (대댓글이 아닌 댓글을 작성했을 시)
  • 멘션 된 유저
    • 현재 댓글 또는 대댓글 작성자에 의해 멘션 된 유저
    • 부모 댓글 작성자가 null일 때, 이 값은 무조건 null이 됨
  • 현재 댓글 또는 대댓글 작성자
    • 새로운 댓글 또는 대댓글을 작성한 유저

알람을 보낼 유저들 조합에 대한 경우의 수

  • Case 1: 부모 댓글 작성자가 null이고 게시물 작성자 ≠ 현재 댓글 작성자 일 경우
    • 게시물 작성자에게만 알림을 보내면 됨
  • Case 2: 게시물 작성자, 부모 댓글 작성자, 멘션 된 유저 중 2명 이상이 같을 경우
    • 같은 유저에게 2번의 알림을 보내는 경우를 제외해야함
  • Case 3: 게시물 작성자, 부모 댓글 작성자, 멘션 된 유저 중 1명 이상이 현재 댓글 또는 대댓글 작성자와 같을 경우
    • 댓글을 작성한 사람 자신이 알림을 받는 경우를 제외해야함

이제 문제점을 살펴보았으니, 알림을 보내는 코드를 살펴보도록 하겠습니다.

- CommentService

💡 알림을 보내는 로직이 필요한 API (e.g. 댓글 작성 API) 에서 sendMessageTo 메소드를 호출해 알림을 보낼 수 있다.
public void sendTotalPush(TotalPost post, PostTotalCommentRequest postTotalCommentRequest, Member creatorMember) throws IOException {
    Member postMember = post.getMember();
    TotalComment parentTotalComment = commentRepository.findTotalCommentById(postTotalCommentRequest.getParentCommentId());
    TotalComment mentionTotalComment = commentRepository.findTotalCommentById(postTotalCommentRequest.getMentionId());

    String messageTitle = "알림 제목";

		**// [1]. 댓글일 경우** 
    if (parentTotalComment == null) {
        if (postMember.getId() == creatorMember.getId()) {
            return;
        } else {
            firebaseCloudMessageService.sendMessageTo(postMember.getFcmToken(), messageTitle, "새로운 댓글이 달렸어요" + postTotalCommentRequest.getContent());
        }
		**// [2] 대댓글일 경우** 
    } else if (parentTotalComment != null) {
        Member parentMember = parentTotalComment.getMember();
        Member mentionMember = mentionTotalComment.getMember();
        Map<Member, String> map = new HashMap<>();
        map.put(postMember, "postMemberId");
        map.put(parentMember, "parentMemberId");
        map.put(mentionMember, "mentionMemberId");
        map.put(creatorMember, "creatorMemberId"); //현재 댓글 작성자와 겹치는지 확인 용도 
        map.remove(creatorMember); //겹치지 않는다면 알림을 보내지 않아야하기에 remove해준다. 
        for (Member member : map.keySet()) { 
            firebaseCloudMessageService.sendMessageTo(member.getFcmToken(), messageTitle, postTotalCommentRequest.getContent());
        }
    }

경우의 수를 고려한 아래 Service 코드 로직

  • 주석 [1]: 댓글일 경우
    • Case 1과 Case 3을 고려해줌
    • 이 경우, 부모 댓글 작성자와 멘션 된 유저가 null
    • 게시물 작성자 == 현재 댓글 작성자일 경우 알림을 보내지 않음 (Case 3)
    • 게시물 작성자 ≠ 현재 댓글 작성자일 경우 게시물 작성자에게만 알림을 보냄 : Case 1 해결
  • 주석 [2] 대댓글일 경우
    • Case 2와 Case 3을 고려해줌 (총 12개의 경우의 수)
    • HashMap의 경우 중복된 key를 허용하지 않고 중복될 경우 마지막에 추가된 <key, value>로 overwrite된다는 점 사용
    • 게시물 작성자, 부모 댓글 작성자, 멘션 된 유저, 현재 댓글 작성자 순서로 HashMap에 추가
    • 게시물 작성자, 부모 댓글 작성자, 멘션 된 유저, 현재 댓글 작성자 중 중복된 유저가 있었을 경우 하나의 유저만 남음 : Case 2 해결
    • 현재 댓글 작성자와 겹치는 유저는 HashMap에서 삭제됨 → 현재 댓글 작성자 제외하고 알림을 보낼 수 있음 : Case 3 해결

- FirebaseCloudMessageService

💡 “sendMessageTo” 메소드로 FCM으로 메시지를 보내주는 클래스이다.
@Component
@RequiredArgsConstructor
public class FirebaseCloudMessageService {

    private final String API_URL = "https://fcm.googleapis.com/v1/projects/***/messages:send";
    private final ObjectMapper objectMapper;

		**/**
     * This method send the message to firebase server so that it could deliver it to target user's device.
     * @param targetToken the token that indicate the target device to send notification
     * @param title the title of notification
     * @param body the body of notification
     * @throws IOException
     */**
    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("AUTHORIZATION", "Bearer " + getAcccessToken())
                .addHeader("CONTENT_TYPE", "application/json; UTF-8")
                .build();
        Response response = client.newCall(request).execute();
        System.out.println(response.body().string());
    }

		**/**
     * This method builds title and body into FCM required message format
     * @param targetToken
     * @param title
     * @param body
     * @return String of formated message
     * @throws com.fasterxml.jackson.core.JsonProcessingException
     */**
    private String makeMessage(String targetToken, String title, String body) throws com.fasterxml.jackson.core.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 getAcccessToken() 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();
    }
}

- FcmMessage

💡 FCM 이 필요로 하는 메시지의 format이다.
@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;
    }
}

0개의 댓글