Updated:

1. 스케줄러 설정

스케줄러는 일정 시간 또는 간격마다 코드를 실행시켜준다.
Application에 @EnableScheduling 어노테이션을 붙이는 간단한 설정으로, Spring Boot에서 스케줄러 사용이 가능하다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

2. 스케줄러 수행 주기

이제는 스케줄러 클래스를 만들어서 작업 시간을 지정해보자
먼저 스케줄러 클래스는 스캔되어야 하기 때문에 상단에 @Component 어노테이션을 추가하고
스케줄링이 필요한 각 메소드 위에 @Scheduled 어노테이션을 추가하면 된다.
@Scheduled 에는 수행 주기를 선언해줄 수 있는데, 크게 cron, fixedDelay, fixedRate 3 종류가 있다.

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class Scheduler {

    // 매달 15일 0시 30분 0초
    @Scheduled(cron = "0 30 0 15 * *")
    public void monthSchedule() {
        System.out.println("monthSchedule 호출");
    }

    // 이전 호출이 완료된 시점부터 1초 후 실행
    @Scheduled(fixedDelay = 1000)
    public void delaySchedule() {
        Date now = new Date();
        System.out.println("delaySchedule 호출");
    }

    // 고정 비율로 1초마다 실행
    @Scheduled(fixedRate = 1000)
    public void rateSchedule() {
        System.out.println("rateSchedule 호출");
    }
}

cron의 포맷은 별 6개로 되어있는데, 각 위치마다 표현되는 의미가 다르다.
*(초[0-59]) *(분[0-59]) *(시간[0-23]) *(일[1-31]) *(월[1-12]) *(요일[0-7])
별을 쓰면 ALL에 해당하고, 숫자를 쓰면 해당 숫자 기간일 때만 실행된다. 그 밖에 다른 표현도 있지만 보통 잘 쓰이진 않아 보인다.

fixedDelay는 이전 호출이 완료된 시점부터 몇 초 후 실행시킬지 지정한다.
fixedRate는 이전 호출 완료 여부 상관없이 몇 초마다 실행시킬지 지정한다.

3. fixedDelay 와 fixedRate 를 같이 사용한다면?

여기서 드는 한 가지 의문점이 있다.
fixedRate는 고정 비율로 실행되므로, 비지니스 로직이 오래걸릴 경우 실행이 겹칠 수 있다.
허나 스케줄러의 디폴트 스레드 풀은 1개여서 병렬 처리되진 않을거고 실행이 끝날 때까지 대기상태가 될 것이다.
만약 fixedDelay랑 fixedRate를 같이 사용하면 어떻게 동작할까?
서로 사이좋게 메소드를 주거니 받거니 실행시킬까?

테스트를 위해서 코드를 추가하도록 한다.
메소드 실행 출력 시 스레드 이름과 출력 시간을 더 보여주도록 했으며
fixedRate에는 sleep을 3초 주었다. 병목 현상이 발생하여 다른 메소드가 실행되지 않는 지 확인하기 위함이다.

// 이전 호출이 완료된 시점부터 1초 후 실행
@Scheduled(fixedDelay = 1000)
public void delaySchedule() {
  System.out.println(Thread.currentThread().getName() + ": delaySchedule 호출, " + LocalDateTime.now());
  }

// 고정 비율로 1초마다 실행
@Scheduled(fixedRate = 1000)
public void rateSchedule() throws InterruptedException {
  System.out.println(Thread.currentThread().getName() + ": rateSchedule 호출, " + LocalDateTime.now());
  Thread.sleep(3000);
  }

테스트해본 결과 사이좋게 작업 순서를 주고받지는 않아 보인다.
스레드 1개로 fixedDelay와 fixedRate를 조합하여 사용하면 불규칙적으로 작업이 수행될 수 있으니 유의해야될 것 같다.

pool-1-thread-1: delaySchedule 호출, 2022-11-27T10:45:09.151207800
pool-1-thread-1: rateSchedule 호출, 2022-11-27T10:45:09.151207800
pool-1-thread-1: rateSchedule 호출, 2022-11-27T10:45:12.164715500
pool-1-thread-1: rateSchedule 호출, 2022-11-27T10:45:15.172611300
pool-1-thread-1: rateSchedule 호출, 2022-11-27T10:45:18.183962100
pool-1-thread-1: delaySchedule 호출, 2022-11-27T10:45:21.191221600
pool-1-thread-1: rateSchedule 호출, 2022-11-27T10:45:21.191221600
pool-1-thread-1: rateSchedule 호출, 2022-11-27T10:45:24.203031700
pool-1-thread-1: rateSchedule 호출, 2022-11-27T10:45:27.210661800
pool-1-thread-1: rateSchedule 호출, 2022-11-27T10:45:30.219421900

4. 스케줄러 비동기 처리 방법

자 그러면 병렬 처리를 해서라도 규칙적으로 실행시켜주고 싶으면 어떻게 하면 좋을까?
Application에 @EnableAsync 어노테이션을 추가하여 비동기적 실행을 선언할 수 있다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
@EnableAsync
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

그리고 스케줄러 스레드풀의 사이즈와 이름을 지정해줄 수 있다.
SchedulingConfigurer 인터페이스를 구현한 SchedulerConfig 클래스를 생성하자. 클래스 상단에 @Configuration 어노테이션도 추가해준다.
예제에서는 스레드풀 사이즈 5개와 prefix 이름을 “myThread” 로 지정했다.

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
    private final int POOL_SIZE = 5; //스레드풀 사이즈

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar){
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();

        threadPoolTaskScheduler.setPoolSize(POOL_SIZE);
        threadPoolTaskScheduler.setThreadNamePrefix("myThread");
        threadPoolTaskScheduler.initialize(); //Set up the ExecutorService.

        // 생성한 Thread pool을 작업용으로 등록
        taskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
    }
}

Application을 재실행해보면 서로 사이좋게 출력하는 것을 확인할 수 있다.
스레드풀도 5개여서 스레드 이름이 다양하게 찍히고 있다.

myThread2: rateSchedule 호출, 2022-11-27T11:03:58.636746600
myThread3: delaySchedule 호출, 2022-11-27T11:03:58.714490500
myThread3: delaySchedule 호출, 2022-11-27T11:03:59.720930900
myThread1: delaySchedule 호출, 2022-11-27T11:04:00.731601700
myThread2: rateSchedule 호출, 2022-11-27T11:04:01.646176200
myThread1: delaySchedule 호출, 2022-11-27T11:04:01.739551200
myThread1: delaySchedule 호출, 2022-11-27T11:04:02.746924900
myThread1: delaySchedule 호출, 2022-11-27T11:04:03.755806900
myThread2: rateSchedule 호출, 2022-11-27T11:04:04.656858900
myThread4: delaySchedule 호출, 2022-11-27T11:04:04.765389300

이처럼 비동기 처리는 빠른 처리속도를 안겨줄 수 있지만 조심히 사용해야 되는 것을 잊지 말자
독립적인 실행이 가능할때나 사용해야한다는 것을!

Leave a comment