개발/서버 플랫폼

스프링 스케줄러 (Feat. 동적 스케줄링, 멀티 스케줄링, 다중화 WAS 처리)

플랜B 2022. 4. 13. 15:40

배경

프로젝트에서 예약 기능을 추가하기 위해서 스케줄링을 공부하고 실제 적용했는데,
평범한 게시판 예약 기능이었다면, 스케줄러를 사용하지 않았을 것이다.. date 컬럼에 미래시간을 적어두었겠지..
아무튼 예약 기능을 위해 시작한 일을 정리하고자 한다.

1. 스케줄링

우선 '정해진 시간에 특정 로직이 동작'할 수 있도록 스케줄러를 구현했다.

 

Java 구현 부분 (클릭!)
@Service
public class ScheduleExecSampleServiceImpl implements ScheduleExecService {

    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    @Scheduled(cron = "*/2 * * * * *")
    public void schedulerRunnable() {
        LOGGER.info("Spring Scheduler Running!!");
    }
}

 

스프링 설정 부분 (클릭!)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd">

    <bean id="scheduler" class="com.spring.schedule.service.impl.ScheduleExecSampleServiceImpl" />
    <task:scheduler id="springScheduler" pool-size="10" />
    <task:executor id="springScheduleExecutor" pool-size="10" />
    <task:annotation-driven executor="springScheduleExecutor" scheduler="springScheduler" />
</beans>

 

2. 동적 스케줄링

예약기능은 사용자가 시간을 등록 / 수정 /삭제를 하는 경우 가 있기 때문에, 구현된 스케줄러에 update, stop 기능을 추가했다.
1번 스케줄러는 등록 / 수정 / 삭제하는 것 뿐만 아니라, 스케줄러가 필요한 서비스를 설정에 하나하나 추가해야하는 번거로움이 있기 때문에 사용하지 않았다.

 

스케줄러 update, stop 기능 추가 (클릭!)
@Component
public class SchedulerServiceImpl implements SchedulerService {
    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
    private ThreadPoolTaskScheduler scheduler;
    private String cron = "*/10 * * * * *";

    @Override
    public void startScheduler() {
        scheduler = new ThreadPoolTaskScheduler();
        scheduler.initialize();
        // scheduler setting
        scheduler.schedule(getRunnable(), getTrigger());
    }

    @Override
    public void setCron(String cron) { this.cron = cron; }

    @Override
    public void stopScheduler(Sample sampleType) { scheduler.shutdown();}

    private Runnable getRunnable() {
        // do something
        Runnable scheduleExecService = new ScheduleExecSamsungServiceImpl();
        return scheduleExecService;
    }

    private Trigger getTrigger() {
        // cronSetting
        return new CronTrigger(cron);
    }
}

// 소스코드 출처: https://myhappyman.tistory.com/235

 

3. 멀티 스케줄링

예약 기능을 한 번에 여러 개 활용 해야 했기 때문에, 스케줄러를 여러 개 등록할 수 있도록 구현했다.
등록한 스케줄들은 Map으로 관리했다.

 

멀티 스케줄러 구현 (클릭!)
@Component
public class SchedulerServiceImpl implements SchedulerService {
    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
    private static final Map<String, ThreadPoolTaskScheduler> scheduledMap = new HashMap<>();
    private String cron = "*/10 * * * * *";

    @Override
    public void startScheduler(Sample sampleType) {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.initialize();
        // scheduler setting
        scheduler.schedule(getRunnable(sampleType), getTrigger());
        scheduledMap.put(sampleType.getSampleType(), scheduler);
    }

    @Override
    public void setCron(String cron) { this.cron = cron; }

    @Override
    public void stopScheduler(Sample sampleType) { scheduledMap.get(sampleType.getSampleType()).shutdown(); }

    private Runnable getRunnable(Sample sampleType) {
        // do something
        Runnable scheduleExecService;
        switch (sampleType){
            case APPLE:
                scheduleExecService = new ScheduleExecAppleServiceImpl();
                break;
            case SAMSUNG:
                scheduleExecService = new ScheduleExecSamsungServiceImpl();
                break;
            default:
                scheduleExecService = new ScheduleExecSamsungServiceImpl();
                break;
        }
        return scheduleExecService;
    }

    private Trigger getTrigger() {
        // cronSetting
        return new CronTrigger(cron);
    }
}

 

4. 다중화 WAS 스케줄 처리

다중화 WAS에서 발생할 수 있는 예상 시나리오

사용자의 등록 / 수정 / 삭제 과정에서 그림과 같이 DB와 각 WAS에 등록된 스케줄의 정합성이 맞지 않아 제 시간에 동작하지 않는 문제가 있을 수 있다.
이를 해결하기 위해, 스케줄러가 동작할 때, DB 테이블에 저장된 시간을 한 번 검증하는 로직을 추가했다.

참고사항

RDBMS를 사용 중인데, 마지막에 정합성이 맞지 않아서 DB를 사용하는 것이 이상하게 느껴질 수 있다.

  1. DB에서 기본적인 동시성 처리를 해준다.
  2. RDBMS의 isolation-level을 잘 설정하면, DB에 저장된 값은 신뢰할 수 있다. (해당 내용은 아래 정리해두었다.)
  3. 그럼에도 불구하고 신뢰하지 못할 수 있는데, 우선 프로젝트 특성상 예약 기능을 사용하는 사용자가 많지 않다.
    위 3가지 이유로 DB를 이용하여 시간을 검증하는 로직을 추가할 수 있었다.

2022.01.10 - [개발/데이터 엔지니어링] - 트랜잭션 격리수준(Isolation Level)

 

트랜잭션 격리수준(Isolation Level)

트랜잭션이란? "더 이상 쪼갤 수 없는 업무 처리의 최소 단위" 를 말한다. 트랜잭션의 특징 1.원자성 원자성(atomicity)은 하나의 트랜잭션이 더 이상 작게 쪼갤 수 없는 최소한의 업무 단위이다. 트

jaksimsamil.tistory.com

프로젝트 전체 코드

https://github.com/jaksimsamil/spring-scheduler

 

GitHub - jaksimsamil/spring-scheduler

Contribute to jaksimsamil/spring-scheduler development by creating an account on GitHub.

github.com

 

반응형