[UMC 3기] Vibecap

내가 작성한 게시글, 댓글에 좋아요나 댓글이 추가되면 알림을 보내주는 기능을 추가하기로 했다.

API 명세서

추상화

IPC 중 Message Queue에서 아이디어를 얻었다.

이벤트(댓글, 대댓글, 좋아요)가 발생하면 그에 대한 notice를 생성하여 DB에 저장한다.
이후 클라이언트가 알림 조회 api를 호출하면 특정 사용자에게 도착한 notice들을 전송해준다.

Notice 클래스

알림을 추상화한 클래스이다. 이벤트 종류, 생성일자, 확인 여부, 보낸 사람, 받는 사람에 대한 정보를 저장할 필드를 가진다.

NoticeManager 클래스

  • sendNotice 메서드: 이벤트에 대한 notice를 생성해 DB에 저장한다.
    PostService, CommentService, SubCommentService 속에서 좋아요, 댓글, 대댓글이 추가될 때 sendNotice 메서드를 호출한다.

NoticeService 클래스

  • getNoticeFor(Long memberId): 클라이언트에게 원하는 회원에게 도착한 아직 확인하지 않은 알림들을 조회하고 전송해준다.
  • checkRead(NoticeEvent type, Long noticeId): 특정 알림을 확인 처리한다.

알림 조회 화면

댓글, 대댓글 알림에는 간단한 요약과 작성자의 닉네임이 들어가야 한다.

구현

좋아요, 댓글, 대댓글에 대한 알림을 구별하기 위해 세 개의 notice 클래스를 따로 만들었다. 구현하다 보니 중복되는 필드가 많아 하나의 조상 클래스를 상속받도록 만들면 좋겠다고 생각했지만 Entity 객체의 상속에 대해서는 아직 잘 몰라 다음에 공부하고 해야겠다고 생각했다.

NoticeComment 클래스

@Getter
@Setter
@Entity
@Table(name = "notice_comment")
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EntityListeners(AuditingEntityListener.class)
public class NoticeComment {

    @Setter(AccessLevel.NONE)
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "notice_comment_id")
    Long noticeCommentId;

    @ManyToOne  // 댓글 하나에 대해 여러 개의 대댓글 알림이 연관됨
    @JoinColumn(name = "post_id")
    @OnDelete(action = OnDeleteAction.CASCADE)
    Post post;  // 댓글이 추가된 게시글

    /**
     * receiver, sender를 member 테이블을 참조하는 외래 키로 지정하지 않은 이유
     * 1. join할 일이 없다.
     * 2. 해당 member가 삭제되어도 상관 없다. 그냥 알림만 보내면 된다.
     */

    @CreatedDate
    @Column(name = "created_time")
    LocalDateTime createdTime;

    @Column(name = "receiver_id")
    Long receiverId;    // 게시글 작성자

    @Column(name = "sender_nickname", length = 16)
    String senderNickname;      // 댓글 작성자 닉네임

    @Column(name = "is_read")
    @ColumnDefault("false")
    boolean isRead;

    @Column(name = "summary", length = 16)
    String summary;

    public NoticeEvent getEventType() {
        return NoticeEvent.COMMENT;
    }
}

NoticeLike 클래스

/**
 * 게시글에 좋아요가 추가되었음을 알림
 */
@Getter
@Setter
@Entity
@Table(name = "notice_like")
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EntityListeners(AuditingEntityListener.class)
public class NoticeLike {

    @Setter(AccessLevel.NONE)
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "notice_like_id")
    Long noticeLikeId;

    @ManyToOne  // 게시글 하나에 대해 여러 개의 좋아요 알림이 연관됨
    @JoinColumn(name = "post_id")
    @OnDelete(action = OnDeleteAction.CASCADE)
    Post post;  // 좋아요가 추가된 게시글

    /**
     * receiver, sender를 member 테이블을 참조하는 외래 키로 지정하지 않은 이유
     * 1. join할 일이 없다.
     * 2. 해당 member가 삭제되어도 상관 없다. 그냥 알림만 보내면 된다.
     */

    @CreatedDate
    @Column(name = "created_time")
    LocalDateTime createdTime;

    @Column(name = "receiver_id")
    Long receiverId;    // 게시글 작성자

    @Column(name = "sender_nickname", length = 16)
    String senderNickname;  // 좋아요 누른 회원 닉네임

    @Column(name = "is_read")
    @ColumnDefault("false")
    boolean isRead;

    public NoticeEvent getEventType() {
        return NoticeEvent.LIKE;
    }
}

NoticeSubComment 클래스

@Getter
@Setter
@Table(name = "notice_sub_comment")
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EntityListeners(AuditingEntityListener.class)
public class NoticeSubComment {

    @Setter(AccessLevel.NONE)
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "sub_comment_notice_id")
    Long subCommentNoticeId;

    @ManyToOne
    @JoinColumn(name = "comment_id")
    @OnDelete(action = OnDeleteAction.CASCADE)
    Comments comment;       // 대댓글이 추가된 댓글

    /**
     * receiver, sender를 member 테이블을 참조하는 외래 키로 지정하지 않은 이유
     * 1. join할 일이 없다.
     * 2. 해당 member가 삭제되어도 상관 없다. 그냥 알림만 보내면 된다.
     */

    @CreatedDate
    @Column(name = "created_time")
    LocalDateTime createdTime;

    @Column(name = "receiver_id")
    Long receiverId;    // 게시글 작성자

    @Column(name = "sender_nickname", length = 16)
    String senderNickname;      // 대댓글 작성자 닉네임

    @Column(name = "is_read")
    @ColumnDefault("false")
    boolean isRead;

    @Column(name = "summary", length = 16)
    String summary;

    public NoticeEvent getEventType() {
        return NoticeEvent.SUB_COMMENT;
    }
}

NoticeManager 클래스

/**
 * 댓글, 대댓글, 좋아요가 추가될 때 알림을 전송 (table에 저장)
 */
@Component
public class NoticeManager {

    private static final int SUMMARY_MAX_LENGTH = 13;

	/**
	 * DI, private member 코드 생략
     */ 

    /**
     * TODO NoticeComment, NoticeSubComment, NoticeLike가 Notice 클래스를 상속받도록 리팩토링
     * TODO sendNotice(<T extends Notice> notice) 로 오버로딩 단순화
     */

    /**
     * 댓글을 작성하는 메서드 내부에서 작동
     * @param comment
     * 내 게시글에 추가된 댓글
     * @return
     */
    public NoticeComment sendNotice(Comments comment) {
        Post targetPost;
        Member receiver;
        Member sender;
        NoticeComment notice;
        String summary;

        targetPost = comment.getPost();     // 댓글이 추가된 게시글
        receiver = targetPost.getMember();  // 게시글 작성자
        sender = comment.getMember();       // 댓글 작성자
        if (comment.getCommentBody().length() > SUMMARY_MAX_LENGTH)
            summary = comment.getCommentBody().substring(0, SUMMARY_MAX_LENGTH) + "...";
        else
            summary = comment.getCommentBody();

        notice = NoticeComment.builder()
                .post(comment.getPost())
                .receiverId(receiver.getMemberId())
                .senderNickname(sender.getNickname())
                .summary(summary)
                .build();

        return noticeCommentRepository.save(notice);
    }

    /**
     * 대댓글을 작성하는 메서드 내부에서 작동
     * 댓글 작성자에게만 알림 전송 (게시글 작성자에게는 알림 전송하지 않음)
     */
    public NoticeSubComment sendNotice(SubComment subComment) {
        Comments targetComment;
        Member receiver;
        Member sender;
        NoticeSubComment notice;
        String summary;

        targetComment = subComment.getComments();     // 대댓글이 추가된 댓글
        receiver = targetComment.getMember();         // 댓글 작성자
        sender = subComment.getMember();              // 대댓글 작성자
        if (subComment.getSubCommentBody().length() > SUMMARY_MAX_LENGTH)
            summary = subComment.getSubCommentBody().substring(0, SUMMARY_MAX_LENGTH) + "...";
        else
            summary = subComment.getSubCommentBody();

        notice = NoticeSubComment.builder()
                .comment(subComment.getComments())
                .receiverId(receiver.getMemberId())
                .senderNickname(sender.getNickname())
                .summary(summary)
                .build();

        return noticeSubCommentRepository.save(notice);
    }

    /**
     * receiver의 게시글에 sender가 좋아요를 눌렀음을 알림
     * @param like
     * @return
     */
    public NoticeLike sendNotice(Likes like) {
        Post targetPost;
        Member receiver;
        Member sender;
        NoticeLike notice;

        targetPost = like.getPost();        // 좋아요가 추가된 게시글
        receiver = targetPost.getMember();  // 게시글 작성자
        sender = like.getMember();          // 좋아요 누른 사람

        notice = NoticeLike.builder()
                .post(like.getPost())
                .receiverId(receiver.getMemberId())
                .senderNickname(sender.getNickname())
                .build();

        return noticeLikeRepository.save(notice);
    }
}

sendNotice 메서드 활용

CommentService 객체의 댓글 작성 기능을 구현한 메서드에서 댓글을 생성한 뒤 알림을 보낸다.
나머지 알림도 동일한 방식으로 전송된다.

    /** 댓글 작성 **/
    @Transactional
    public CommentDto writeComment(Long PostId, CommentDto commentDto, Member member) {
        Comments comment = new Comments();
        comment.setCommentBody(commentDto.getCommentBody());

        // 게시판 번호로 게시글 찾기
        Post post = postsRepository.findById(PostId).orElseThrow(() -> {
            return new IllegalArgumentException("게시판을 찾을 수 없습니다.");
        });

        comment.setMember(member);
        comment.setPost(post);
        commentRepository.save(comment);

        countComments(PostId);

        // comment 알림 전송 (본인 게시글에 댓글 단 경우 제외)
        if (post.getMember().getMemberId() != member.getMemberId())
            noticeManager.sendNotice(comment);

        return CommentDto.toDto(comment);
    }

Comments