diff --git a/src/main/java/com/jiwon/mylog/domain/comment/service/CommentService.java b/src/main/java/com/jiwon/mylog/domain/comment/service/CommentService.java index 1056959..5f6ae66 100644 --- a/src/main/java/com/jiwon/mylog/domain/comment/service/CommentService.java +++ b/src/main/java/com/jiwon/mylog/domain/comment/service/CommentService.java @@ -15,6 +15,8 @@ import com.jiwon.mylog.global.common.error.exception.NotFoundException; import com.jiwon.mylog.domain.post.repository.PostRepository; import com.jiwon.mylog.domain.user.repository.UserRepository; +import java.util.HashSet; +import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; @@ -49,12 +51,18 @@ public CommentResponse createComment(Long userId, Long postId, CommentCreateRequ Comment comment = Comment.create(request, parent, user, post); Comment savedComment = commentRepository.save(comment); - Long receiverId = post.getUser().getId(); + Set receiverIds = new HashSet<>(); + receiverIds.add(post.getUser().getId()); + if (parent != null) { + receiverIds.add(parent.getUser().getId()); + } + receiverIds.remove(userId); eventPublisher.publishEvent( new CommentCreatedEvent( postId, - receiverId, + post.getUser().getId(), + receiverIds, savedComment.getId(), userId, user.getUsername(), diff --git a/src/main/java/com/jiwon/mylog/domain/event/NotificationEventListener.java b/src/main/java/com/jiwon/mylog/domain/event/NotificationEventListener.java index 4744eee..e599ac1 100644 --- a/src/main/java/com/jiwon/mylog/domain/event/NotificationEventListener.java +++ b/src/main/java/com/jiwon/mylog/domain/event/NotificationEventListener.java @@ -1,22 +1,18 @@ package com.jiwon.mylog.domain.event; +import com.jiwon.mylog.domain.event.dto.NotificationSendEvent; import com.jiwon.mylog.domain.event.dto.comment.CommentCreatedEvent; import com.jiwon.mylog.domain.event.dto.follow.FollowCreatedEvent; import com.jiwon.mylog.domain.event.dto.follow.FollowDeletedEvent; import com.jiwon.mylog.domain.event.dto.like.LikeCreatedEvent; -import com.jiwon.mylog.domain.notification.entity.Notification; -import com.jiwon.mylog.domain.notification.repository.NotificationRepository; import com.jiwon.mylog.domain.notification.service.NotificationService; import com.jiwon.mylog.domain.notification.entity.NotificationType; -import com.jiwon.mylog.domain.user.entity.User; -import com.jiwon.mylog.domain.user.repository.UserRepository; -import com.jiwon.mylog.global.common.error.ErrorCode; -import com.jiwon.mylog.global.common.error.exception.NotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; @Slf4j @RequiredArgsConstructor @@ -24,33 +20,29 @@ public class NotificationEventListener { private final NotificationService notificationService; - private final NotificationRepository notificationRepository; - private final UserRepository userRepository; - @Transactional - @EventListener + @Async("Async") + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleCommentCreated(CommentCreatedEvent event) { - if (event.postWriterId().equals(event.commentWriterId())) { - return; - } - - try { - handleNotification( - event.postWriterId(), - event.commentWriterName() + "왹이 외계 수집물에 발 사를 남기다!", - "/posts/" + event.postId(), - NotificationType.COMMENT - ); - } catch (Exception e) { - log.warn("댓글 생성 이벤트 알림 처리 실패: {}", event, e); + for (Long receiverId : event.receiverIds()) { + try { + notificationService.saveNotification( + receiverId, + event.commentWriterName() + "왹이 외계 수집물에 발 사를 남기다!", + "/posts/" + event.postId(), + NotificationType.COMMENT + ); + } catch (Exception e) { + log.warn("댓글 생성 이벤트 알림 처리 실패: {}", event, e); + } } } - @Transactional - @EventListener + @Async("Async") + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleLikeCreated(LikeCreatedEvent event) { try { - handleNotification( + notificationService.saveNotification( event.receiverId(), event.senderName() + "왹이 외계 수집물에 푸 딩을 달았다!", "/posts/" + event.targetId(), @@ -61,11 +53,11 @@ public void handleLikeCreated(LikeCreatedEvent event) { } } - @Transactional - @EventListener + @Async("Async") + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleFollowCreated(FollowCreatedEvent event) { try { - handleNotification( + notificationService.saveNotification( event.receiverId(), event.followerName() + "왹이 잡 았다! 너 잡혔다!", "/" + event.followerId(), @@ -76,37 +68,26 @@ public void handleFollowCreated(FollowCreatedEvent event) { } } - @Transactional - @EventListener + @Async("Async") + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleUnFollowCreated(FollowDeletedEvent event) { try { - handleNotification( + notificationService.saveNotification( event.receiverId(), "오오자비로운" + event.followerName() + "왹께서널놓아주시니", "/" + event.followerId(), - NotificationType.FOLLOW + NotificationType.UNFOLLOW ); } catch (Exception e) { log.warn("언팔로우 이벤트 알림 처리 실패: {}", event, e); } } - private void handleNotification(Long receiverId, String content, String url, NotificationType type) { - User receiver = getReceiver(receiverId); - Notification notification = Notification.create(receiver, content, url, type); - notificationRepository.save(notification); - - sendSSE(receiverId, notification); - } - - private User getReceiver(Long receiverId) { - return userRepository.findById(receiverId) - .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_USER)); - } - - private void sendSSE(Long receiverId, Notification notification) { + @Async("Async") + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void sendSSE(NotificationSendEvent event) { try { - notificationService.sendNotification(receiverId, notification); + notificationService.sendNotification(event.receiverId(), event.content()); } catch(Exception e) { log.warn("SSE 전송 실패"); } diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/NotificationSendEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/NotificationSendEvent.java new file mode 100644 index 0000000..0623512 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/event/dto/NotificationSendEvent.java @@ -0,0 +1,7 @@ +package com.jiwon.mylog.domain.event.dto; + +public record NotificationSendEvent( + Long receiverId, + String content +) { +} diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/comment/CommentCreatedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/comment/CommentCreatedEvent.java index c3898dc..0b7ae95 100644 --- a/src/main/java/com/jiwon/mylog/domain/event/dto/comment/CommentCreatedEvent.java +++ b/src/main/java/com/jiwon/mylog/domain/event/dto/comment/CommentCreatedEvent.java @@ -1,9 +1,11 @@ package com.jiwon.mylog.domain.event.dto.comment; import java.time.LocalDateTime; +import java.util.Set; public record CommentCreatedEvent( Long postId, Long postWriterId, + Set receiverIds, Long commentId, Long commentWriterId, String commentWriterName, LocalDateTime createdAt) { } diff --git a/src/main/java/com/jiwon/mylog/domain/notification/service/NotificationService.java b/src/main/java/com/jiwon/mylog/domain/notification/service/NotificationService.java index 2d7f5d8..5acfc76 100644 --- a/src/main/java/com/jiwon/mylog/domain/notification/service/NotificationService.java +++ b/src/main/java/com/jiwon/mylog/domain/notification/service/NotificationService.java @@ -1,15 +1,22 @@ package com.jiwon.mylog.domain.notification.service; +import com.jiwon.mylog.domain.event.dto.NotificationSendEvent; import com.jiwon.mylog.domain.notification.dto.NotificationCountResponse; import com.jiwon.mylog.domain.notification.dto.NotificationResponse; import com.jiwon.mylog.domain.notification.entity.Notification; +import com.jiwon.mylog.domain.notification.entity.NotificationType; import com.jiwon.mylog.domain.notification.repository.NotificationRepository; import com.jiwon.mylog.domain.notification.repository.SseEmitterRepository; +import com.jiwon.mylog.domain.user.entity.User; +import com.jiwon.mylog.domain.user.repository.UserRepository; import com.jiwon.mylog.global.common.entity.PageResponse; +import com.jiwon.mylog.global.common.error.ErrorCode; +import com.jiwon.mylog.global.common.error.exception.NotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -26,8 +33,20 @@ public class NotificationService { private static final Long DEFAULT_TIMEOUT = 30 * 60 * 1000L; + private final ApplicationEventPublisher eventPublisher; + private final SseEmitterRepository emitterRepository; private final NotificationRepository notificationRepository; + private final UserRepository userRepository; + + @Transactional + public void saveNotification(Long receiverId, String content, String url, NotificationType type) { + User receiver = getReceiver(receiverId); + Notification notification = Notification.create(receiver, content, url, type); + notificationRepository.save(notification); + + eventPublisher.publishEvent(new NotificationSendEvent(receiverId, content)); + } @CacheEvict(value = "notification::count", key="'userId:' + #userId", condition = "#userId != null") @Transactional @@ -71,7 +90,7 @@ public SseEmitter subscribe(Long userId) { return emitter; } - public void sendNotification(Long userId, Notification notification) { + public void sendNotification(Long userId, String content) { List emitterIds = emitterRepository.findEmitterIdsByUserId(userId); for (String emitterId : emitterIds) { @@ -82,7 +101,7 @@ public void sendNotification(Long userId, Notification notification) { } log.info("notification: {}", emitterId); - send(emitter, emitterId, "notification", notification.getContent()); + send(emitter, emitterId, "notification", content); log.info("notification: {} 성공", emitterId); } } @@ -99,4 +118,9 @@ private void send(SseEmitter emitter, String emitterId, String name, String cont throw new RuntimeException(); } } + + private User getReceiver(Long receiverId) { + return userRepository.findById(receiverId) + .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_USER)); + } } diff --git a/src/main/java/com/jiwon/mylog/global/common/config/AsyncConfig.java b/src/main/java/com/jiwon/mylog/global/common/config/AsyncConfig.java index f738a7d..ef1019e 100644 --- a/src/main/java/com/jiwon/mylog/global/common/config/AsyncConfig.java +++ b/src/main/java/com/jiwon/mylog/global/common/config/AsyncConfig.java @@ -12,7 +12,7 @@ @Configuration public class AsyncConfig implements AsyncConfigurer { - @Bean("mailExecutor") + @Bean("Async") public Executor customTaskExecutor() { ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor(); ex.setCorePoolSize(5); diff --git a/src/main/java/com/jiwon/mylog/global/mail/service/MailService.java b/src/main/java/com/jiwon/mylog/global/mail/service/MailService.java index 2de41aa..e151b75 100644 --- a/src/main/java/com/jiwon/mylog/global/mail/service/MailService.java +++ b/src/main/java/com/jiwon/mylog/global/mail/service/MailService.java @@ -41,7 +41,7 @@ public void verifyEmailCode(String email, String code) { redisUtil.delete(email); } - @Async("mailExecutor") + @Async("Async") public void sendCodeMailAsync(String email) { if (redisUtil.exist(email)) { redisUtil.delete(email);