[4부] Spring Scheduler에서 Quartz로 전환기

황제연·2024년 12월 19일
0
post-thumbnail

서론

기본 FCM Topic 구독 과정을 개발했습니다.

이제 사용자는 즐겨찾기한 이벤트 정보를
시작 10분전과 시작시간에 웹 푸시알림으로 받을 수 있습니다.

다음 과제는 정한 시간에 맞춰 웹 푸시알림 메세지를
서버에서 자체적으로 보낼 수 전송하는 방법입니다.

이 과제를 해결하기 위해 Scheduling 방식을 선택했습니다

Spring의 Scheduler 개념정리

Spring의 Scheduler은 작업 Scheduling 기능을 제공합니다.

작업 스케줄링이란?
작업스케줄링은 시스템의 전반적인 효율성과 효과적인 운영을 최적화하기 위해 작업 또는 작업을 사용 가능한 자원에 할당하는 과정입니다.

그리고 Scheduler는 여러 개의 작업이 요청되었을 때, 그것을 효과적으로 할당해주고
순서를 정해주는 방법입니다.

Spring Scheduler 사용처

Spring Scheduler는 주로 백그라운드에서 반복적으로 수행하는 작업에 사용됩니다.

정해진 시간이나 주기에 따라 작업을 실행할 수 있기 때문에
프로젝트의 목표인 Topic 구독 사용자에게
정해진 시간에 웹 푸시 알림을 전달하는 데 적합하다고 판단했습니다

Scheduler 작업 대상 선정

구현하기 전, 어떤 작업을 수행할지 정의했습니다.

  • 현재시간과 이벤트 시작 시간 비교
  • 시작 10분전, 시작시간에 웹 푸시알림 전송

Scheduler는 위 두가지 작업을 진행합니다.

Scheduler 주기 설정

Spring Scheduler은 다음 두가지 방식으로 주기를 설정할 수 있습니다

  • 일정 시간간격마다 현재시간과 이벤트 시작시간 비교
  • 특정 예약 시간에 작업 실행

이벤트 시작 시간이라는 고정된 개념이 존재하기 때문에,
이벤트 시작 10분전과 시작시간에 Scheduler에 등록한 작업을 실행하도록 설계했습니다.

Scheduling 범위 설정

모든 이벤트를 Scheduling하면 서버 리소스 부담이 커질 수 있습니다.
따라서 당일 이벤트만 Scheduling하기로 결정했습니다

당일 이벤트 Scheduling 시간

당일 이벤트 Scheduling은 매일 특정 시간에 실행하도록 설계했습니다.
모든 행사의 이벤트를 확인하는 작업이기 때문에,
사용자가 적은 시간대인 0시 30분에 Scheduling하는 것을 선택했습니다.

Scheduling 정리

다음과 같이 Scheduling방식을 정리했습니다.

  • Scheduling 대상: Event 시작 시간
  • 당일 Scheduling 시간: 0시 30분

매일 0시 30분에 Scheduler가 당일 이벤트를 조회하고 등록합니다.
그리고 예약된 시간에 웹 푸시 알림을 전송합니다

첫 Scheduler 방법 선택과 설계

Spring boot에서는 두 가지 Scheduling 방법을 제공합니다

  • Spring Scheduler
  • Quartz

Spring Scheduler 선택

초기에는 Spring Scheduler를 선택했습니다.

Spring Scheduler를 선택한 이유는 다음 두가지입니다.

  • 사용이 쉬워 개발을 빨리 할 수 있다
  • 프로젝트에서 복잡한 스케줄링을 필요로 하지 않는다.

사용 방법

스프링 스케줄러는 의존성 추가 없이 바로 사용할 수 있습니다
다음 두 애노테이션을 붙여주면 사용할 수 있습니다

  • @EnableScheudling
  • @Scheduled
@EnableScheduling  
@EnableJpaAuditing  
@SpringBootApplication  
public class CheckingApplication {  
  
    public static void main(String[] args) {  
       SpringApplication.run(CheckingApplication.class, args);  
    }  
  
}

@EnableScheudling의 경우 메인 클래스에서 적용하면 됩니다

@Scheduled(cron = "0 30 0 * * ?")  
@Transactional  
public void updateEvents() {  
    List<Long> contestIds = getAllContestIds();  
    log.info("스케줄러 실행");  
  
    for (Long contestId : contestIds) {  
        updateContestEvents(contestId);  
    }  
}

@Scheduled는 스케줄링 작업할 메소드에 적용해서
특정한 시간이나 일정한 시간 간격으로 원하는 메소드 작업을 실행할 수 있습니다.

Cron 표현식

Scheduler 시간을 설정할 때, Cron표현식을 사용합니다

스케줄러 표현식 형태
'초' '분' '시' '일' '월' '년'

위 표현식을 활용해서, 특정 시간대에 Scheduling 작업을 하도록 설정할 수 있습니다

@Scheduled(cron = "0 30 0 * * ?")  

위와 같이 0시 30분 0초에 진행하도록 설정했습니다

Scheduling Task Flow 설계

Spring Scheduler에서 처리할 Task flow는 다음과 같이 설계했습니다

  1. 0시 30분에 Spring Scheduler 실행
  2. 작일 등록한 이벤트 task 삭제
  3. 금일 이벤트 task 등록

0시 30분에 위와 같은 작업이 진행됩니다
그리고 등록한 이벤트 시작 10분전과 시작시간에 이벤트가 발생하고
웹 푸시 알림이 발송됩니다.

Scheduling 테스트

0시 30분까지 기다려서 테스트할 수는 없기 때문에,
시간을 조정해서 테스트를 진행했습니다

그 결과 즐겨찾기를 등록한 사용자는 다음과 같은 웹 푸시 알림을 받았습니다!

예외상황 고민과 Quartz로의 전환

이제 사용자에게 이벤트 시작시간에 맞춰 웹 푸시알림을 전송할 수 있습니다

예외상황 고민

하지만 현재 방식은 다음과 같은 예외상황에서 대처할 수 없습니다

  • 당일 Scheduling 시간 이후 이벤트 CREATE
  • 당일 Scheduling 시간 이후 이벤트 시작시간 UPDATE
  • 당일 이벤트 삭제

Spring Scheduler의 한계

앞서 고민한 예외상황은 다음과 같이 대처할 수 있습니다.

  • Scheduler에 작업 등록하는 로직 개발
  • Scheduler에 등록한 이벤트 시작시간 조정
  • Scheduler에 등록한 이벤트 삭제

위 방식으로 개발하기 위해서는 Scheduler에 등록한 작업을 찾아야합니다
등록한 작업을 찾아서 삭제하려면, 작업의 고유 ID를 파악하고 있어야 하는데
HashMap 자료구조를 이용해서 관리할 수 있습니다.

Map 저장 방식의 문제점

Map 저장 방식은 다음 문제가 예상됩니다

  • 직접 작업 상태를 관리
  • WAS 종료 시 데이터 손실
    직접 작업상태를 관리하는 문제는,
    너무 많은 변수가 존재하기 때문에 좋은 방법이 아니라고 판단했습니다.

또한 WAS가 종료되는 경우, Map에 저장된 데이터는 모두 삭제됩니다.
이 문제를 해결하기 위해서
DB에 상태를 저장하면 되는데, 관리의 대상이 더 늘어나는 문제가 발생합니다.

Quartz의 선택

따라서 작업의 동적 등록/수정/삭제가 편리하며,
복잡한 작업을 편리하게 해결할 수 있는 Quartz 방법을 선택했습니다.

기존 Spring Scheduler의 설정은 모두 삭제하고,
Quartz를 이용하기 위해 gradle에 다음 의존성을 추가했습니다

implementation 'org.springframework.boot:spring-boot-starter-quartz'

해당 의존성으로 별도의 추가 설정없이 Quartz Scheduler를 사용할 수 있습니다

Quartz를 이용한 Scheduling 기능 개발

기존에 설계한 Task flow를 Quartz를 이용해서 개발했습니다
먼저 Cron Scheduling은 다음 두가지 Bean을 등록해서 전환했습니다.

@Bean  
public JobDetail eventScheduleJob() {  
    return JobBuilder.newJob(QuartzSchedulerComponent.class)  
            .withIdentity("eventNotification", "dailyEventNotification")  
            .storeDurably()  
            .build();  
}  
  
@Bean  
public Trigger eventScheduleTrigger() {  
    return TriggerBuilder.newTrigger()  
            .forJob(eventScheduleJob())  
            .withIdentity("eventNotificationTrigger", "dailyEventNotificationTrigger")  
            .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(0, 30))  
            .startNow()  
            .build();  
}

Quartz 사용 정리

Quartz를 사용하기 위해서는 몇 가지 추가 용어에 대한 이해가 필요합니다

Job

Job은 실행할 작업을 정의합니다.
Job에 등록한 작업을 Trigger가 발생했을 때, 작업합니다.

Trigger

Trigger는 Job이 실행되는 조건입니다.
0시 30분에 실행한다는 것도 Job의 실행 조건입니다

JobBuilder

새로운 Job은 JobBuilder의 빌더 패턴을 이용해서 등록할 수 있습니다
withIdentity로 Job의 ID와 group명을 지정해서 등록할 수 있습니다

이것을 이용하면 나중에 등록한 작업을 찾는데 도움이 될 수 있습니다!

TriggerBuilder

JobBuilder와 동일하게 Builder 패턴으로 Trigger를 설정할 수 있습니다

withSchedule로 발생 조건을 등록할 수 있습니다.
매일 0시 30분에 동작하도록 설정했습니다

execute

public class QuartzSchedulerComponent implements Job {  
  
    private final Scheduler scheduler;  
    private final EventSchedulerService eventSchedulerService;  
  
    /**  
     * 매일 12시 30분에 실행되는 스케줄러  
     * 1. 이전날 이벤트 모두 불러와서 태스크 삭제  
     * 2. 당일 이벤트 모두 불러와서 태스크 등록  
     */  
    @Override  
    public void execute(JobExecutionContext jobExecutionContext) {  
        // 1. 이전날 이벤트 모두 불러와서 태스크 삭제  
        try {  
            scheduler.clear();  
            log.info("스케줄러에 등록된 모든 이벤트 삭제");  
        } catch (SchedulerException e) {  
            log.info("스케줄러에 등록된 모든 이벤트 삭제도중 예외 발생: {}", e.getMessage());  
            throw new RuntimeException(e);  
        }  
  
        // 2. 당일 이벤트 모두 불러와서 태스크 등록  
        List<Long> contestIds = eventSchedulerService.getAllContestIds();  
        log.info("스케줄러 실행, 스케줄링 대회 수: {} ", contestIds.size());  
        for (Long contestId : contestIds) {  
            eventSchedulerService.updateContestEvents(contestId);  
        }  
    }  
  
}

execute에서는 Scheduler에 등록한 Job의 실제 로직을 개발할 수 있습니다.
Job 인터페이스를 implementation해서 execute를 구현했습니다.

앞서 계획한대로 매일 0시 30분에 Scheduler를 실행하고,
전날 이벤트를 모두 삭제한뒤 당일 이벤트를 조회해서 Job으로 등록했습니다

JobDetail

새로운 Job을 Schduler에 등록하기 위해서는 JobDetail 객체를 생성해야합니다

JobDetail jobDetailBeforeEvent = JobBuilder.newJob(NotificationService.class)  
        .withIdentity("EventJob_" + NotificationSchedule + "_BeforeEvent")  
        .usingJobData("eventId", event.getId())  
        .usingJobData("notification Type", "10분 전 알림")  
        .usingJobData("contestId", event.getEventContest().getId())  
        .usingJobData("eventName", event.getName())  
        .build();  
  
Trigger triggerBeforeEvent = TriggerBuilder.newTrigger()  
        .forJob(jobDetailBeforeEvent)  
        .withIdentity("Trigger_" + NotificationSchedule + "_BeforeEvent")  
        .startAt(Date.from(notificationTimeBefore  
                .atZone(ZoneId.systemDefault())  
                .toInstant()))  
        .build();

위와 같은 JobDetail 객체와 Trigger를 생성한 뒤

try {  
    if(LocalDateTime.now().isBefore(notificationTimeBefore)){  
        scheduler.scheduleJob(jobDetailBeforeEvent, triggerBeforeEvent);  
        log.info("10분전 알림 등록: {}", notificationTimeBefore);  
    }  
  
    if(LocalDateTime.now().isBefore(startTime)){  
        scheduler.scheduleJob(jobDetailStartEvent, triggerStartEvent);  
        log.info("시작 알림 등록: {}", startTime);  
    }  
} catch (SchedulerException e) {  
    log.error("스케줄링 예외 발생: {}", e.getMessage());  
    throw new RuntimeException(e);  
}

Scheduler의 새로운 Job으로 등록했습니다

JobDetail 개발 과정 트러블 슈팅

JobDetail을 사용하는 과정에서 두가 지 문제를 경험했습니다

  • Job을 실행했을 때, 데이터를 다시 조회하는 문제
  • Trigger 시작시간 타입문제

1번 문제

Scheduler에 등록할 때 조회한 데이터를
Job이 실행될 때 사용하지 못하기 때문에 다시 DB에서 조회하는 문제를 발견했습니다.

해당 문제는 간단하게 해결할 수 있습니다
usingJobData 설정을 통해 key-value 형식으로 저장하면 Job이 실행될 때
사용할 수 있습니다

2번 문제

Event의 시작시간 타입은 LocalDateTime이지만,
Quartz에 시작시간을 등록하기 위해서는 Date 타입이 필요했습니다

따라서 LocalDateTime을 Date 타입으로 바꾸기 위해

.startAt(Date.from(startTime  
        .atZone(ZoneId.systemDefault())  
        .toInstant()))

위와 같이 LocalDateTime을 특정 시간대 Zone으로 변환하고 Instance 타입으로 바꾼 뒤,
다시 Date 타입으로 변환했습니다

Quartz를 활용한 예외상황 대처 로직 개발

이제 기존 기능을 모두 전환했습니다.
앞서 고민했던 예외상황 대처 로직을 개발할 시간입니다.

다시 정리하면 개발할 부분은 다음과 같습니다

  • Scheduler에 CREATE한 Event 등록
  • 시작시간을 UPDATE한 Event Job 수정
  • DELETE한 Event job 삭제

CREATE EVENT 등록 과정 개발

0시 30분 이후에 새로 CREATE한 Event를 Scheduler에 새롭게 등록합니다.

/**  
 * 스케줄링 끝난 상태에서 신규 이벤트 생성할 경우  
 * 1. 오늘 시작하는 이벤트인지 확인  
 * 2. 참이라면, 이벤트 발송 DB에 데이터 있는지 확인  
 * 3. 없다면 스케줄러에 예약 등록  
 * 4. 이어서 INSERT 진행  
 */  
@Transactional  
public void scheduleNewEvent(EventResponseDto eventResponseDto){  
  
    // 1번 로직  
    if(!eventResponseDto.getStartTime().toLocalDate()  
            .isEqual(LocalDate.now())){  
        return;  
    }  
  
    // 2번 로직  
    Long eventId = cryptoUtil.decrypt(eventResponseDto.getSecureId());  
    List<EventSending> eventSends = eventSendingRepository  
            .findByEventSendingEvent_Id(eventId);  
  
    if(!eventSends.isEmpty()){  
        return;  
    }
  
    Event event = eventRepository.findById(eventId).orElseThrow  
            (() -> new ApiException(EVENT_NOT_FOUND));  
  
    // 3번 로직  
    scheduleNotifications(Collections.singleton(event));  
  
    // 4번 로직  
    scheduleEvent(event);  
}

위와 같이 4단계로 나눠서 동작하도록 개발했습니다.
해당 로직을 CREATE 컨트롤러에 등록해서 Event Create한 뒤,
Scheduler에 등록하도록 개발했습니다

로직을 정리하면 다음과 같습니다

여기서 웹 푸시알림 발송 여부를 확인하는 로직이 나오는데,
이 내용은 뒤에서 다루겠습니다

UPDATE EVENT 등록 과정 개발

event 시작 당일, event의 시작시간이 1시간 뒤로 변경되거나 혹은
다른 날로 변경되는 예외상황이 발생할 수 있습니다

이미 Scheduler에 등록되었기 때문에,
UPDATE하기 전의 시간으로 웹 푸시알림이 발송될 위험이 있습니다.

따라서 EVENT가 UPDATE되면 등록한 Job도 수정해야합니다

Trigger 시작 시간 변경

등록한 Job의 Trigger 시작시간을 변경하면 됩니다.

하지만 Quartz의 Trigger는 String 타입처럼 불변이기 때문에,
직접 변경할 수 없고 새로운 Trigger를 등록해야 합니다.

따라서 기존 Job을 삭제하고, 새로운 Job과 Trigger를 등록하도록 변경했습니다.

/**  
 * 스케줄링에 등록한 이벤트를 수정한 경우  
 * 1. 스케줄러에 등록한 이벤트 job 삭제  
 * 2. 오늘 시작하는 이벤트인지 확인  
 * 3. 업데이트한 이벤트 스케줄러에 등록  
 * 4. 연결된 이벤트 발송목록 불러와서, SENDING이면 PENDING으로 변경  
 */  
@Transactional  
public void scheduleUpdateEvent(EventResponseDto eventResponseDto){  
    Long eventId = cryptoUtil.decrypt(eventResponseDto.getSecureId());  
    Event event = eventRepository.findById(eventId).orElseThrow  
            (() -> new ApiException(EVENT_NOT_FOUND));  
    // 1번 로직  
    deleteJob(event);  
  
    // 2번 로직  
    if(!eventResponseDto.getStartTime().toLocalDate()  
            .isEqual(LocalDate.now())){  
        return;  
    }  
  
    // 3번 로직  
    scheduleNotifications(Collections.singleton(event));  
  
    // 4번 로직  
    List<EventSending> eventSends = eventSendingRepository  
            .findByEventSendingEvent_Id(eventId);  
  
    for (EventSending eventSend : eventSends) {  
        // 발송 상태가 SENDING인 경우 PENDING 변경 필요  
        if(eventSend.getSendingStatus().equals(SendingStatus.SENDING)){  
            // 현재시간이 등록시간보다 이전이면 PENDING으로 변경  
  
            // 시작 10분전 알림 설정  
            if(LocalDateTime.now().isBefore(eventResponseDto.getStartTime().minusMinutes(10))  
                    && eventSend.getSendingType().equals(SendingType.EVENT_BEFORE_10MINUTES)){  
                eventSend.changeStatus(SendingStatus.PENDING);  
                continue;            }  
  
            // 시작 알림 설정  
            if(LocalDateTime.now().isBefore(eventResponseDto.getStartTime())  
                    && eventSend.getSendingType().equals(SendingType.EVENT_START_NOW)){  
                eventSend.changeStatus(SendingStatus.PENDING);  
            }  
        }  
    }  
}

로직을 정리하면 다음과 같습니다

Job을 삭제하는 로직을 추가한뒤,
이후 로직은 CREATE와 동일하게 동작하도록 개발했습니다

4번 로직만 기존과 다르게 동작하도록 개발했습니다.
해당 로직은 뒤에서 더 다루겠습니다

DELETE EVENT 삭제 과정 개발

만약 오늘 예정된 이벤트가 갑자기 취소된다면 해당 EVENT를 삭제할 것입니다.
이 과정에서 Job을 삭제하지 않으면,
없는 EVENT를 푸시알림으로 전달하거나 nullException 예외가 발생할 수 있습니다

따라서 EVENT가 삭제된다면 JOB도 삭제하도록 개발했습니다.

삭제 순서

하지만 EVENT를 먼저 삭제하고 JOB을 삭제하면, Null Exception 예외가 발생합니다.
따라서 Job을 먼저 삭제하고 Event를 삭제하도록 개발했습니다

/**  
 * 스케줄링에 등록한 이벤트를 삭제한 경우  
 * 1. 스케줄러에 등록한 이벤트 작업 삭제  
 */  
@Transactional  
public void scheduleDeleteEvent(Long eventId){  
    // 1번 로직  
    Event event = eventRepository.findById(eventId).orElseThrow  
            (() -> new ApiException(EVENT_NOT_FOUND));  
    deleteJob(event);  
    eventSendingRepository.deleteByEventSendingEvent_Id(eventId);  
}

매우 간단하게 해결되었습니다!

초기화 문제 해결과정

앞서 고민한 예외상황에 대처할 수 있는 기능을 개발했습니다.
하지만 Scheduling 과정에서 더 많은 문제가 발생할 수 있습니다

가장 큰 문제는 바로 WAS가 종료되고 다시 실행하면,
기존에 등록한 Job이 모두 삭제되는 문제입니다.

Scheduling 작업은 RAM에서 동작합니다.
따라서 WAS가 종료되면, RAM에 등록한 모든 작업은 삭제됩니다

따라서 당일 Scheduling 이후, WAS를 종료하면 당일 정상적으로 작동해야하는
웹 푸시알림이 동작하지 않을 것입니다.

해결방법 고민

WAS가 종료되었을 때, 등록한 Job이 삭제된다면 WAS를 다시 실행할 때
삭제된 Job을 등록하면 됩니다.

하지만 이미 발송한 Job의 경우 등록할 필요가 없기 때문에,
발송 상태를 기록해서 구분할 필요가 있습니다

발송상태 기록 Entity

웹 푸시 알림 발송 상태를 기록하기 위해 추가 Entity를 만들었습니다

Event Entity와 1대 다로 연관관계를 맺으며,
Event Type(시작시간 10분전 발송 알림, 시작시간 발송 알림)
Event Status(Pending, Sending)을 필드로 설정했습니다.

Pending은 발송 전, Sending은 발송 후를 의미합니다.

발송상태 기록 및 관리

앞서 CREATE / UPDATE / DELETE 과정에서 발송상태를 기록/관리하는 로직을 정리했습니다.

CREATE할 때는 Pending Status로 (10분전, 시작시간) Type을 갖는 값을
각각 DB에 저장하도록 개발했습니다

UPDATE 과정에서는 Sending 상태인 경우 Pending으로 변경하며,
Pending은 그대로 유지되도록 개발했습니다

DELETE 과정에서는 Sending 객체를 삭제하도록 개발했습니다.

ObjectAlreadyExistsException 트러블 슈팅

테스트 하는 과정 중에 해당 예외가 발생했습니다.
이벤트를 새로 등록하고 난뒤, WAS를 재실행하면 해당 예외가 발생했습니다.

문제의 원인은 작성한 로직에 존재했습니다
UPDATE과정에서 EventSending 객체를 모두 가져와 확인하는데,
이때 같은 Event와 연결된 EventSending을 중복으로 가져와서 발생하는 문제였습니다.

위와 같이 같은 이벤트를 두번 가져와서 두가지 Sending 타입을 등록하기 떄문에,
문제가 발생했습니다.

해당 문제를 해결하기 위해 중복등록을 막을 수 있는 Set 자료구조를 사용해서 해결했습니다

WAS 종료/시작 시점 초기화 개발

이어서 WAS가 종료될 때 Scheduler를 초기화하고
시작할 때는 Scheduler에 다시 등록하는 기능을 개발했습니다

WAS 종료 시점 초기화

@PreDestroy  
public void shutdownScheduler(){  
    try {  
        scheduler.shutdown(true);  
        log.info("스케줄러 종료 완료");  
    } catch (SchedulerException e) {  
        log.error("스케줄러 종료 작업 에러 발생: {}", e.getMessage());  
        throw new RuntimeException(e);  
    }  
}

WAS 종료 시점에 초기화하는 작업은 @PreDestroy 애노테이션을 통해 개발했습니다

Scheduler.shutdown()에서 true설정을 하면
현재 진행중인 작업이 모두 완료될때까지 기다렸다가 WAS를 종료할 수 있습니다.

즉, GracefulShutdown이 가능합니다.
알림 발송도중 WAS를 종료시켜서 사용자에게 정해진 시간에
웹 푸시알림을 보내지 못하는 상황을 만들고 싶지 않았기 때문에, true 설정을 추가했습니다

WAS 시작 시점 초기화

public class SchedulerInitializer implements ApplicationListener<ApplicationReadyEvent> {  
  
    private final EventSchedulerService eventSchedulerService;  
    private final Scheduler scheduler;  
  
    @Override  
    public void onApplicationEvent(ApplicationReadyEvent event) {  
        try {  
            scheduler.clear();  
        } catch (SchedulerException e) {  
            throw new RuntimeException(e);  
        }  
  
        eventSchedulerService.initializeScheduler();  
    }
}

이번에는 WAS 시작 시점에 초기화하는 로직을 개발했습니다
ApplicationListner를 implementation해서 WAS 시작 이벤트가 발생하면
초기화 작업을 진행하도록 개발했습니다

/**  
 * WAS 재시작 시, 초기화 작업  
 */  
@Transactional  
public void initializeScheduler(){  
    Set<Event> events = eventSendingRepository  
            .findBySendingStatusNot(SendingStatus.SENDING)  
            .stream()  
            .map(EventSending::getEventSendingEvent)  
            .collect(Collectors.toSet());  
  
    scheduleNotifications(events);  
    log.info("스케줄러 초기화 작업 완료");  
}

eventSending Entity를 확인해서, SENDING 상태가 아닌 이벤트를 모두 가져옵니다
이 로직을 통해 발송하지 않은 Event를 모두 Job에 다시 등록할 수 있습니다!

Thread 설정 개수 고민

만약 동일한 시간에 시작하는 이벤트가 많아서,
같은 시간에 Trigger가 발생하는 Job이 많다면 지연시간이 발생할 것입니다.

Quartz에서는 이 문제를 해결하기 위해 Thread Pool 방식을 사용하며,
Default값으로 10을 설정되어있습니다.

하지만 해당 프로젝트에서는 동일한 시작시간을 가진 이벤트가 많지 않습니다.
그렇다고 1로 설정하면 지연시간이 발생할 가능성이 존재하며,
너무 많게 설정해도 서버 리소스가 낭비됩니다.

따라서 Thread Pool을 적당한 크기로 줄이기 위해
Default의 절반인 5로 줄이기로 결정했습니다

quartz:  
  job-store-type: memory  
  properties:  
    org:  
      quartz:  
        threadPool:  
          threadCount: 5

위와 같은 설정을 추가해서 Thread개수를 5로 설정했습니다

WAS를 실행할 때, 정상적으로 반영되는 것을 확인할 수 있습니다!

마무리

이제 WAS가 재실행되었을 때,
Scheduling 이후 CREATE/UPDATE/DELETE 작업에 대해서 유연하게 대처할 수 있습니다

하지만 아직 남은 문제가 몇 가지 존재합니다

  • 웹 푸시알림 발송 실패 대처
  • Scheduling 작업 실패 대처

해당 내용은 5부에 이어서 작성하겠습니다

참고

profile
Software Developer

0개의 댓글