ThreadPoolTaskScheduler를 통해 스케줄러 동작 시간 동적으로 변경하기
ThreadPoolTaskScheduler를 통해 스프링 스케줄러 동작 시간 동적으로 변경하는 방법
기존에는 스케줄링 기능이 필요할 때 '@Scheduler' 어노테이션을 통해 간단하게 스케줄러 기능을 사용했었는데요.
이번에 외부에서 받은 값을 통해 스케줄링 시간을 동적으로 변경하는 기능이 필요하여 알아보던 중 'ThreadPoolTaskScheduler'를 알게 되어 관련 내용을 직접 테스트해 보며 정리한 내용입니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.3.xsd">
... (생략)
<!-- 스케줄러 생성 -->
<task:annotation-driven executor="executor" scheduler="scheduler"/>
<!-- pool-size 지정하지 않을 경우 쓰레드 풀의 기본값은 1 -->
<task:scheduler id="scheduler" pool-size="10"/>
<task:executor id="executor" pool-size="10"/>
</beans>
(root-context.xml 파일 중 스케줄러 관련 task 설정이며, task 외 부분은 생략되었습니다.)
위 코드는 스프링에서 스케줄러를 사용하기 위해 root-context.xml 파일에 task 관련 설정을 추가한 내용입니다.
ThreadPoolTaskScheduler 스케줄러 동작 시간 동적으로 변경하기
@Component
public class DynamicScheduler {
private ThreadPoolTaskScheduler scheduler;
private String cron = "*/5 * * * * *";
private int ms = 3000;
public void startScheduler() {
scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.initialize();
// scheduler setting
scheduler.schedule(getRunnable(), getCronTrigger());
}
public void stopScheduler() {
scheduler.shutdown();
}
public Runnable getRunnable() {
return () -> {
// do someting.
};
}
private Trigger getCronTrigger() {
return new CronTrigger(cron);
}
public void updateCronSet(String cron) {
this.cron = cron;
}
private Trigger getPeriodcTrigger() {
return new PeriodicTrigger(ms, TimeUnit.MILLISECONDS);
}
public void updateMillisecondSet(int ms) {
this.ms = ms;
}
}
(DynamicScheduler class)
스케줄러 동작 시간을 변경하기 위해서는 Spring에서 제공하는 'ThreadPoolTaskScheduler' 클래스를 활용하여 다음 예시(Trigger를 사용한)와 같은 코드를 사용할 수도 있으며, 'schedule()' 메서드 외에도 필요에 따라 'scheduleAtFixedRate()', 'scheduleWithRixedDelay()' 메서드를 사용하여 동작에 필요한 매개변수를 update 하는 방식으로 사용할 수 있습니다.
* fixedRate의 경우 task 시작 시점으로부터 정의된 시간이 지난 후 task를 실행하며, fixedDelay의 경우 task 종료 시점으로부터 정의된 시간이 지난 후 task를 실행합니다.
(추가적으로 InitializingBean, DisposableBean 인터페이스를 상속하여 빈 생성과 소멸 시점에 스케줄러를 시작 또는 종료하는 코드를 추가로 활용할 수도 있습니다.)
@Autowired
DynamicScheduler scheduler;
public void updateCronSet(String cron) {
scheduler.stopScheduler();
scheduler.updateCronSet(cron);
scheduler.startScheduler();
}
public void updateMillisecondSet(int ms) {
scheduler.stopScheduler();
scheduler.updateMillisecondSet(ms);
scheduler.startScheduler();
}
(스케줄링 시간 동적 변경 예시)
스케줄러의 동작 시간을 변경하는 쪽에서는 다음과 같이 스케줄러를 중지했다가 매개변수 update 후 재시작하는 방식으로 사용할 수 있습니다.
(이 부분은 단순 예시이므로 참고만 하여 필요에 따라 코드를 구현하시면 될 것 같습니다.)
ThreadPoolTaskScheduler 사용 과정에서 알게 된 부분
만약 scheduler의 동작 주기보다 task 작업 시간이 더 길다면 어떻게 될까요?
'fixedDelay'의 경우 원래 task가 종료된 시점으로부터 정의된 시간이 지난 후 다시 scheduler가 동작하기 때문에 상관이 없지만, 'fixedRate'와 'cron' 방식의 경우 task 실행 후 결과가 나오지 않은 상태에서 스케줄러의 동작 주기가 다시 돌아온다면 task를 실행하지 않고 다음에 돌아오는 스케줄러의 동작 주기를 기다리게 됩니다.
public void exampleScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(5);
taskScheduler.initialize();
// 스케줄링 주기가 2초로 설정된 작업 스케줄링
taskScheduler.schedule(() -> {
System.out.println("작업 시작 시간: " + LocalDateTime.now());
try {
Thread.sleep(5000); // 작업이 완료되는데 5초가 걸린다고 가정
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("작업 종료 시간: " + LocalDateTime.now());
}, new CronTrigger("*/2 * * * * *"));
}
// 작업 시작 시간: 2024-01-23T00:07:56.013512
// 작업 종료 시간: 2024-01-23T00:08:01.013926
// 작업 시작 시간: 2024-01-23T00:08:02.002003
// 작업 종료 시간: 2024-01-23T00:08:07.006692
// 작업 시작 시간: 2024-01-23T00:08:08.001003
// 작업 종료 시간: 2024-01-23T00:08:13.004278
(스케줄러의 동작 주기인 2초마다 반복되지 않고, task가 끝나야 다시 실행되는 결과를 볼 수 있습니다.)
즉, task의 작업 시간이 스케줄러의 동작 주기보다 길지만, 스케줄러는 동일한 주기로 계속 실행되기를 원하는 경우 다를 방법을 고려해보아야 합니다.
결과 값이 필요 없는 경우 task 내부적으로 비동기 메서드를 호출하는 방식도 가능합니다.
shutdown() method 주의사항
'ThreadPoolTaskScheduler' 사용에는 주의해야 할 부분이 있습니다. 바로 'shutdown()' 메서드인데요.
위 공식 문서의 설명에서 볼 수 있는 것처럼 해당 메서드는 이름과 다르게(?) 호출 시 진행 중이던 task가 종료되는 것을 기다린 이후 종료된다는 것입니다.
때문에 shutdown 요청을 했을 때, 진행 중인 테스트 작업을 마무리해도 된다면 상관이 없지만, 만약 해당 테스크가 시간이 너무 오래 걸리거나 리소스적인 측면에서 즉시 종료를 해야 한다면 추가적인 방법이 필요한데요.
이 경우 스레드를 강제 종료하기 위해서 'Thread.interrupt()'를 사용할 수 있습니다.
하지만 해당 메서드 역시 단순 호출만 하면 되는 것이 아니라 동작 원리를 파악하고 호출 이후 처리까지 해줘야 하는데요.
interrupt() 메서드는 스레드가 일시 정지 상태에 있을 때 'InterruptedException' 예외를 발생시키며, 해당 예외에 대한 처리를 통해 thread의 run() 메서드를 종료시키는 방식을 적용할 수 있습니다.
하지만 스레드가 일시 정지 상태가 되지 않는다면 'InterruptedException'이 발생하지 않는다는 문제가 있으며, 이 경우 'isInterrupted()' 메서드를 통해 해당 스레드가 interrupted 되었는지를 로직에서 직접 체크하여 스레드를 중단시킬 수 있습니다.
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(5);
taskScheduler.initialize();
// 스케줄링 주기가 2초로 설정된 작업 스케줄링
taskScheduler.schedule(() -> {
System.out.println("작업 시간: " + LocalDateTime.now());
}, new CronTrigger("*/2 * * * * *"));
쉽게 이야기하면 다음과 같이 2초 주기로 반복되는 스케줄러 작업을 실행했을 때, shutdown() 메서드를 호출해도 해당 task는 종료되지 않고 계속 실행된다는 것입니다.
(관련 내용에 관해서는 포스팅의 본 주제와 멀어지기 때문에 따로 정리하여 포스팅하도록 하겠습니다.)
* ThreadPoolTaskScheduler 클래스의 shutdown() 메서드 역시 동작 원리를 타고 들어가면 최종적으로 ThreadPoolExecutor 클래스에서 interruptIdleWorkers() 메서드를 통해 Thread.interrupt() 메서드가 실행되고 있습니다.
< 참고 자료 >
https://roomconerdeveloper.tistory.com/116
< Thread.interrupt() 관련 자료 >
https://velog.io/@dailylifecoding/java-executor-service-weird-exit-methods
https://ict-nroo.tistory.com/22