From 4aa936515fff305e16d8ad863d75c70c5a8076ec Mon Sep 17 00:00:00 2001 From: jemin Date: Sat, 20 Jan 2024 00:18:49 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EC=8A=A4=EC=BC=80=EC=A5=B4?= =?UTF-8?q?=EB=9F=AC,=20Async=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../detailgoal/domain/QDetailGoal.java | 2 +- .../repository/DetailGoalQueryRepository.java | 2 - .../backend/global/config/AsyncConfig.java | 29 +++++++++ .../backend/global/config/AysncConfig.java | 9 --- .../global/config/SchedulerConfig.java | 17 +++-- .../global/event/AlarmEventHandler.java | 8 ++- .../global/event/GoalEventHandler.java | 31 --------- .../global/event/ReminderEventHandler.java | 23 ------- .../global/scheduler/SchedulerConstant.java | 8 +++ .../global/scheduler/SchedulerService.java | 63 ++++++++++--------- .../infrastructure/fcm/FcmService.java | 37 +++++------ 12 files changed, 105 insertions(+), 126 deletions(-) create mode 100644 src/main/java/com/backend/global/config/AsyncConfig.java delete mode 100644 src/main/java/com/backend/global/config/AysncConfig.java delete mode 100644 src/main/java/com/backend/global/event/GoalEventHandler.java delete mode 100644 src/main/java/com/backend/global/event/ReminderEventHandler.java create mode 100644 src/main/java/com/backend/global/scheduler/SchedulerConstant.java diff --git a/build.gradle b/build.gradle index 0170941..b7560e4 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,7 @@ dependencies { // Querydsl implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' - annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" diff --git a/src/main/generated/com/backend/detailgoal/domain/QDetailGoal.java b/src/main/generated/com/backend/detailgoal/domain/QDetailGoal.java index e384066..c8cdac6 100644 --- a/src/main/generated/com/backend/detailgoal/domain/QDetailGoal.java +++ b/src/main/generated/com/backend/detailgoal/domain/QDetailGoal.java @@ -22,7 +22,7 @@ public class QDetailGoal extends EntityPathBase { public final com.backend.global.entity.QBaseEntity _super = new com.backend.global.entity.QBaseEntity(this); - public final SetPath> alarmDays = this.>createSet("alarmDays", java.time.DayOfWeek.class, EnumPath.class, PathInits.DIRECT2); + public final ListPath> alarmDays = this.>createList("alarmDays", java.time.DayOfWeek.class, EnumPath.class, PathInits.DIRECT2); public final BooleanPath alarmEnabled = createBoolean("alarmEnabled"); diff --git a/src/main/java/com/backend/detailgoal/domain/repository/DetailGoalQueryRepository.java b/src/main/java/com/backend/detailgoal/domain/repository/DetailGoalQueryRepository.java index 53048a0..0b4ca9d 100644 --- a/src/main/java/com/backend/detailgoal/domain/repository/DetailGoalQueryRepository.java +++ b/src/main/java/com/backend/detailgoal/domain/repository/DetailGoalQueryRepository.java @@ -1,9 +1,7 @@ package com.backend.detailgoal.domain.repository; import com.backend.detailgoal.application.dto.response.DetailGoalAlarmResponse; -import com.backend.goal.domain.QGoal; import com.backend.goal.domain.enums.GoalStatus; -import com.backend.member.domain.QMember; import com.querydsl.core.types.Projections; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/backend/global/config/AsyncConfig.java b/src/main/java/com/backend/global/config/AsyncConfig.java new file mode 100644 index 0000000..bdcd8bd --- /dev/null +++ b/src/main/java/com/backend/global/config/AsyncConfig.java @@ -0,0 +1,29 @@ +package com.backend.global.config; + +import java.util.concurrent.Executor; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@EnableAsync +@Configuration +public class AsyncConfig implements AsyncConfigurer { + + /* + @Async를 사용할때 ThreadPoolTaskExecutor를 설정하지 않으면 쓰레드 1개로 동작합니다. + 따라서 쓰레드 풀을 할당해서 멀티 쓰레드로 동작하도록 만들었습니다. + executor.setPrestartAllCoreThreads(false)로 설정해서 초반에 요청이 들어올때마다 CorePoolSize까지 쓰레드 개수를 늘리도록 만들었습니다. + */ + @Override + public Executor getAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(10); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(1000); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.initialize(); + return executor; + } +} diff --git a/src/main/java/com/backend/global/config/AysncConfig.java b/src/main/java/com/backend/global/config/AysncConfig.java deleted file mode 100644 index 58fa6e0..0000000 --- a/src/main/java/com/backend/global/config/AysncConfig.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.backend.global.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableAsync; - -@EnableAsync -@Configuration -public class AysncConfig { -} diff --git a/src/main/java/com/backend/global/config/SchedulerConfig.java b/src/main/java/com/backend/global/config/SchedulerConfig.java index 38d600f..7dac795 100644 --- a/src/main/java/com/backend/global/config/SchedulerConfig.java +++ b/src/main/java/com/backend/global/config/SchedulerConfig.java @@ -10,16 +10,25 @@ @Configuration @EnableScheduling -@EnableSchedulerLock(defaultLockAtLeastFor = "10s", defaultLockAtMostFor = "10s") +@EnableSchedulerLock(defaultLockAtLeastFor = "5s", defaultLockAtMostFor = "5s") public class SchedulerConfig implements SchedulingConfigurer { + private static final String SCHEDULER_THREAD_POOL_NAME = "scheduler thread pool"; + private static final String THREAD_NAME_PREFIX = "scheduler-thread-"; + private static final int POOL_SIZE = 3; + + /* + 쓰레드풀 사이즈를 선정할때는 필요 이상으로 크게 할당하는걸 경계해야 합니다. + 서비스에 쓰레드 개수가 늘어나는건 쓰레드 간 경합을 증가시켜서 컨텍스트 스위칭 비용을 증가시킵니다. + 따라서 현재 서비스에 스케쥴러가 2개만 존재하고 스케쥴링 간격이 긴 만큼 여유분 1개를 추가해서 총 3개의 쓰레드를 할당했습니다. + */ @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); - threadPoolTaskScheduler.setPoolSize(3); - threadPoolTaskScheduler.setThreadGroupName("scheduler thread pool"); - threadPoolTaskScheduler.setThreadNamePrefix("scheduler-thread-"); + threadPoolTaskScheduler.setPoolSize(POOL_SIZE); + threadPoolTaskScheduler.setThreadGroupName(SCHEDULER_THREAD_POOL_NAME); + threadPoolTaskScheduler.setThreadNamePrefix(THREAD_NAME_PREFIX); threadPoolTaskScheduler.initialize(); taskRegistrar.setTaskScheduler(threadPoolTaskScheduler); diff --git a/src/main/java/com/backend/global/event/AlarmEventHandler.java b/src/main/java/com/backend/global/event/AlarmEventHandler.java index 348d5ff..2b1bb05 100644 --- a/src/main/java/com/backend/global/event/AlarmEventHandler.java +++ b/src/main/java/com/backend/global/event/AlarmEventHandler.java @@ -4,6 +4,9 @@ import com.backend.detailgoal.domain.event.AlarmEvent; import com.backend.infrastructure.fcm.FcmService; import lombok.RequiredArgsConstructor; + +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; @@ -15,10 +18,9 @@ public class AlarmEventHandler { private final FcmService fcmService; - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Async + @EventListener public void sendAlarm(AlarmEvent event) { - fcmService.sendMessage(event.uid(), event.detailGoalTitle()); } - } diff --git a/src/main/java/com/backend/global/event/GoalEventHandler.java b/src/main/java/com/backend/global/event/GoalEventHandler.java deleted file mode 100644 index 2ec8daf..0000000 --- a/src/main/java/com/backend/global/event/GoalEventHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.backend.global.event; - -import com.backend.detailgoal.domain.DetailGoal; -import com.backend.detailgoal.domain.repository.DetailGoalRepository; -import com.backend.goal.domain.event.RemoveRelatedDetailGoalEvent; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.event.TransactionPhase; -import org.springframework.transaction.event.TransactionalEventListener; - -import java.util.List; - -@Component -@RequiredArgsConstructor -public class GoalEventHandler { - - private final DetailGoalRepository detailGoalRepository; - - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void removeDetailGoalList(RemoveRelatedDetailGoalEvent event) { - - List detailGoalList = detailGoalRepository.findAllByGoalIdAndIsDeletedFalse(event.goalId()); - detailGoalList.forEach((DetailGoal::remove)); - } - - - -} diff --git a/src/main/java/com/backend/global/event/ReminderEventHandler.java b/src/main/java/com/backend/global/event/ReminderEventHandler.java deleted file mode 100644 index b2f3d1b..0000000 --- a/src/main/java/com/backend/global/event/ReminderEventHandler.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.backend.global.event; - - -import com.backend.goal.domain.event.ReminderEvent; -import com.backend.infrastructure.fcm.FcmService; -import com.backend.member.domain.MemberRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.transaction.event.TransactionPhase; -import org.springframework.transaction.event.TransactionalEventListener; - -@Component -@RequiredArgsConstructor -public class ReminderEventHandler { - - private final FcmService fcmService; - private final MemberRepository memberRepository; - - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - public void sendAlarm(ReminderEvent event) { - - } -} diff --git a/src/main/java/com/backend/global/scheduler/SchedulerConstant.java b/src/main/java/com/backend/global/scheduler/SchedulerConstant.java new file mode 100644 index 0000000..eedfa48 --- /dev/null +++ b/src/main/java/com/backend/global/scheduler/SchedulerConstant.java @@ -0,0 +1,8 @@ +package com.backend.global.scheduler; + +public abstract class SchedulerConstant { + + public static final String OUTDATED_GOAL_LOCK = "outdated_goal_lock"; + public static final String SEND_ALARM_LOCK = "send_alarm_lock"; + public static final String LOCAL_TIME_ZONE = "Asia/Seoul"; +} diff --git a/src/main/java/com/backend/global/scheduler/SchedulerService.java b/src/main/java/com/backend/global/scheduler/SchedulerService.java index c2fb703..9c733a9 100644 --- a/src/main/java/com/backend/global/scheduler/SchedulerService.java +++ b/src/main/java/com/backend/global/scheduler/SchedulerService.java @@ -1,30 +1,27 @@ package com.backend.global.scheduler; -import com.backend.detailgoal.application.dto.response.DetailGoalAlarmResponse; +import static com.backend.global.scheduler.SchedulerConstant.*; -import com.backend.detailgoal.domain.event.AlarmEvent; -import com.backend.detailgoal.domain.repository.DetailGoalQueryRepository; -import com.backend.goal.domain.Goal; -import com.backend.goal.domain.repository.GoalQueryRepository; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.DayOfWeek; -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.List; - import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; -@Slf4j +import com.backend.detailgoal.application.dto.response.DetailGoalAlarmResponse; +import com.backend.detailgoal.domain.event.AlarmEvent; +import com.backend.detailgoal.domain.repository.DetailGoalQueryRepository; +import com.backend.goal.domain.Goal; +import com.backend.goal.domain.repository.GoalQueryRepository; + +import lombok.RequiredArgsConstructor; + @Service -@Transactional @RequiredArgsConstructor public class SchedulerService { @@ -34,26 +31,32 @@ public class SchedulerService { private final ApplicationEventPublisher applicationEventPublisher; - - @SchedulerLock(name = "outdate_goal_lock", lockAtMostFor = "10s", lockAtLeastFor = "10s") - @Scheduled(cron = "0 0 * * * *", zone = "Asia/Seoul") + /* + 매일 0시 0분에 달성 기간이 지난 목표들을 보관함으로 이동시킵니다. + 서버가 Scale out되면 스케쥴러가 중복 실행될 수 있으므로 Redis 기반의 스케쥴러 락을 사용해서 하나의 스케쥴러만 실행되도록 만들었습니다. + RDBMS(MySQL)를 사용할수도 있고, Redis를 사용할 수도 있습니다. 현재 서비스에서 인증 토큰 용도로 Redis를 도입했기 때문에 추가 인프라 비용 없이 Redis를 선택했습니다. + 서비스가 성장해서 스케쥴러의 종류가 많아지고 스케일 아웃으로 서버 수도 증가하면, 스케쥴러 락을 사용하기 보다는 알림 서버를 따로 분리하는 선택을 할 것 같습니다. + - 현재는 인프라 비용 문제로 AWS 상에서 단일 서버만 운영하고 있습니다. + */ + @SchedulerLock(name = OUTDATED_GOAL_LOCK) + @Scheduled(cron = "0 0 * * * *", zone = LOCAL_TIME_ZONE) + @Transactional public void storeOutDateGoal() { List goalList = goalQueryRepository.findGoalListEndDateExpired(LocalDate.now()); goalList.forEach(Goal::store); } - @SchedulerLock(name = "send_alarm_lock", lockAtMostFor = "10s", lockAtLeastFor = "10s") - @Scheduled(cron = "0 */30 * * * *", zone = "Asia/Seoul") - public void sendAlarm() - { - DayOfWeek dayOfWeek = LocalDate.now().getDayOfWeek(); - LocalTime localTime = LocalTime.now(); - LocalTime now = LocalTime.of(localTime.getHour(), localTime.getMinute(), 0); - - List detailGoalAlarmList = detailGoalQueryRepository.getMemberIdListDetailGoalAlarmTimeArrived(dayOfWeek, now); - log.info("{}",detailGoalAlarmList.size()); - detailGoalAlarmList.forEach(alarmDto -> - applicationEventPublisher.publishEvent(new AlarmEvent(alarmDto.uid(), alarmDto.detailGoalTitle()))); + /* + 사용자가 지정한 시간(30분 단위)에 목표에 대한 알림을 전송합니다. + */ + @SchedulerLock(name = SEND_ALARM_LOCK) + @Scheduled(cron = "0 */30 * * * *", zone = LOCAL_TIME_ZONE) + public void sendAlarm() { + List detailGoalAlarmList = detailGoalQueryRepository.getMemberIdListDetailGoalAlarmTimeArrived( + LocalDate.now().getDayOfWeek(), LocalTime.now()); + + detailGoalAlarmList.forEach(alarmDto -> applicationEventPublisher.publishEvent( + new AlarmEvent(alarmDto.uid(), alarmDto.detailGoalTitle()))); } } diff --git a/src/main/java/com/backend/infrastructure/fcm/FcmService.java b/src/main/java/com/backend/infrastructure/fcm/FcmService.java index 001b31b..ffa3cad 100644 --- a/src/main/java/com/backend/infrastructure/fcm/FcmService.java +++ b/src/main/java/com/backend/infrastructure/fcm/FcmService.java @@ -1,55 +1,48 @@ package com.backend.infrastructure.fcm; +import java.util.Objects; + +import org.springframework.stereotype.Service; + import com.backend.auth.application.FcmTokenService; -import com.backend.global.exception.BusinessException; import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.Message; import com.google.firebase.messaging.Notification; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.Objects; @Slf4j @Service @RequiredArgsConstructor public class FcmService { - private final FcmTokenService fcmTokenService; private final FirebaseMessaging firebaseMessaging; - public void sendMessage(String uid, String detailGoalTitle) - { + public void sendMessage(String uid, String detailGoalTitle) { String fcmToken = fcmTokenService.findFcmToken(uid); - if(Objects.isNull(fcmToken)) - { + if (Objects.isNull(fcmToken)) { return; } Notification notification = Notification.builder() - .setTitle(PushWord.PUSH_TITLE) - .setBody(detailGoalTitle + PushWord.PUSH_CONTENT) - .build(); + .setTitle(PushWord.PUSH_TITLE) + .setBody(detailGoalTitle + PushWord.PUSH_CONTENT) + .build(); Message message = Message.builder() - .setToken(fcmToken) - .setNotification(notification) - .build(); + .setToken(fcmToken) + .setNotification(notification) + .build(); try { - - log.info("message send start..."); - String send = firebaseMessaging.send(message); - log.info("message send finished, {}", send); - + firebaseMessaging.send(message); } catch (FirebaseMessagingException e) { - e.printStackTrace(); + log.error(e.getMessage()); } } - }