Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: notification을 타입계층으로 리팩터링 #929

Merged
merged 14 commits into from
Jan 8, 2025
Merged
40 changes: 33 additions & 7 deletions backend/src/main/java/com/ody/mate/service/MateService.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,15 @@
import com.ody.meeting.domain.Meeting;
import com.ody.meeting.dto.response.MateEtaResponsesV2;
import com.ody.member.domain.Member;
import com.ody.notification.domain.FcmTopic;
import com.ody.notification.domain.Notification;
import com.ody.notification.domain.types.DepartureReminder;
import com.ody.notification.domain.types.Entry;
import com.ody.notification.domain.types.MateLeave;
import com.ody.notification.domain.types.MemberDeletion;
import com.ody.notification.domain.types.Nudge;
import com.ody.notification.service.NotificationService;
import com.ody.route.domain.DepartureTime;
import com.ody.route.domain.RouteTime;
import com.ody.route.service.RouteService;
import com.ody.util.TimeUtil;
Expand Down Expand Up @@ -43,14 +50,32 @@ public MateSaveResponseV2 saveAndSendNotifications(
Member member,
Meeting meeting
) {
validateMeetingOverdue(meeting);
validateAlreadyAttended(member, meeting);
Mate mate = saveMateAndEta(mateSaveRequest, member, meeting);

FcmTopic fcmTopic = new FcmTopic(meeting);
sendEntry(mate, fcmTopic);
notificationService.subscribeTopic(member.getDeviceToken(), fcmTopic);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... Entry 알림을 발송 + 구독 + DepartureReminder 알림을 발송 하는 식으로 MateService 로직에 종속적인 로직이 구현되어 있습니다.

그러나, 위의 로직은 MateService에서 드러나야 하는 로직이지, 알림 서비스가 알고 있어야할 내용이 아니라 생각했습니다.

[질문] 노션에 작성해주신 부분 중에 해당 로직을 MateService 로직에 종속적인 로직이라고 본 이유가 있나요?

저는 콜리와 반대로 '토픽을 구독한다'를 MateService가 알고 있을 필요가 없다고 생각하는데, 그 이유는 토픽을 구독하는 것은 모임원 모두에게 알림을 보내기 위한 방법이라서 입니다. MateService는 모두에게 알림을 보낸다 정도만 알아도 충분하다고 생각해서 오히려 내부 구현 + fcm 관련 배경 지식이 노출된 느낌이 들어요.

Copy link
Contributor Author

@coli-geonwoo coli-geonwoo Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

첫째로, 해당 메서드가 약속 참여 플로우 이외에 그 어떤 비즈니스 로직에도 확장성있게 사용될수가 없었어요. 참여 알림 보내고 + 구독하고 + 약속리마인더 알림을 보낸다 -> 라는 흐름은 약속 참여시 약속을 보내는 방법이지 알림 서비스가 알아야할 약속 참여 시 필요한 알림 로직이 아니라 생각했습니다.

두번째로, saveAndSendNotification으로 추상화된 메서드 명을 보면 그저 알림을 저장하고 보내는 것으로 생각이 되는데, 저장을 하는 알림이 약속 리마인더와 출발알림으로 정해져 있습니다. 다른 알림은 저장될수가 없고 보낼수도 없습니다. mateService 참여 시 필요한 알림을 해당 메서드에서 보내고 있었습니다.

세번째로, fcm 구현이 밖으로 드러나있다는 점에 대해서는 동의합니다. 해당 구현을 캡슐화하기 위해 sendToDevice, sendToTopic 등의 메서드를 만들어주어 subscribe를 드러내지 않는 것은 동의합니다. 그러나, notificationService가 특정 알림 객체를 위한 메서드를 가지는 것은 비즈니스 로직상 어떤 알림을 보냈는지를 상위 서비스가 아니라 하위 서비스인 notificationService가 알게된다는 점에서 너무 많은 것을 알고 있다고 생각합니다. 또한, 기존에 notificationServic가 unsubscribe라는 메서드를 가지고 있다는 점, fcmTopic 필드를 meeting이 가지고 있다는 점에서 fcm 로직을 도메인들도 알고 있다고 판단한 부분도 있습니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 납득했습니다!! 자세하게 설명해주어 고마워요 🥦🙇‍♀️

sendDepartureReminder(meeting, mate, fcmTopic);
return MateSaveResponseV2.from(meeting);
}

private void sendEntry(Mate mate, FcmTopic fcmTopic) {
Entry entry = new Entry(mate, fcmTopic);
notificationService.saveAndSchedule(entry.toNotification());
}

private void sendDepartureReminder(Meeting meeting, Mate mate, FcmTopic fcmTopic) {
DepartureTime departureTime = new DepartureTime(meeting, mate.getEstimatedMinutes());
DepartureReminder departureReminder = new DepartureReminder(mate, departureTime, fcmTopic);
notificationService.saveAndSchedule(departureReminder.toNotification());
}

private void validateMeetingOverdue(Meeting meeting) {
if (meeting.isOverdue()) {
throw new OdyBadRequestException("참여 가능한 시간이 지난 약속에 참여할 수 없습니다.");
}

Mate mate = saveMateAndEta(mateSaveRequest, member, meeting);
notificationService.saveAndSendNotifications(meeting, mate, member.getDeviceToken());
return MateSaveResponseV2.from(meeting);
}

public void validateAlreadyAttended(Member member, Meeting meeting) {
Expand Down Expand Up @@ -78,7 +103,8 @@ public void nudge(NudgeRequest nudgeRequest) {
Mate requestMate = findFetchedMate(nudgeRequest.requestMateId());
Mate nudgedMate = findFetchedMate(nudgeRequest.nudgedMateId());
validateNudgeCondition(requestMate, nudgedMate);
notificationService.sendNudgeMessage(requestMate, nudgedMate);
Nudge nudge = new Nudge(nudgedMate);
notificationService.sendNudgeMessage(requestMate, nudge);
Comment on lines +106 to +107
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[제안]
1안
saveAndSchedule(notification)처럼 saveAndSend(notification) 메서드를 만드는건 어떤가요?

  • saveAndSchedule() : 알림을 저장하고 예약한다
  • saveAndSend() : 알림을 저장하고 바로 보낸다
  • save() : 알림을 저장만 하고 보내지 않는다

알림을 저장하고, 보낸다라는 행위만 알면되도록 저수준의 서비스에 맞게 알림의 구체적인 종류는 모르도록 리팩터링할 수 있었습니다.

sendNudgeMessage()의 경우엔 알림의 구체적인 종류에 의존적이라, 추상화 수준이 달라 어색하게 느껴지는 것 같아요.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2안
1안에서 언급한 saveAndSchedule()saveAndSend()를 통합하는 방법입니다.

스케줄링 로직을 외부에서 알 필요가 있을까요? 외부에서 NotificationService를 사용할 땐, 알림을 보낸다/안 보낸다만 알면 되지 않을까요?
Notification의 sendAt 시점을 보고도 스케줄링을 해야 할지, 하지 않아도 될지 알 수 있을 것 같아요.
즉, NotificationService public 메서드는 두개로 구분되게요.

  • saveAndSend() : 알림을 저장하고 보낸다.(스케줄링 포함)
  • save() : 알림을 저장만 하고 보내지 않는다

단, NotificationService 내에서 private 메서드로 스케줄링이 필요한지 아닌지 분기처리가 필요해지는 단점이 있을 수 있어요.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이슈 파서 작업해주겠습니다~

}

private void validateNudgeCondition(Mate requestMate, Mate nudgedMate) {
Expand Down Expand Up @@ -133,15 +159,15 @@ public void deleteAllByMember(Member member) {

@Transactional
public void withdraw(Mate mate) {
Notification memberDeletionNotification = Notification.createMemberDeletion(mate);
Notification memberDeletionNotification = new MemberDeletion(mate).toNotification();
notificationService.save(memberDeletionNotification);
delete(mate);
}

@Transactional
public void leaveByMeetingIdAndMemberId(Long meetingId, Long memberId) {
Mate mate = findByMeetingIdAndMemberId(meetingId, memberId);
Notification leaveNotification = Notification.createMateLeave(mate);
Notification leaveNotification = new MateLeave(mate).toNotification();
notificationService.save(leaveNotification);
delete(mate);
}
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

~ 👍

Original file line number Diff line number Diff line change
Expand Up @@ -60,58 +60,8 @@ public Notification(
this(null, mate, type, sendAt, status, fcmTopic);
}

public static Notification createEntry(Mate mate, FcmTopic fcmTopic) {
return new Notification(
mate,
NotificationType.ENTRY,
LocalDateTime.now(),
NotificationStatus.DONE,
fcmTopic
);
}

public static Notification createDepartureReminder(Mate mate, LocalDateTime sendAt, FcmTopic fcmTopic) {
return new Notification(
mate,
NotificationType.DEPARTURE_REMINDER,
sendAt,
NotificationStatus.PENDING,
fcmTopic
);
}

public static Notification createNudge(Mate nudgeMate) {
return new Notification(
nudgeMate,
NotificationType.NUDGE,
LocalDateTime.now(),
NotificationStatus.DONE,
null
);
}

public static Notification createMemberDeletion(Mate mate) {
return new Notification(
mate,
NotificationType.MEMBER_DELETION,
LocalDateTime.now(),
NotificationStatus.DONE,
null
);
}

public static Notification createMateLeave(Mate mate) {
return new Notification(
mate,
NotificationType.LEAVE,
LocalDateTime.now(),
NotificationStatus.DONE,
null
);
}

public boolean isDepartureReminder() {
return this.type.isDepartureReminder();
return type.isDepartureReminder();
}

public boolean isStatusDismissed() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.ody.notification.domain.types;

import com.ody.mate.domain.Mate;
import com.ody.notification.domain.FcmTopic;
import com.ody.notification.domain.Notification;
import com.ody.notification.domain.NotificationStatus;
import com.ody.notification.domain.NotificationType;
import java.time.LocalDateTime;

public abstract class AbstractNotification {

private final Mate mate;
private final NotificationType type;
private final LocalDateTime sendAt;
private final NotificationStatus status;
private final FcmTopic fcmTopic;

protected AbstractNotification(
Mate mate,
LocalDateTime sendAt,
NotificationStatus status,
FcmTopic fcmTopic
) {
this.mate = mate;
this.type = getType();
this.sendAt = calculateSendAt(sendAt);
this.status = status;
this.fcmTopic = fcmTopic;
}

private static LocalDateTime calculateSendAt(LocalDateTime sendAt) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[제안]
static 키워드 이제는 제거해주어도 되겠네요! 이 부분은 제가 코드 수정하면서 반영하겠습니다ㅎㅎ

Suggested change
private static LocalDateTime calculateSendAt(LocalDateTime sendAt) {
private LocalDateTime calculateSendAt(LocalDateTime sendAt) {

if (sendAt.isBefore(LocalDateTime.now())) {
return LocalDateTime.now();
}
return sendAt;
}
Comment on lines +31 to +36
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[질문] 해당 부분은 Departure Reminder를 위한 로직인데 AbstractNotification에 위치시킨 이유가 있나요?

Copy link
Contributor Author

@coli-geonwoo coli-geonwoo Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음에는 Departure Reminder에 위치시켰었는데요.

이전 Notifiaction 로직에서 NotificationService의 private method로 보내기 전에 calualteSendAt으로 모든 notification의 sendAt을 체크해서 공통로직인 줄 알았습니다.

전송 시간이 과거이면 현재 보낸다 라는 규칙을 Notification 전체 규칙으로 바라볼 것인지, Departure_Reminder 특수 도메인 규칙으로 바라볼 것인지의 차이일 것 같아요.

제리는 어떻게 생각하시나요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음.. DepartureReminder의 특수 규칙이었을 때 더 확장성이 좋을 것 같아요.
전송 시간이 과거이면 보내지 않는다라는 선택지가 생길 수 있으니까요.
물론, 지금은 그런 로직이 없어서 전체 규칙으로 바라보아도 무방할 것 같습니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금까지 공통 로직으로 서비스되긴 했지만.... 😭
Departure Reminder만을 위한 규칙 혹은 중요한 발송 알림 대상 규칙으로 두어야 좋다고 생각해요!!


abstract NotificationType getType();

public Notification toNotification() {
return new Notification(null, mate, type, sendAt, status, fcmTopic);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[제안]
Notification id를 받지 않은 생성자에서 null을 넣어주고 있으니 제거해주어도 되겠네요! 괜찮으시면 이 부분도 제가 추후에 반영해두겠습니다~

Suggested change
return new Notification(null, mate, type, sendAt, status, fcmTopic);
return new Notification(mate, type, sendAt, status, fcmTopic);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.ody.notification.domain.types;

import com.ody.mate.domain.Mate;
import com.ody.notification.domain.FcmTopic;
import com.ody.notification.domain.NotificationStatus;
import com.ody.notification.domain.NotificationType;
import com.ody.route.domain.DepartureTime;

public class DepartureReminder extends AbstractNotification {

public DepartureReminder(Mate mate, DepartureTime departureTime, FcmTopic fcmTopic) {
super(mate, departureTime.getValue(), NotificationStatus.PENDING, fcmTopic);
}

@Override
public NotificationType getType() {
return NotificationType.DEPARTURE_REMINDER;
}
}
20 changes: 20 additions & 0 deletions backend/src/main/java/com/ody/notification/domain/types/Entry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ody.notification.domain.types;

import com.ody.mate.domain.Mate;
import com.ody.notification.domain.FcmTopic;
import com.ody.notification.domain.NotificationStatus;
import com.ody.notification.domain.NotificationType;
import java.time.LocalDateTime;

public class Entry extends AbstractNotification {

public Entry(Mate mate, FcmTopic fcmTopic) {
super(mate, LocalDateTime.now(), NotificationStatus.DONE, fcmTopic);
}

@Override
public NotificationType getType() {
return NotificationType.ENTRY;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ody.notification.domain.types;

import com.ody.mate.domain.Mate;
import com.ody.notification.domain.NotificationStatus;
import com.ody.notification.domain.NotificationType;
import java.time.LocalDateTime;

public class MateLeave extends AbstractNotification {

public MateLeave(Mate mate) {
super(mate, LocalDateTime.now(), NotificationStatus.DONE, null);
}

@Override
public NotificationType getType() {
return NotificationType.LEAVE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ody.notification.domain.types;

import com.ody.mate.domain.Mate;
import com.ody.notification.domain.NotificationStatus;
import com.ody.notification.domain.NotificationType;
import java.time.LocalDateTime;

public class MemberDeletion extends AbstractNotification {

public MemberDeletion(Mate mate) {
super(mate, LocalDateTime.now(), NotificationStatus.DONE, null);
}

@Override
public NotificationType getType() {
return NotificationType.MEMBER_DELETION;
}
}
18 changes: 18 additions & 0 deletions backend/src/main/java/com/ody/notification/domain/types/Nudge.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ody.notification.domain.types;

import com.ody.mate.domain.Mate;
import com.ody.notification.domain.NotificationStatus;
import com.ody.notification.domain.NotificationType;
import java.time.LocalDateTime;

public class Nudge extends AbstractNotification {

public Nudge(Mate nudgeMate) {
super(nudgeMate, LocalDateTime.now(), NotificationStatus.DONE, null);
}

@Override
public NotificationType getType() {
return NotificationType.NUDGE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
import com.ody.notification.domain.NotificationStatus;
import com.ody.notification.domain.NotificationType;
import com.ody.notification.domain.message.GroupMessage;
import com.ody.notification.domain.types.Nudge;
import com.ody.notification.dto.response.NotiLogFindResponses;
import com.ody.notification.repository.NotificationRepository;
import com.ody.notification.service.event.NoticeEvent;
import com.ody.notification.service.event.NudgeEvent;
import com.ody.notification.service.event.PushEvent;
import com.ody.notification.service.event.SubscribeEvent;
import com.ody.notification.service.event.UnSubscribeEvent;
import com.ody.route.domain.DepartureTime;
import com.ody.util.InstantConverter;
import java.time.Instant;
import java.time.LocalDateTime;
Expand All @@ -40,35 +40,14 @@ public class NotificationService {
private final TaskScheduler taskScheduler;

@Transactional
public void saveAndSendNotifications(Meeting meeting, Mate mate, DeviceToken deviceToken) {
FcmTopic fcmTopic = new FcmTopic(meeting);

Notification entryNotification = Notification.createEntry(mate, fcmTopic);
saveAndScheduleNotification(entryNotification);

SubscribeEvent subscribeEvent = new SubscribeEvent(this, deviceToken, fcmTopic);
fcmEventPublisher.publish(subscribeEvent);

saveAndSendDepartureReminderNotification(meeting, mate, fcmTopic);
}

private void saveAndSendDepartureReminderNotification(Meeting meeting, Mate mate, FcmTopic fcmTopic) {
DepartureTime departureTime = new DepartureTime(meeting, mate.getEstimatedMinutes());
LocalDateTime sendAt = calculateSendAt(departureTime);
Notification notification = Notification.createDepartureReminder(mate, sendAt, fcmTopic);
saveAndScheduleNotification(notification);
}

private LocalDateTime calculateSendAt(DepartureTime departureTime) {
if (departureTime.isBefore(LocalDateTime.now())) {
return LocalDateTime.now();
}
return departureTime.getValue();
public void saveAndSchedule(Notification notification) {
Notification savedNotification = save(notification);
scheduleNotification(savedNotification);
}

private void saveAndScheduleNotification(Notification notification) {
Notification savedNotification = notificationRepository.save(notification);
scheduleNotification(savedNotification);
@Transactional
public Notification save(Notification notification) {
return notificationRepository.save(notification);
}

private void scheduleNotification(Notification notification) {
Expand All @@ -83,9 +62,14 @@ private void scheduleNotification(Notification notification) {
);
}

public void subscribeTopic(DeviceToken deviceToken, FcmTopic fcmTopic){
SubscribeEvent subscribeEvent = new SubscribeEvent(this, deviceToken, fcmTopic);
fcmEventPublisher.publish(subscribeEvent);
}

@Transactional
public void sendNudgeMessage(Mate requestMate, Mate nudgedMate) {
Notification nudgeNotification = notificationRepository.save(Notification.createNudge(nudgedMate));
public void sendNudgeMessage(Mate requestMate, Nudge nudge) {
Notification nudgeNotification = notificationRepository.save(nudge.toNotification());
NudgeEvent nudgeEvent = new NudgeEvent(this, requestMate, nudgeNotification);
fcmEventPublisher.publishWithTransaction(nudgeEvent);
}
Expand All @@ -107,11 +91,6 @@ public void schedulePendingNotification() {
log.info("애플리케이션 시작 - PENDING 상태 출발 알림 {}개 스케줄링", notifications.size());
}

@Transactional
public Notification save(Notification notification) {
return notificationRepository.save(notification);
}

@DisabledDeletedFilter
public NotiLogFindResponses findAllNotiLogs(Long meetingId) {
List<Notification> notifications = notificationRepository.findAllByMeetingIdAndSentAtBeforeDateTimeAndStatusIsNotDismissed(
Expand Down
Loading
Loading