프로젝트를 진행하다 보면 정보의 업데이트나 금주의 주요 정보 분석 등 주기적인 동작을 구현해야 하곤 한다.
이 경우, 일반적으로 Spring Scheduler를 사용한다.
필요한 패키지인 org.springframework.scheduling은 본래 spring-context 모듈 내에 있으나,
spring-boot-starter-web 내에도 포함되어 있어 해당 모듈을 사용하면 된다.
Gradle
dependencies{ implementation 'org.springframework.boot:spring-boot-starter-web' //implementation 'org.springframework:spring-context:6.1.5' }
Maven
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.1.5</version> </dependency> -->
스케줄링 작업을 구현하기에 앞서, 메인 Application Class에 @EnableScheduling을 선언해야 한다.
@EnableScheduling은 스프링에서 스케줄링을 활성화시키는 어노테이션으로, 스프링 컨테이너 내의 빈에서 후술할 어노테이션인
@Scheduled가 선언된 메서드를 찾아 활성화한다.
예시
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; @EnableScheduling @SpringBootApplication public class ExampleApplication { public static void main(String[] args) { SpringApplication.run(ExampleApplication.class, args); } }
예약할 메서드를 표시하는 어노테이션이다.
스프링 빈으로 관리되는 영역 내에 선언하는 것으로, 해당 메서드를 설정한 값에 따라 주기적으로 실행한다.
cron속성으로 작업 시간대를 설정하는 방법과, fixedDelay 및 fixedRate속성으로 작업 간격을 설정하는 방법이 있다.
고정 시간대 스케줄러 속성으로, 메서드의 실행을 정적으로 선언한 시간에 예약한다.
6개의 필드값을 띄어 쓴 문자열을 통해 설정하며, zone 속성과 병행하여 설정한 지역의 시간대를 기준으로 할 수 있다.
만약 zone 속성을 설정하지 않았을 경우, 자동으로 Local 시간대가 기준이 된다.
지원하는 zone 속성값은 여기에서 확인 가능하다.
형식
//필드값 6개, zone 속성 선언 @Scheduled(cron = "초(0~59) 분(0~59) 시간(0~23) 일(1~31) 월(1~12) 요일(0~6)", zone = "대륙/도시")
cron 표현식 특수문자
* : 해당 필드에서 사용 가능한 모든 값 (매일, 매주 등) ? : 어떤 값이든 상관 없음 - : 범위 내의 값 (시작값-종료값 -> 선언한 두 값까지 모두 포함한 범위에 해당하는 경우 동작) , : 복수의 값 (값1,값2,값3 -> 선언된 값에 해당하는 경우에만 동작) / : 시작시간과 반복 간격 (설정한 값부터 시작/설정한 값을 간격으로 동작) L : 선언된 필드 기준 마지막 값 W : 가장 가까운 평일 # : 특정 주간과 특정 요일 (주간#요일)
필드별 사용 가능한 표현식
초 : *-,/ 분 : *-,/ 시간 : *-,/ 일 : *?-,/LW 월 : JAN-DEC과 *-,/ 요일 : SUN-SAT과 *?-,/L#
예시
@Scheduled(cron = "0 0 0 15 * ?", zone = "Asia/Seoul") public void example1Method() { /* 서울 시간대를 기준으로 매달 15일 0시 0분 0초 정각에 실행하는 메서드. 요일 필드에 ?를 사용하여 어느 요일이든 상관없이 실행되도록 함. 만약 요일 필드값이 1일 경우, 그달 15일이 월요일인 경우에만 동작한다. */ }
프레임 간격 스케줄러 속성으로, 메서드의 실행을 정적으로 선언한 프레임 간격마다 실행한다.
fixedDelay는 메서드가 끝난 시점을 기준으로 한다.
fixedRate는 메서드가 시작한 시점을 기준으로 한다.
initialDelay는 상기한 두 속성과 병행하여 초기 지연시간을 설정할 수 있다.
fixedDelay와 fixedRate의 차이는 스케줄링 작업 수행 시간에 따른 설정값 준수 여부에 있다.
fixedDelay은 실질적으로 다음 스케줄러가 "현재 스케줄링 작업시간 + fixedDelay 설정값" 만큼의 시간이 지나야 실행된다.
반면 fixedRate는 설정값보다 현재 스케줄링 작업 수행 시간이 클 경우, 해당 시간이 지나야 실행된다.
형식
//fixedDelay @Scheduled(fixedDelay = 정수값) //fixedDelay, initialDelay선언 @Scheduled(fixedDelay = 정수값, initialDelay = 정수값) //fixedRate @Scheduled(fixedRate = 정수값)
예시
@Scheduled(fixedDelay = 7000, initialDelay = 3000) public void example2Method() { //3초의 대기시간 후, 이 메서드가 끝날 때마다 7초 간격으로 이 메서드를 실행한다. } @Scheduled(fixedRate = 4000) public void example3Method() { //4초 간격으로 이 메서드를 실행한다. //만약 이 메서드의 수행시간이 4초를 넘길 경우, 예약된 4초 시점에서 실행하지 않고 메서드가 끝난 뒤에 실행한다. }
여러개의 스케줄러를 사용하게 된다면, 각 스케줄링 작업이 밀려 의도한 간격으로 실행되지 않을 수 있다.
이는 @EnableScheduling이 스케줄링 작업에 대해 스레드가 하나만 있는 ThreadPool을 만들기 때문이다.
때문에 각 스케줄링 작업을 동시에 수행할 수 있도록 멀티 스레드를 활용해야 한다.
SchedulingConfigurer 인터페이스를 이용해 @Configuration 어노테이션이 선언된 클래스에서 스케줄된 작업에 대해 사용할 ThreadPool 범위를 설정할 수 있다.
예시
@Configuration public class SchedulerConfig implements SchedulingConfigurer { private final static int POOL_SIZE = 5; @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(POOL_SIZE); scheduler.initialize(); taskRegistrar.setTaskScheduler(scheduler); } }
멀티스레드를 활용해 여러개의 스케줄러를 다룰 수 있게 되었지만, 위의 방식은 하나의 스레드에 하나의 스케줄러를 Task로 등록하는 특성상 하나의 스케줄러를 중복해서 여러 스레드에 다룰 수는 없다.
이는 fixedRate를 사용할 때 두드러지는데, 현재 메서드의 작업 수행 시간에 상관 없이 설정값에 따라 작업하고 싶어도 이전 작업이 끝나지 않았다면 다른 스레드에서도 수행할 수 없음을 의미한다.
Async annotation을 이용한 비동기 스케줄링으로 해당 문제를 해결할 수 있다.
메인 Application Class에 @EnableAsync를 선언하고, 기존 스케줄링 메서드에 @Async를 추가하여 이전 스케줄링 작업에 상관없이 동일한 스케줄러를 다른 스레드에서 실행할 수 있다.
예시
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling @EnableAsync public class ExampleApplication { public static void main(String[] args) { SpringApplication.run(SchedulerDemoApplication.class, args); } }
@Scheduled(fixedRate = 4000) @Async public void fixedExample3Method() { //4초 간격으로 이 메서드를 실행한다. //만약 이 메서드의 수행시간이 4초를 넘길 경우, 다른 스레드에서 이 메서드를 실행한다. }
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/annotation/Scheduled.html#cron()
https://www.callicoder.com/spring-boot-task-scheduling-with-scheduled-annotation
https://medium.com/javarevisited/using-async-schedulers-in-spring-boot-78c15f9df466