Skip to content

[#17] 알림 시스템 설계 및 구현#28

Open
wooni97 wants to merge 27 commits intomainfrom
feature/77
Open

[#17] 알림 시스템 설계 및 구현#28
wooni97 wants to merge 27 commits intomainfrom
feature/77

Conversation

@wooni97
Copy link
Copy Markdown
Owner

@wooni97 wooni97 commented Dec 5, 2024

1. 알림 기능 정의

알림 기능의 핵심 : 사용자에게 정보를 전달하는 것.

(1) 정보 전달의 추상화: AbstractNotificationData
알림 데이터를 정의하는 추상 클래스입니다.

공통 필드 :

  • String notificationMessageContent: 알림 메시지의 내용
  • LocalDateTime scheduledAt: 예약 전송 시간

추가로 필요한 필드는 각 구현체에서 개별적으로 정의.

(2) 전달 방식의 추상화: NotificationSender
알림을 전송하는 방법을 추상화한 클래스입니다.

주요 메서드 :

  • public abstract Class<? extends AbstractNotificationData> getSupportedDataTypeClass(): 지원하는 데이터 타입 반환
  • public abstract void send(AbstractNotificationData notificationData): 알림 데이터를 전송

2. NotificaionService 설계

NotificationService는 AbstractNotificationData의 구현체를 특정 NotificationSender 구현체와 매핑하여 알림을 전송하는 역할을 담당합니다. 이 과정에서 추상화된 타입만 사용하도록 설계되었습니다.


3. DB와의 동적 매핑

DB에 저장된 AbstractNotificationData를 올바른 구현체로 매핑하기 위한 매핑 클래스를 정의했습니다.


4. 데이터베이스 테이블 설계 : NOTIFICATION

컬럼명 데이터 타입 제약 조건 설명
ID BIGINT PRIMARY KEY, AUTO_INCREMENT 알림의 고유 식별자
NOTIFICATION_TYPE VARCHAR(255) NOT NULL 알림 데이터 구현체의 클래스 이름
SCHEDULED_AT DATETIME NULLABLE 예약된 전송 시간 (NULL 가능)
PAYLOAD TEXT NOT NULL 알림 데이터를 JSON으로 직렬화한 내용

@wooni97 wooni97 added the feature label Dec 5, 2024
@wooni97 wooni97 requested a review from Habist December 5, 2024 07:46
Comment on lines +10 to +14
public abstract class AbstractNotificationData {

private final String notificationMessageContent;
private final LocalDateTime scheduledAt;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

사용자에게 정보를 전달한다는 맥락에서 사용되는 추상 메시지인데 사용자와 관련된 정보가 없네요. 어떤 사용자에게 보내는지를 식별할 수 있는 member id를 속성으로 가져도 되지 않을까요?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

반영완료

Comment on lines +15 to +17
public boolean isScheduled() {
return scheduledAt != null && scheduledAt.isAfter(LocalDateTime.now());
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

사용자에게 정보를 전달한다의 맥락에 메시지를 언제 보낼 것이다와 같은 맥락이 포함되는 건 단순히 누군가에게 메시지를 전달한다를 표현하는 모델에 구체적 동작이 포함된 구조이지 않나요?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

예약 시간을 필드로 관리하는 ScheduledNotification 클래스를 추가했습니다. 이 클래스는 별도의 테이블로 생성하여 관리하도록 수정했습니다. 또한 주기적으로 폴링하면서 알림 데이터의 예약 시간을 확인한 후 NotificationService에 작업을 위임하는 ScheduledNotificationProcessService를 생성했는데, 이런 방식으로 설계하는 것이 적절할지 궁금합니다!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

그렇게 구현해도 될 것 같고 알림 발송 기능을 사용자에게 정보를 언제 전달할지 결정 -> 어떻게 전달할지 결정 -> 알림 전달 과 같은 플로우로 설계하고 싶다면 스케줄이 가능한 타입을 따로 두어서 해당 타입을 확장하고 있는 구현체면 DB에 저장했다가 주기적으로 폴링하여 전송해도 될 것 같네요.

아니면 좀 더 단순하게 구현하고 싶으면 예약 발송 기능을 일반 알림 발송 기능과 아예 분리해서 구현하는 것도 괜찮을 것 같습니다.

Comment on lines +27 to +42
public void send(AbstractNotificationData notificationData) {
if(!(notificationData instanceof MailNotificationData mailNotification))
throw new ClassCastException("Expected MailNotification, but received: " + notificationData.getClass().getSimpleName());

MimeMessage mimeMessage = javaMailSender.createMimeMessage();
try {
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
mimeMessageHelper.setTo(mailNotification.getRecipientEmailAddress());
mimeMessageHelper.setSubject(mailNotification.getSubject());
mimeMessageHelper.setText(mailNotification.getNotificationMessageContent());

javaMailSender.send(mimeMessage);
} catch (MessagingException e) {
throw new RuntimeException(e.getMessage());
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

메시지를 send 하는 로직을 보면 NotificationData의 타입이 Sender가 올바르게 처리할 수 있는 타입이 아니면 예외를 던지고 이후에 구체적인 mail client를 사용해서 메일을 보내는 단계로 이루어져있는데

이때 JavaMailSender가 아닌 Aws의 Ses로 변경된다면? 네이버 email 서비스로 변경된다면? local 환경에서는 실제 메일을 보내는 것이 아닌 로그를 남기는 형태로 모킹을 하고 싶다면?

NotificationData의 타입 체크를 Sender가 하고 실제 클라이언트를 사용해서 메일을 전송하는 책임은 다른 객체에게 위임해도 되지 않을까요?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

반영완료

Comment on lines +13 to +20
private final List<NotificationSender> notificationSenders;
private final Map<Class<? extends AbstractNotificationData>, NotificationSender> senderMap = new HashMap<>();

public NotificationService(List<NotificationSender> notificationSenders) {
this.notificationSenders = notificationSenders;
initializeSenderMap();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

HashMap과 생성자의 매개변수로 전달받은 List를 그대로 할당하면 어떤 문제가 발생할 수 있을까요? final 선언만으로 불변임을 보장할 수 있을까요?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

final 선언은 재할당을 방지하지만, 참조하고 있는 원본 리스트가 변경될 경우 불변성을 보장하지 못합니다. 이를 해결하기 위해 우선 List.copyOf를 사용했지만 리스트에 담긴 객체가 불변 클래스가 아닌 경우 여전히 문제가 발생할 수 있습니다.

현재 리스트에 인터페이스를 담고 있는데, 전달받은 인터페이스 리스트의 불변성을 확실히 보장하기 위해 별도의 처리 로직이 필요할지 궁급합니다..!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

아뇨 리스트가 담고 있는 Sender는 서비스 성경의 무상태 객체이기 때문에 불변을 굳이 보장해주지 않아도 될 것 같습니다.

다만 자바의 List, Map, Set 같은 컬렉션 타입의 큰 단점 중 하나인데 런타임 시점에 add, remove, put 과 같은 추가, 삭제 행위를 List.copyOf와 같은 코드로 방지할 수 있지만 여전히 타입 자체에 해당 메소드를 사용할 수 있는 문제가 있습니다.

즉, 해당 컬렉션 객체가 추가나 삭제 행위가 불가능하다는 사실을 컴파일타임 수준에서는 알 수 없어요.

이런 문제를 해결하기 위해서 Kotlin에서는 Map, List, Set 타입과 MutableMap, MutableList 타입으로 아예 나눠놨습니다. 추후에 컬렉션 객체가 수정이 가능한지 불가능한지 컴파일타임 수준에서의 보장이 필요하다면 Kotlin을 도입해보는 것도 고려해보면 좋겠네요.


AbstractNotificationData mailNotification =
new MailNotificationData(content, null, subject, event.getUserEmail());
notificationRepository.save(mailNotification);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

회원가입 이벤트를 소비할 때 바로 NotificationService에 위임하지 않고 영속화시켰다가 다시 소비할 필요가 있나요?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

현재는 불필요해서 바로 NotificationService에 위임하도록 수정했습니다.

Comment on lines -8 to -13
class StudyRoomServiceTest {

private final StudyRoomRepository studyRoomRepository = Mockito.mock(StudyRoomRepository.class);


@Test
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

지금 스터디룸 관련 테스트 코드를 다 삭제하셨는데 이유가 뭘까요?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

StudyRoom 도메인이 더이상 불필요해서 테스트 코드를 삭제했습니다.

Copy link
Copy Markdown
Collaborator

@Habist Habist Dec 16, 2024

Choose a reason for hiding this comment

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

음 이 작업은 엄밀히 따지면 알림 기능 구현과는 다른 맥락인 것 같네요. 그럼 해당 테스트 코드를 삭제하는 작업을 다른 PR로 분리했어도 됐을 것 같습니다.

작업 단위를 작게 정의하여 PR을 올려주면 협업하는 동료들의 인지 부하를 줄일 수 있거든요.

Comment on lines +5 to +7
public interface NotificationRepository {
void save(AbstractNotificationData notification);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

이 코드는 더이상 사용하지 않는 코드인 것 같네요

Comment on lines +3 to +5
public interface MailClient {
void sendMail(String subject, String content, String receiverEmailAddress);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

매개변수를 전달할 때 MailNotificationData 를 그대로 전달해도 되지 않을까요? 이러면 필드가 추가될 때마다 해당 타입과 구현체 모두 코드 변경이 필요할 것 같아요

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants