스프링부트 스케쥴러를 사용해보자

Kan9hee·2024년 3월 31일
0
post-thumbnail

주기적인 작업이 필요할 때

프로젝트를 진행하다 보면 정보의 업데이트나 금주의 주요 정보 분석 등 주기적인 동작을 구현해야 하곤 한다.
이 경우, 일반적으로 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>
-->

@EnableScheduling

스케줄링 작업을 구현하기에 앞서, 메인 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);
	}
}

@Scheduled

예약할 메서드를 표시하는 어노테이션이다.
스프링 빈으로 관리되는 영역 내에 선언하는 것으로, 해당 메서드를 설정한 값에 따라 주기적으로 실행한다.
cron속성으로 작업 시간대를 설정하는 방법과, fixedDelayfixedRate속성으로 작업 간격을 설정하는 방법이 있다.

• cron

고정 시간대 스케줄러 속성으로, 메서드의 실행을 정적으로 선언한 시간에 예약한다.
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

프레임 간격 스케줄러 속성으로, 메서드의 실행을 정적으로 선언한 프레임 간격마다 실행한다.
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

0개의 댓글