diff --git a/build.gradle b/build.gradle index 17e936c..232762e 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,8 @@ dependencies { // JPA / MySQL implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'com.mysql:mysql-connector-j' + implementation 'net.javacrumbs.shedlock:shedlock-spring:7.7.0' + implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:7.7.0' // Security / JWT implementation 'org.springframework.boot:spring-boot-starter-security' diff --git a/database/migrations/20260813_01_shedlock.sql b/database/migrations/20260813_01_shedlock.sql new file mode 100644 index 0000000..795477b --- /dev/null +++ b/database/migrations/20260813_01_shedlock.sql @@ -0,0 +1,7 @@ +CREATE TABLE shedlock ( + name VARCHAR(64) NOT NULL, + lock_until TIMESTAMP(3) NOT NULL, + locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + locked_by VARCHAR(255) NOT NULL, + PRIMARY KEY (name) +); diff --git a/src/main/java/com/example/todayEng/domain/notification/scheduler/DiaryReminderScheduler.java b/src/main/java/com/example/todayEng/domain/notification/scheduler/DiaryReminderScheduler.java index b0aad69..78103c3 100644 --- a/src/main/java/com/example/todayEng/domain/notification/scheduler/DiaryReminderScheduler.java +++ b/src/main/java/com/example/todayEng/domain/notification/scheduler/DiaryReminderScheduler.java @@ -5,6 +5,7 @@ import java.time.ZoneId; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -22,6 +23,11 @@ public class DiaryReminderScheduler { cron = "${notification.diary-reminder.cron:0 0 22 * * *}", zone = "${notification.diary-reminder.zone:Asia/Seoul}" ) + @SchedulerLock( + name = "diaryReminderScheduler", + lockAtLeastFor = "1m", + lockAtMostFor = "1h" + ) public void sendDiaryReminders() { LocalDate today = LocalDate.now(ZoneId.of(reminderZone)); diff --git a/src/main/java/com/example/todayEng/domain/notification/service/NotificationService.java b/src/main/java/com/example/todayEng/domain/notification/service/NotificationService.java index ea5f38f..2c512b4 100644 --- a/src/main/java/com/example/todayEng/domain/notification/service/NotificationService.java +++ b/src/main/java/com/example/todayEng/domain/notification/service/NotificationService.java @@ -49,6 +49,12 @@ public void sendTestNotification(Long userId) { "테스트 알림입니다.", "/home" ); + + log.info( + "테스트 알림 발송 성공. notificationSettingId={}, userId={}", + notificationSetting.getId(), + userId + ); } public void sendDiaryReminders(LocalDate today) { @@ -70,6 +76,12 @@ public void sendDiaryReminders(LocalDate today) { "오늘 하루를 영어로 천천히 돌아보세요.", "/home" ); + + log.info( + "회고 알림 발송 성공. notificationSettingId={}, userId={}", + target.notificationSettingId(), + target.userId() + ); } catch (PushSubscriptionExpiredException exception) { try { pushSubscriptionCleanupService.clearExpiredSubscription( @@ -99,4 +111,4 @@ public void sendDiaryReminders(LocalDate today) { } } } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/todayEng/domain/notification/service/WebPushService.java b/src/main/java/com/example/todayEng/domain/notification/service/WebPushService.java index 4f86611..0477852 100644 --- a/src/main/java/com/example/todayEng/domain/notification/service/WebPushService.java +++ b/src/main/java/com/example/todayEng/domain/notification/service/WebPushService.java @@ -8,15 +8,18 @@ import java.io.IOException; import java.security.GeneralSecurityException; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import nl.martijndwars.webpush.Notification; import nl.martijndwars.webpush.PushService; -import org.apache.http.HttpResponse; import org.jose4j.lang.JoseException; import org.springframework.stereotype.Service; import nl.martijndwars.webpush.Encoding; import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +@Slf4j @Service @RequiredArgsConstructor public class WebPushService { @@ -98,21 +101,27 @@ private void send( Encoding.AES128GCM ); - HttpResponse response = - webPushHttpClient.execute(request); + try (CloseableHttpResponse response = + webPushHttpClient.execute(request)) { + int statusCode = + response.getStatusLine().getStatusCode(); + EntityUtils.consume(response.getEntity()); - int statusCode = - response.getStatusLine().getStatusCode(); + if (statusCode == NOT_FOUND_STATUS + || statusCode == GONE_STATUS) { + throw new PushSubscriptionExpiredException(); + } - if (statusCode == NOT_FOUND_STATUS - || statusCode == GONE_STATUS) { - throw new PushSubscriptionExpiredException(); - } + if (statusCode < 200 || statusCode >= 300) { + throw new IllegalStateException( + "Web Push 전송에 실패했습니다. status=" + + statusCode + ); + } - if (statusCode < 200 || statusCode >= 300) { - throw new IllegalStateException( - "Web Push 전송에 실패했습니다. status=" - + statusCode + log.info( + "Web Push Provider 응답 성공. status={}", + statusCode ); } } catch ( @@ -126,4 +135,4 @@ private void send( ); } } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/todayEng/global/config/SchedulingConfig.java b/src/main/java/com/example/todayEng/global/config/SchedulingConfig.java index 6902907..c7748ed 100644 --- a/src/main/java/com/example/todayEng/global/config/SchedulingConfig.java +++ b/src/main/java/com/example/todayEng/global/config/SchedulingConfig.java @@ -1,9 +1,26 @@ package com.example.todayEng.global.config; +import javax.sql.DataSource; +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.scheduling.annotation.EnableScheduling; @Configuration @EnableScheduling +@EnableSchedulerLock(defaultLockAtMostFor = "1h") public class SchedulingConfig { + + @Bean + public LockProvider lockProvider(DataSource dataSource) { + return new JdbcTemplateLockProvider( + JdbcTemplateLockProvider.Configuration.builder() + .withJdbcTemplate(new JdbcTemplate(dataSource)) + .usingDbTime() + .build() + ); + } }