diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/service/scheduler/NotificationCleanupExecutor.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/service/scheduler/NotificationCleanupExecutor.java new file mode 100644 index 00000000..3683d321 --- /dev/null +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/service/scheduler/NotificationCleanupExecutor.java @@ -0,0 +1,31 @@ +package com.whereyouad.WhereYouAd.domains.notification.domain.service.scheduler; + +import com.whereyouad.WhereYouAd.domains.notification.persistence.repository.NotificationDeliveryRepository; +import com.whereyouad.WhereYouAd.domains.notification.persistence.repository.NotificationRepository; +import com.whereyouad.WhereYouAd.domains.notification.persistence.repository.UserNotificationRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class NotificationCleanupExecutor { + + private final NotificationRepository notificationRepository; + private final UserNotificationRepository userNotificationRepository; + private final NotificationDeliveryRepository notificationDeliveryRepository; + + @Transactional + public void deleteNotificationChunk(List notificationIds) { + int userNotificationCount = userNotificationRepository.deleteByNotificationIdIn(notificationIds); + int deliveryCount = notificationDeliveryRepository.deleteByNotificationIdIn(notificationIds); + notificationRepository.deleteAllByIdInBatch(notificationIds); + + log.debug("만료 알림 청크 삭제 - 알림(Notification): {}건, 회원 알림 내역(UserNotification): {}건, 발송 이력(NotificationDelivery): {}건", + notificationIds.size(), userNotificationCount, deliveryCount); + } +} diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/service/scheduler/NotificationCleanupScheduler.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/service/scheduler/NotificationCleanupScheduler.java new file mode 100644 index 00000000..cb5a870d --- /dev/null +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/service/scheduler/NotificationCleanupScheduler.java @@ -0,0 +1,50 @@ +package com.whereyouad.WhereYouAd.domains.notification.domain.service.scheduler; + +import com.whereyouad.WhereYouAd.domains.notification.persistence.repository.NotificationRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class NotificationCleanupScheduler { + + private static final int RETENTION_DAYS = 30; + private static final int CHUNK_SIZE = 1000; + + private final NotificationRepository notificationRepository; + private final NotificationCleanupExecutor notificationCleanupExecutor; + + @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") + public void deleteOldNotifications() { + // 스케줄러 실행 시점으로 부터 만료 기준일 (30일 과거) 설정 + LocalDateTime threshold = LocalDate.now(ZoneId.of("Asia/Seoul")).minusDays(RETENTION_DAYS).atStartOfDay(); + log.info("오래된 알림 내역 삭제 스케줄러 실행 - threshold: {}", threshold); + + int totalDeleted = 0; + while (true) { + // 삭제 대상인 알림 내역 (30일 이상 지난 내역) Id 를 List 조회 + List notificationIds = + notificationRepository.findIdsByCreatedAtBefore(threshold, PageRequest.of(0, CHUNK_SIZE)); + + // 삭제 대상인 알림 내역이 없으면 종료 + if (notificationIds.isEmpty()) { + break; + } + + // 청크 단위 알림 내역 삭제 + notificationCleanupExecutor.deleteNotificationChunk(notificationIds); + totalDeleted += notificationIds.size(); + } + + log.info("오래된 알림 내역 삭제 완료 - 삭제 알림 갯수: {}", totalDeleted); + } +} diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/Notification.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/Notification.java index 33afbfe8..6fe0bc34 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/Notification.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/Notification.java @@ -4,13 +4,18 @@ import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.Organization; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; @Entity -@Table(name = "notification") +@Table(name = "notification", indexes = { + // 오래된 알림 삭제 스케줄러의 청크 조회용 인덱스 + @Index(name = "idx_notification_created_at_id", columnList = "created_at, notification_id") +}) @EntityListeners(AuditingEntityListener.class) @Getter @Builder @@ -42,6 +47,7 @@ public class Notification { // 연관 관계 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "org_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) private Organization organization; @Enumerated(EnumType.STRING) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/NotificationDelivery.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/NotificationDelivery.java index dcfbd545..f183622d 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/NotificationDelivery.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/NotificationDelivery.java @@ -48,6 +48,7 @@ public class NotificationDelivery extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "notification_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) private Notification notification; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/OrgNotificationSetting.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/OrgNotificationSetting.java index 63a7adf2..87ba7e74 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/OrgNotificationSetting.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/OrgNotificationSetting.java @@ -5,6 +5,8 @@ import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import org.springframework.util.StringUtils; @Entity @@ -22,6 +24,7 @@ public class OrgNotificationSetting extends BaseEntity { @OneToOne(fetch = FetchType.LAZY) @MapsId // PK == FK(식별 관계) @JoinColumn(name = "org_id") + @OnDelete(action = OnDeleteAction.CASCADE) private Organization organization; @Column(name = "slack_webhook_url", length = 512) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/UserNotification.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/UserNotification.java index 96b1701d..63a4cdcd 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/UserNotification.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/UserNotification.java @@ -4,6 +4,8 @@ import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import java.time.LocalDateTime; @@ -30,10 +32,12 @@ public class UserNotification { // 연관관계 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "notification_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) private Notification notification; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) private User user; // 알림 읽음 처리 diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/repository/NotificationDeliveryRepository.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/repository/NotificationDeliveryRepository.java index 0db71843..4a05e798 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/repository/NotificationDeliveryRepository.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/repository/NotificationDeliveryRepository.java @@ -2,8 +2,17 @@ import com.whereyouad.WhereYouAd.domains.notification.persistence.entity.NotificationDelivery; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface NotificationDeliveryRepository extends JpaRepository { + // 만료 알림 정리용 - 발송 이력 벌크 삭제 + @Modifying + @Query("DELETE FROM NotificationDelivery nd WHERE nd.notification.id IN :notificationIds") + int deleteByNotificationIdIn(@Param("notificationIds") List notificationIds); } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/repository/NotificationRepository.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/repository/NotificationRepository.java index 20aa94fe..ac4ad439 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/repository/NotificationRepository.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/repository/NotificationRepository.java @@ -1,7 +1,17 @@ package com.whereyouad.WhereYouAd.domains.notification.persistence.repository; import com.whereyouad.WhereYouAd.domains.notification.persistence.entity.Notification; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; public interface NotificationRepository extends JpaRepository { + + // 보관 기간이 지난 알림 id 조회 (청크 단위로 끊어서 조회) + @Query("SELECT n.id FROM Notification n WHERE n.createdAt < :threshold ORDER BY n.createdAt, n.id") + List findIdsByCreatedAtBefore(@Param("threshold") LocalDateTime threshold, Pageable pageable); } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/repository/UserNotificationRepository.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/repository/UserNotificationRepository.java index dfeaa872..2032ff28 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/repository/UserNotificationRepository.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/repository/UserNotificationRepository.java @@ -9,6 +9,7 @@ import org.springframework.data.repository.query.Param; import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; public interface UserNotificationRepository extends JpaRepository { @@ -56,4 +57,8 @@ int markAllAsRead(@Param("userId") Long userId, @Param("orgId") Long orgId, @Param("now") LocalDateTime now); + // 만료 알림 정리용 - 회원별 알림 내역 벌크 삭제 + @Modifying + @Query("DELETE FROM UserNotification un WHERE un.notification.id IN :notificationIds") + int deleteByNotificationIdIn(@Param("notificationIds") List notificationIds); }