그룹 초대 , 친구 요청 서비스를 수행하는데 있어 ‘알림 전송 서비스’를 구현할 때 FCM을 사용했다.
1. FCM 자체의 성능 개선을 위해 RabbitMQ을 사용함.
2. FCM 요청에 있어 오류가 발생할 경우 처리를 위해 데드레터 처리
기존 Spring 서버에서 FCM 까지의 메시지 전송은 동기방식이기 때문에 짧은 시간에 많은 요청이 수행되는 경우 성능상 개선 필요, 메시지 큐를 사용하여 비동기 방식으로 처리
Message Type 정의
public enum NotificationType {
FRIEND_REQUEST, // 친구 요청
GROUP_REQUEST, // 그룹 참여 요청
DESTINATION_CHANGE, // 목적지 변경
PLAN_REQUEST // 만남 참여 요청
}
NotificationRequest 정의
public NotificationRequest(
final String deviceToken, final String title, final String body, final String id) {
this.deviceToken = deviceToken;
this.title = title;
this.body = body;
this.id = id;
}
FcmProducer : RabbitMQ에 알림 메시지 저장
알림관련 정보 즉, rabbitmq로 발행할 메시지 정보들을 db에 저장한 이후 rabbitmq로 알림 발송을 위한 메시지를 발행한다.
@Component
@RequiredArgsConstructor
public class FcmProducer {
private final RabbitTemplate rabbitTemplate;
private final NotificationRepository notificationRepository;
private final NotificationMapper notificationMapper;
// 알림 전송 (DB 저장 후 큐로 전송)
public void sendNotification(final NotificationRequest request, final NotificationType type) {
final UUID id =
notificationRepository
.save(
new Notification(
request.getTitle(),
request.getBody(),
request.getId(),
request.getDeviceToken(),
type))
.getId();
request.setNotificationId(id.toString());
sendToQueue(request);
}
// 알림 메시지를 큐로 전송
private void sendToQueue(final NotificationRequest request) {
rabbitTemplate.convertAndSend(
RabbitMQConstant.NOTIFICATION_EXCHANGE.getName(),
RabbitMQConstant.NOTIFICATION_ROUTING_KEY.getName(),
request);
}
}
FcmCosumer : 큐에 저장된 메시지를 FCM 전송
@Transactional
@RabbitListener(queues = "notification.queue")
public void sendNotification(
final NotificationRequest request,
final Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) final long tag)
throws FirebaseMessagingException, IOException {
firebaseMessaging.send(request.toMessage());
final Optional<Notification> notification =
notificationRepository.findById(UUID.fromString(request.getNotificationId()));
if (notification.isPresent()) {
notification.get().complete();
return;
}
channel.basicNack(tag, false, false);
}
(적용) 만남 초대 서비스에서의 사용
@Transactional
public PlanResponse invitePlan(PlanMemberRequest request) {
Member member = authService.getMemberByJwt();
Plan plan = getPlanEntity(request.getPlanId());
plan.addMember(member);
// 만남 초대 알림 전송
sendRequestNotification(member.getDeviceTokenValue(), plan.getName(), plan.getId());
return planMapper.toPlanResponse(plan);
}
private void sendRequestNotification(
final String deviceToken, final String planName, final UUID id) {
fcmProducer.sendNotification(
new NotificationRequest(
deviceToken,
NotificationTitle.PLAN_REQUEST_TITLE.getName(),
NotificationBody.PLAN_REQUEST_BODY.toNotificationBody(planName),
id.toString()),
NotificationType.PLAN_REQUEST);
}
소프트웨어 시스템에서 오류로 인해 처리할 수 없는 메시지
데드 레터 메시지를 임시로 저장하는 특수 유형의 메시지 큐
- 기본적인 Message Queue는 FIFO 방식이다. 다음 메시지를 처리하기 위해서는 이전의 메시지들이 어떤 형태로든 처리되어야 한다.
- 앞의 메시지에 장애가 발생할 경우 DLQ를 통해 유연한 처리가 가능하도록 해야 한다.
데드레터 발생 시 슬랙 알림
현재는 알림 Entity의 상태를 실패로 미리 저장해둔 다음 성공시 상태를 성공으로 업데이트
DB를 확인했을 때 일정 시간(대략 30초) 성공으로 업데이트되지 않으면 실패했다고 인지
메시지가 유실된 경우 관리자 권한의 API를 생성하여 유실된 알림 재전송 구현
- 모든 실패 상태의 알림을 발송하기에는 현재진행 중인 알림이 있을 수 있기에 id를 직접 받아 처리
- 직접 관리자가 확인하여 수동으로 처리해야하기에 이 부분에 대한 개선 방안이 있을지 고민 중
관리자 권한 재전송 API
알림 발송 로직
이러한 수동적인 방법 말고 메시지 유실 자체를 방지하는 방법은?