Skip to content

Commit

Permalink
FCM 알림 서비스 SQS + Lambda 리팩토링 (#238)
Browse files Browse the repository at this point in the history
* feature: SQS + lambda 사용하여 Fcm 알림 구현

* sqsProperties 추가

* fix: test코드 SqsAutoConfiguration Exclude처리

* fix: FcmMessage record로 수정 및 createBatches 수정
  • Loading branch information
dbscks97 authored Aug 28, 2024
1 parent 57157b2 commit 368b82b
Show file tree
Hide file tree
Showing 17 changed files with 181 additions and 179 deletions.
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ dependencies {
// FCM
implementation 'com.google.firebase:firebase-admin:9.3.0'
implementation 'com.fasterxml.jackson.core:jackson-core:2.16.1'

// SQS
implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1")
implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs'
implementation 'software.amazon.awssdk:sqs'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
package com.depromeet.stonebed.domain.fcm.api;

import com.depromeet.stonebed.domain.fcm.application.FcmNotificationService;
import com.depromeet.stonebed.domain.fcm.application.FcmService;
import com.depromeet.stonebed.domain.fcm.application.FcmTokenService;
import com.depromeet.stonebed.domain.fcm.dto.request.FcmSendRequest;
import com.depromeet.stonebed.domain.fcm.dto.request.FcmTokenRequest;
import com.depromeet.stonebed.domain.fcm.dto.response.FcmNotificationResponse;
import com.depromeet.stonebed.global.util.FcmNotificationUtil;
import com.google.firebase.messaging.Notification;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
Expand All @@ -31,22 +26,9 @@
@RequestMapping("/alarm")
@RequiredArgsConstructor
public class FcmController {
private final FcmService fcmService;
private final FcmTokenService fcmTokenService;
private final FcmNotificationService fcmNotificationService;

@Operation(summary = "푸시 메시지 전송", description = "저장된 모든 토큰에 푸시 메시지를 전송합니다.")
@PostMapping("/send")
public ResponseEntity<Void> pushMessageToAll(
@RequestBody @Validated FcmSendRequest fcmSendRequest) {
Notification notification =
FcmNotificationUtil.buildNotification(
fcmSendRequest.title(), fcmSendRequest.body());
List<String> tokens = fcmTokenService.getAllTokens();
fcmService.sendMulticastMessage(notification, tokens);
return ResponseEntity.ok().build();
}

@Operation(summary = "FCM 토큰 저장", description = "로그인 시 FCM 토큰을 저장합니다.")
@PostMapping("/token")
public ResponseEntity<Void> fcmTokenStore(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.depromeet.stonebed.domain.fcm.dao.FcmNotificationRepository;
import com.depromeet.stonebed.domain.fcm.dao.FcmRepository;
import com.depromeet.stonebed.domain.fcm.domain.FcmMessage;
import com.depromeet.stonebed.domain.fcm.domain.FcmNotification;
import com.depromeet.stonebed.domain.fcm.domain.FcmNotificationType;
import com.depromeet.stonebed.domain.fcm.domain.FcmToken;
Expand All @@ -11,19 +12,19 @@
import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordBoostRepository;
import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository;
import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord;
import com.depromeet.stonebed.domain.sqs.application.SqsMessageService;
import com.depromeet.stonebed.global.common.constants.FcmNotificationConstants;
import com.depromeet.stonebed.global.error.ErrorCode;
import com.depromeet.stonebed.global.error.exception.CustomException;
import com.depromeet.stonebed.global.util.FcmNotificationUtil;
import com.depromeet.stonebed.global.util.MemberUtil;
import com.google.firebase.messaging.Notification;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
Expand All @@ -35,7 +36,7 @@
@RequiredArgsConstructor
@Transactional
public class FcmNotificationService {
private final FcmService fcmService;
private final SqsMessageService sqsMessageService;
private final FcmNotificationRepository notificationRepository;
private final MissionRecordBoostRepository missionRecordBoostRepository;
private final MissionRecordRepository missionRecordRepository;
Expand Down Expand Up @@ -151,17 +152,18 @@ private FcmNotificationConstants determineNotificationType(Long totalBoostCount)

private void sendBoostNotification(
MissionRecord missionRecord, FcmNotificationConstants notificationConstants) {
Notification notification =
FcmNotificationUtil.buildNotification(
notificationConstants.getTitle(), notificationConstants.getMessage());

String token =
fcmRepository
.findByMember(missionRecord.getMember())
.map(FcmToken::getToken)
.orElseThrow(() -> new CustomException(ErrorCode.FAILED_TO_FIND_FCM_TOKEN));

fcmService.sendSingleMessage(notification, token);
FcmMessage fcmMessage =
FcmMessage.of(
notificationConstants.getTitle(),
notificationConstants.getMessage(),
token);
sqsMessageService.sendMessage(fcmMessage);

saveNotification(
FcmNotificationType.BOOSTER,
Expand Down Expand Up @@ -204,10 +206,23 @@ private List<FcmNotification> buildNotificationList(
}

public void sendAndNotifications(String title, String message, List<String> tokens) {
Notification notification = FcmNotificationUtil.buildNotification(title, message);
fcmService.sendMulticastMessage(notification, tokens);
List<List<String>> batches = createBatches(tokens, 10);

for (List<String> batch : batches) {
sqsMessageService.sendBatchMessages(batch, title, message);
}

List<FcmNotification> notifications = buildNotificationList(title, message, tokens);
notificationRepository.saveAll(notifications);
}

private List<List<String>> createBatches(List<String> tokens, int batchSize) {
return IntStream.range(0, (tokens.size() + batchSize - 1) / batchSize)
.mapToObj(
i ->
tokens.subList(
i * batchSize,
Math.min(tokens.size(), (i + 1) * batchSize)))
.collect(Collectors.toList());
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.depromeet.stonebed.domain.fcm.domain;

public record FcmMessage(String title, String body, String token) {

public static FcmMessage of(String title, String body, String token) {
return new FcmMessage(title, body, token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.depromeet.stonebed.domain.sqs.application;

import com.depromeet.stonebed.domain.fcm.domain.FcmMessage;
import com.depromeet.stonebed.infra.properties.SqsProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.BatchResultErrorEntry;
import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequest;
import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequestEntry;
import software.amazon.awssdk.services.sqs.model.SendMessageBatchResponse;
import software.amazon.awssdk.services.sqs.model.SendMessageRequest;
import software.amazon.awssdk.services.sqs.model.SendMessageResponse;

@Slf4j
@Transactional
@RequiredArgsConstructor
@Service
public class SqsMessageService {

private final SqsClient sqsClient;

private final SqsProperties sqsProperties;

private final ObjectMapper objectMapper;

public void sendMessage(Object message) {
try {
String messageBody = objectMapper.writeValueAsString(message);
SendMessageRequest sendMsgRequest =
SendMessageRequest.builder()
.queueUrl(sqsProperties.queueUrl())
.messageBody(messageBody)
.build();

SendMessageResponse sendMsgResponse = sqsClient.sendMessage(sendMsgRequest);
log.info("메시지 전송 완료, ID: {}", sendMsgResponse.messageId());
} catch (Exception e) {
log.error("SQS 메시지 전송 실패: {}", e.getMessage());
}
}

public void sendBatchMessages(List<String> tokens, String title, String message) {
List<SendMessageBatchRequestEntry> entries = new ArrayList<>();
for (String token : tokens) {
try {
FcmMessage fcmMessage = FcmMessage.of(title, message, token);
String messageBody = objectMapper.writeValueAsString(fcmMessage);
SendMessageBatchRequestEntry entry =
SendMessageBatchRequestEntry.builder()
.id(UUID.randomUUID().toString())
.messageBody(messageBody)
.build();
entries.add(entry);
} catch (Exception e) {
log.error("메시지 직렬화 실패: {}", e.getMessage());
}
}

SendMessageBatchRequest batchRequest =
SendMessageBatchRequest.builder()
.queueUrl(sqsProperties.queueUrl())
.entries(entries)
.build();

try {
SendMessageBatchResponse batchResponse = sqsClient.sendMessageBatch(batchRequest);

// 실패한 메시지 처리
List<BatchResultErrorEntry> failedMessages = batchResponse.failed();
if (!failedMessages.isEmpty()) {
for (BatchResultErrorEntry failed : failedMessages) {
log.error("메시지 전송 실패, ID {}: {}", failed.id(), failed.message());
}
}

} catch (Exception e) {
log.error("SQS 배치 메시지 전송 실패: {}", e.getMessage());
}
}
}

This file was deleted.

Loading

0 comments on commit 368b82b

Please sign in to comment.