Skip to content

Commit

Permalink
refactor: 스케쥴러, Async 리팩토링
Browse files Browse the repository at this point in the history
  • Loading branch information
jemin committed Jan 19, 2024
1 parent 9d324ff commit 4aa9365
Show file tree
Hide file tree
Showing 12 changed files with 105 additions and 126 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class QDetailGoal extends EntityPathBase<DetailGoal> {

public final com.backend.global.entity.QBaseEntity _super = new com.backend.global.entity.QBaseEntity(this);

public final SetPath<java.time.DayOfWeek, EnumPath<java.time.DayOfWeek>> alarmDays = this.<java.time.DayOfWeek, EnumPath<java.time.DayOfWeek>>createSet("alarmDays", java.time.DayOfWeek.class, EnumPath.class, PathInits.DIRECT2);
public final ListPath<java.time.DayOfWeek, EnumPath<java.time.DayOfWeek>> alarmDays = this.<java.time.DayOfWeek, EnumPath<java.time.DayOfWeek>>createList("alarmDays", java.time.DayOfWeek.class, EnumPath.class, PathInits.DIRECT2);

public final BooleanPath alarmEnabled = createBoolean("alarmEnabled");

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/com/backend/global/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
9 changes: 0 additions & 9 deletions src/main/java/com/backend/global/config/AysncConfig.java

This file was deleted.

17 changes: 13 additions & 4 deletions src/main/java/com/backend/global/config/SchedulerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 5 additions & 3 deletions src/main/java/com/backend/global/event/AlarmEventHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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());
}

}
31 changes: 0 additions & 31 deletions src/main/java/com/backend/global/event/GoalEventHandler.java

This file was deleted.

23 changes: 0 additions & 23 deletions src/main/java/com/backend/global/event/ReminderEventHandler.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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";
}
63 changes: 33 additions & 30 deletions src/main/java/com/backend/global/scheduler/SchedulerService.java
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -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<Goal> 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<DetailGoalAlarmResponse> 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<DetailGoalAlarmResponse> detailGoalAlarmList = detailGoalQueryRepository.getMemberIdListDetailGoalAlarmTimeArrived(
LocalDate.now().getDayOfWeek(), LocalTime.now());

detailGoalAlarmList.forEach(alarmDto -> applicationEventPublisher.publishEvent(
new AlarmEvent(alarmDto.uid(), alarmDto.detailGoalTitle())));
}
}
37 changes: 15 additions & 22 deletions src/main/java/com/backend/infrastructure/fcm/FcmService.java
Original file line number Diff line number Diff line change
@@ -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());
}
}

}

0 comments on commit 4aa9365

Please sign in to comment.