diff --git a/src/main/java/in/koreatech/koin/domain/notification/repository/NotificationJdbcRepository.java b/src/main/java/in/koreatech/koin/domain/notification/repository/NotificationJdbcRepository.java new file mode 100644 index 000000000..aad7c069b --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/notification/repository/NotificationJdbcRepository.java @@ -0,0 +1,60 @@ +package in.koreatech.koin.domain.notification.repository; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import in.koreatech.koin.domain.notification.model.Notification; +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class NotificationJdbcRepository { + + private static final int BATCH_SIZE = 1_000; + + private final JdbcTemplate jdbcTemplate; + + public void batchInsert(List notifications) { + if (notifications.isEmpty()) { + return; + } + + String sql = """ + INSERT INTO notification ( + app_path, + scheme_uri, + title, + message, + image_url, + type, + users_id, + is_read, + created_at, + updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """; + + LocalDateTime now = LocalDateTime.now(); + + jdbcTemplate.batchUpdate( + sql, + notifications, + BATCH_SIZE, + (preparedStatement, notification) -> { + preparedStatement.setString(1, notification.getMobileAppPath().name()); + preparedStatement.setString(2, notification.getSchemeUri()); + preparedStatement.setString(3, notification.getTitle()); + preparedStatement.setString(4, notification.getMessage()); + preparedStatement.setString(5, notification.getImageUrl()); + preparedStatement.setString(6, notification.getType().toUpperCase()); + preparedStatement.setInt(7, notification.getUser().getId()); + preparedStatement.setBoolean(8, notification.isRead()); + preparedStatement.setObject(9, now); + preparedStatement.setObject(10, now); + } + ); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/notification/service/NotificationService.java b/src/main/java/in/koreatech/koin/domain/notification/service/NotificationService.java index c2fcead70..891d7c034 100644 --- a/src/main/java/in/koreatech/koin/domain/notification/service/NotificationService.java +++ b/src/main/java/in/koreatech/koin/domain/notification/service/NotificationService.java @@ -16,7 +16,7 @@ import in.koreatech.koin.domain.notification.model.NotificationDetailSubscribeType; import in.koreatech.koin.domain.notification.model.NotificationSubscribe; import in.koreatech.koin.domain.notification.model.NotificationSubscribeType; -import in.koreatech.koin.domain.notification.repository.NotificationRepository; +import in.koreatech.koin.domain.notification.repository.NotificationJdbcRepository; import in.koreatech.koin.domain.notification.repository.NotificationSubscribeRepository; import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.domain.user.repository.UserRepository; @@ -34,18 +34,26 @@ public class NotificationService { public record NotificationDeliveryResult(Notification notification, boolean delivered) {} private final UserRepository userRepository; - private final NotificationRepository notificationRepository; private final NotificationPersistenceService notificationPersistenceService; private final FcmClient fcmClient; private final NotificationSubscribeRepository notificationSubscribeRepository; + private final NotificationJdbcRepository notificationJdbcRepository; @Transactional public void pushNotifications(List notifications) { if (notifications.isEmpty()) { return; } - notificationRepository.saveAll(notifications); - runAfterCommit(() -> notifications.forEach(this::sendNotificationSafely)); + notificationJdbcRepository.batchInsert(notifications); + notifications.forEach(notification -> fcmClient.sendMessage( + notification.getUser().getDeviceToken(), + notification.getTitle(), + notification.getMessage(), + notification.getImageUrl(), + notification.getMobileAppPath(), + notification.getSchemeUri(), + notification.getType().toLowerCase() + )); } @Transactional diff --git a/src/test/java/in/koreatech/koin/unit/domain/notification/service/NotificationServiceTest.java b/src/test/java/in/koreatech/koin/unit/domain/notification/service/NotificationServiceTest.java index 7e5172853..693125744 100644 --- a/src/test/java/in/koreatech/koin/unit/domain/notification/service/NotificationServiceTest.java +++ b/src/test/java/in/koreatech/koin/unit/domain/notification/service/NotificationServiceTest.java @@ -3,26 +3,21 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.InOrder; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import in.koreatech.koin.domain.notification.model.Notification; import in.koreatech.koin.domain.notification.model.NotificationFactory; -import in.koreatech.koin.domain.notification.repository.NotificationRepository; +import in.koreatech.koin.domain.notification.repository.NotificationJdbcRepository; import in.koreatech.koin.domain.notification.repository.NotificationSubscribeRepository; import in.koreatech.koin.domain.notification.service.NotificationPersistenceService; import in.koreatech.koin.domain.notification.service.NotificationService; @@ -40,9 +35,6 @@ class NotificationServiceTest { @Mock private UserRepository userRepository; - @Mock - private NotificationRepository notificationRepository; - @Mock private NotificationPersistenceService notificationPersistenceService; @@ -55,6 +47,9 @@ class NotificationServiceTest { @Mock private NotificationFactory notificationFactory; + @Mock + private NotificationJdbcRepository notificationJdbcRepository; + @Test @DisplayName("알림 전송 결과 조회는 전송 성공 시에만 알림 레코드를 저장한다.") void pushNotificationsWithResult_whenDelivered_savesNotification() { @@ -137,12 +132,11 @@ void pushNotification_savesNotificationBeforeSend() { notificationService.pushNotification(notification); - InOrder inOrder = inOrder(notificationRepository, fcmClient); - inOrder.verify(notificationRepository).saveAll(List.of(notification)); + InOrder inOrder = inOrder(notificationJdbcRepository, fcmClient); + inOrder.verify(notificationJdbcRepository).batchInsert(List.of(notification)); inOrder.verify(fcmClient).sendMessage( anyString(), anyString(), anyString(), any(), any(), anyString(), anyString() ); - verify(notificationRepository, never()).save(notification); } private Notification createNotification(String deviceToken) {