Skip to content

Commit 6966b18

Browse files
committed
fix notification async dispatch
1 parent 9792528 commit 6966b18

9 files changed

Lines changed: 630 additions & 200 deletions

ontime-back/src/main/java/devkor/ontime_back/config/SchedulerConfig.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
import org.springframework.context.annotation.Bean;
44
import org.springframework.context.annotation.Configuration;
55
import org.springframework.scheduling.TaskScheduler;
6+
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
67
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
78

9+
import java.util.concurrent.Executor;
10+
811
@Configuration
912
public class SchedulerConfig {
1013

@@ -16,4 +19,15 @@ public TaskScheduler taskScheduler() {
1619
scheduler.initialize();
1720
return scheduler;
1821
}
19-
}
22+
23+
@Bean(name = "notificationAsyncExecutor")
24+
public Executor notificationAsyncExecutor() {
25+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
26+
executor.setCorePoolSize(4);
27+
executor.setMaxPoolSize(8);
28+
executor.setQueueCapacity(200);
29+
executor.setThreadNamePrefix("notification-");
30+
executor.initialize();
31+
return executor;
32+
}
33+
}

ontime-back/src/main/java/devkor/ontime_back/repository/NotificationScheduleRepository.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import devkor.ontime_back.entity.NotificationSchedule;
44
import org.springframework.data.jpa.repository.JpaRepository;
55
import org.springframework.data.jpa.repository.Query;
6+
import org.springframework.data.repository.query.Param;
67
import org.springframework.stereotype.Repository;
78

89
import java.time.LocalDateTime;
910
import java.util.List;
11+
import java.util.Optional;
1012
import java.util.UUID;
1113

1214
@Repository
@@ -17,5 +19,11 @@ public interface NotificationScheduleRepository extends JpaRepository<Notificati
1719
"WHERE n.notificationTime > :now AND n.isSent = false")
1820
List<NotificationSchedule> findAllWithScheduleAndUser(LocalDateTime now);
1921

22+
@Query("SELECT n FROM NotificationSchedule n " +
23+
"JOIN FETCH n.schedule s " +
24+
"JOIN FETCH s.user " +
25+
"WHERE n.id = :notificationId")
26+
Optional<NotificationSchedule> findByIdWithScheduleAndUser(@Param("notificationId") Long notificationId);
27+
2028
List<NotificationSchedule> findAllByScheduleScheduleIdOrderByIdAsc(UUID scheduleId);
2129
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package devkor.ontime_back.service;
2+
3+
import com.google.firebase.messaging.FirebaseMessaging;
4+
import com.google.firebase.messaging.Message;
5+
import devkor.ontime_back.entity.NotificationSchedule;
6+
import devkor.ontime_back.entity.Schedule;
7+
import devkor.ontime_back.entity.User;
8+
import devkor.ontime_back.entity.UserSetting;
9+
import devkor.ontime_back.repository.NotificationScheduleRepository;
10+
import devkor.ontime_back.repository.UserSettingRepository;
11+
import lombok.RequiredArgsConstructor;
12+
import lombok.extern.slf4j.Slf4j;
13+
import org.springframework.stereotype.Service;
14+
import org.springframework.transaction.annotation.Transactional;
15+
16+
import java.util.List;
17+
18+
@Slf4j
19+
@Service
20+
@RequiredArgsConstructor
21+
@Transactional(readOnly = true)
22+
public class NotificationDeliveryService {
23+
24+
private final UserSettingRepository userSettingRepository;
25+
private final AlarmService alarmService;
26+
private final NotificationScheduleRepository notificationScheduleRepository;
27+
28+
@Transactional
29+
public void sendReminder(NotificationSchedule notificationSchedule, String message) {
30+
Long userId = notificationSchedule.getSchedule().getUser().getId();
31+
32+
if (userId != null) {
33+
UserSetting userSetting = userSettingRepository.findByUserId(userId)
34+
.orElseThrow(() -> new IllegalArgumentException("No UserSetting found in schedule's user"));
35+
log.debug("사용자 알림 전송 설정 여부: " + userSetting.getIsNotificationsEnabled());
36+
37+
if (Boolean.TRUE.equals(userSetting.getIsNotificationsEnabled())) {
38+
if (alarmService.shouldSuppressLegacyReminder(
39+
userId,
40+
notificationSchedule.getSchedule().getScheduleId(),
41+
notificationSchedule.getNotificationTime())) {
42+
log.info("현재 기기 로컬 알람 커버리지로 인해 레거시 푸시 알림을 생략합니다. scheduleId={}",
43+
notificationSchedule.getSchedule().getScheduleId());
44+
return;
45+
}
46+
sendNotificationToUser(notificationSchedule.getSchedule(), message);
47+
notificationSchedule.changeStatusToSent();
48+
notificationScheduleRepository.save(notificationSchedule);
49+
}
50+
}
51+
}
52+
53+
public void sendReminder(List<Schedule> schedules, String message) {
54+
for (Schedule schedule : schedules) {
55+
User user = schedule.getUser();
56+
Long userId = user.getId();
57+
58+
if (userId != null) {
59+
UserSetting userSetting = userSettingRepository.findByUserId(userId)
60+
.orElseThrow(() -> new IllegalArgumentException("No UserSetting found in schedule's user"));
61+
62+
if (userSetting != null && userSetting.getIsNotificationsEnabled()) {
63+
sendNotificationToUser(schedule, message);
64+
}
65+
}
66+
}
67+
}
68+
69+
public void sendNotificationToUser(Schedule schedule, String message) {
70+
User user = schedule.getUser();
71+
String firebaseToken = user.getFirebaseToken();
72+
73+
Message firebaseMessage = Message.builder()
74+
.putData("title", "약속 알림")
75+
.putData("content", user.getName() + "님 " + message + "\n약속명: " + schedule.getScheduleName())
76+
.setToken(firebaseToken)
77+
.build();
78+
79+
try {
80+
FirebaseMessaging.getInstance().send(firebaseMessage);
81+
log.info("Firebase에 성공적으로 push notification 요청을 보냈으며, Firebase로부터 적절한 응답을 받았습니다 \n알림 푸시한 약속:" + schedule.getScheduleName());
82+
} catch (Exception e) {
83+
e.printStackTrace();
84+
}
85+
}
86+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package devkor.ontime_back.service;
2+
3+
import devkor.ontime_back.entity.NotificationSchedule;
4+
import devkor.ontime_back.repository.NotificationScheduleRepository;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.scheduling.annotation.Async;
8+
import org.springframework.stereotype.Service;
9+
import org.springframework.transaction.annotation.Transactional;
10+
11+
@Slf4j
12+
@Service
13+
@RequiredArgsConstructor
14+
public class NotificationDispatchService {
15+
16+
private final NotificationScheduleRepository notificationScheduleRepository;
17+
private final NotificationDeliveryService notificationDeliveryService;
18+
19+
@Async("notificationAsyncExecutor")
20+
@Transactional
21+
public void dispatchReminder(Long notificationId, String message) {
22+
NotificationSchedule notificationSchedule = notificationScheduleRepository.findByIdWithScheduleAndUser(notificationId)
23+
.orElse(null);
24+
if (notificationSchedule == null) {
25+
log.warn("예약된 알림을 찾을 수 없어 푸시 알림을 건너뜁니다. notificationId={}", notificationId);
26+
return;
27+
}
28+
29+
notificationDeliveryService.sendReminder(notificationSchedule, message);
30+
}
31+
}
Lines changed: 14 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
11
package devkor.ontime_back.service;
22

3-
import com.google.firebase.messaging.FirebaseMessaging;
4-
import com.google.firebase.messaging.Message;
53
import devkor.ontime_back.entity.NotificationSchedule;
64
import devkor.ontime_back.entity.Schedule;
7-
import devkor.ontime_back.entity.User;
8-
import devkor.ontime_back.entity.UserSetting;
9-
import devkor.ontime_back.repository.NotificationScheduleRepository;
10-
import devkor.ontime_back.repository.UserSettingRepository;
115
import lombok.RequiredArgsConstructor;
126
import lombok.extern.slf4j.Slf4j;
137
import org.springframework.scheduling.TaskScheduler;
14-
import org.springframework.scheduling.annotation.Async;
15-
import org.springframework.scheduling.annotation.EnableAsync;
168
import org.springframework.stereotype.Service;
17-
import org.springframework.transaction.annotation.Transactional;
189

1910
import java.time.LocalDateTime;
2011
import java.time.ZoneId;
@@ -26,13 +17,11 @@
2617
@Slf4j
2718
@Service
2819
@RequiredArgsConstructor
29-
@Transactional(readOnly = true)
3020
public class NotificationService {
3121

32-
private final UserSettingRepository userSettingRepository;
33-
private final AlarmService alarmService;
3422
private final TaskScheduler taskScheduler;
35-
private final NotificationScheduleRepository notificationScheduleRepository;
23+
private final NotificationDispatchService notificationDispatchService;
24+
private final NotificationDeliveryService notificationDeliveryService;
3625
private final ConcurrentHashMap<Long, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
3726

3827
public void scheduleReminder(NotificationSchedule notificationSchedule) {
@@ -43,12 +32,19 @@ public void scheduleReminder(NotificationSchedule notificationSchedule) {
4332
return;
4433
}
4534

35+
Long notificationId = notificationSchedule.getId();
36+
if (notificationId == null) {
37+
throw new IllegalArgumentException("NotificationSchedule must be persisted before scheduling");
38+
}
39+
4640
ScheduledFuture<?> future = taskScheduler.schedule(
47-
() -> sendReminder(notificationSchedule, "준비 시작해야 합니다.(현재 시각: 약속시각 - (여유시간 + 이동시간 + 총준비시간) )"),
41+
() -> notificationDispatchService.dispatchReminder(
42+
notificationId,
43+
"준비 시작해야 합니다.(현재 시각: 약속시각 - (여유시간 + 이동시간 + 총준비시간) )"),
4844
Date.from(reminderTime.atZone(ZoneId.systemDefault()).toInstant())
4945
);
5046

51-
scheduledTasks.put(notificationSchedule.getId(), future);
47+
scheduledTasks.put(notificationId, future);
5248

5349
log.info("스케줄 등록 완료 {} ({})", notificationSchedule.getSchedule().getScheduleName(), reminderTime);
5450
}
@@ -62,64 +58,15 @@ public void cancelScheduledNotification(Long notificationId) {
6258
}
6359
}
6460

65-
@Async
66-
@Transactional
6761
public void sendReminder(NotificationSchedule notificationSchedule, String message) {
68-
Long userId = notificationSchedule.getSchedule().getUser().getId();
69-
70-
if (userId != null) {
71-
UserSetting userSetting = userSettingRepository.findByUserId(userId)
72-
.orElseThrow(() -> new IllegalArgumentException("No UserSetting found in schedule's user"));
73-
log.debug("사용자 알림 전송 설정 여부: " + userSetting.getIsNotificationsEnabled());
74-
75-
if (Boolean.TRUE.equals(userSetting.getIsNotificationsEnabled())) {
76-
if (alarmService.shouldSuppressLegacyReminder(
77-
userId,
78-
notificationSchedule.getSchedule().getScheduleId(),
79-
notificationSchedule.getNotificationTime())) {
80-
log.info("현재 기기 로컬 알람 커버리지로 인해 레거시 푸시 알림을 생략합니다. scheduleId={}",
81-
notificationSchedule.getSchedule().getScheduleId());
82-
return;
83-
}
84-
sendNotificationToUser(notificationSchedule.getSchedule(), message);
85-
notificationSchedule.changeStatusToSent();
86-
notificationScheduleRepository.save(notificationSchedule);
87-
}
88-
}
62+
notificationDeliveryService.sendReminder(notificationSchedule, message);
8963
}
9064

9165
public void sendReminder(List<Schedule> schedules, String message) {
92-
for (Schedule schedule : schedules) {
93-
User user = schedule.getUser();
94-
Long userId = user.getId();
95-
96-
if (userId != null) {
97-
UserSetting userSetting = userSettingRepository.findByUserId(userId)
98-
.orElseThrow(() -> new IllegalArgumentException("No UserSetting found in schedule's user"));// Repository 메서드 가정
99-
100-
if (userSetting != null && userSetting.getIsNotificationsEnabled()) {
101-
sendNotificationToUser(schedule, message);
102-
}
103-
}
104-
}
66+
notificationDeliveryService.sendReminder(schedules, message);
10567
}
10668

107-
@Transactional
10869
public void sendNotificationToUser(Schedule schedule, String message) {
109-
User user = schedule.getUser();
110-
String firebaseToken = user.getFirebaseToken();
111-
112-
Message firebaseMessage = Message.builder()
113-
.putData("title", "약속 알림")
114-
.putData("content", user.getName() + "님 " + message + "\n약속명: " + schedule.getScheduleName())
115-
.setToken(firebaseToken)
116-
.build();
117-
118-
try {
119-
FirebaseMessaging.getInstance().send(firebaseMessage);
120-
log.info("Firebase에 성공적으로 push notification 요청을 보냈으며, Firebase로부터 적절한 응답을 받았습니다 \n알림 푸시한 약속:" + schedule.getScheduleName());
121-
} catch (Exception e) {
122-
e.printStackTrace();
123-
}
70+
notificationDeliveryService.sendNotificationToUser(schedule, message);
12471
}
12572
}

0 commit comments

Comments
 (0)