spring / / 2023. 8. 21. 08:37

스프링 스케쥴러(Spring Scheduler)

스케쥴링은 특정 시간에 실행이 되는 일련의 작업이다. 스프링에서는 @Scheduled 어노테이션을 통해서 예약 작업을 구성할 수 있다.

@Scheduled가 붙은 메소드는 다음과 같은 규칙이 있다.

  • 일반적으로 리턴 타입이 없다. (있다면 무시될 것이다)
  • 어떤 파라미터도 받아서는 안된다.

스케쥴링 사용

스프링에서 스케쥴 작업과 @Scheduled 어노테이션을 사용하기 위해서 아래와 같은 방법으로 @EnableScheduling을 추가한다.

1. Spring Boot에 추가하는 방법

@SpringBootApplication
@EnableScheduling
public class SpringSchedulerDemo {

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

2. Configuration에 추가하는 방법

@Configuration
@EnableScheduling
public class SchedulerConfig {
    ...
}

Fixed Delay 사용

이 방식은 작업 실행 시 정해진 시간동안 대기하는 방식이다.

@Scheduled(fixedDelay = 1000)
public void fixedDelay() {
    log.info("fixed delay");
}

여기서 fixedDelay는 마지막 실행다음 실행 사이의 간격이 고정이라는 의미이다. 작업은 항상 이전 작업이 끝날때 까지 대기한다. 이 작업은 이전 작업이 다음 실행 전에 끝나야 할 경우에 사용된다.

실행 결과
2023-08-20 19:37:58.333  INFO 3907 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:37:59.335  INFO 3907 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:38:00.337  INFO 3907 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:38:01.341  INFO 3907 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:38:02.344  INFO 3907 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:38:03.349  INFO 3907 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:38:04.350  INFO 3907 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:38:05.351  INFO 3907 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay

시간이 정확하게 1초 단위는 아니지만 거의 1초 간격으로 실행이 된다. 하지만 실행되는 시간이 점점 증가되는 증상이 있다.

(xx.333에서 실행해서 나중에는 xx.351에서 실행이 된다. 점점 시간이 증가되는 현상)

Fixed Rate 사용

이 방식은 특정 고정된 시간 간격으로 작업을 실행하는 방식이다. 이 옵션은 각 작업의 실행이 무관할 때 사용된다.

작업은 기본적으로 병렬로 실행되지 않는다는 것을 주의하자. 그래서 fixedRate를 사용할 지라도, 다음 작업은 이전 작업이 완료될 때까지 실행되지 않을 것이다.

@Scheduled(fixedRate = 1000)
public void fixedRate() {
    log.info("fixed rate");
}
실행결과
2023-08-20 19:40:18.709  INFO 4857 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:40:19.705  INFO 4857 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:40:20.711  INFO 4857 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:40:21.710  INFO 4857 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:40:22.709  INFO 4857 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:40:23.708  INFO 4857 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:40:24.706  INFO 4857 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:40:25.709  INFO 4857 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:40:26.707  INFO 4857 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:40:27.709  INFO 4857 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate

이 방식 또한 시간이 정확하게 1초 단위는 아니지만 거의 1초 간격으로 실행이 된다. 하지만 시간이 지속적으로 증가되지 않고 일시적으로 증가하더라도 다시 처음 시작했던 시간으로 내려와서 실행되는 현상이 있다.

Fixed Delay vs Fixed Rate 비교

Fixed Delay와 Fixed Rate를 좀 더 정확한 차이를 비교하기 위해 0.5초 간 대기를 걸어주면 더 명확히 알 수가 있다.

fixedDelay + 500ms(delay)
@Scheduled(fixedDelay = 1000)
public void fixedDelay() {
    log.info("fixed delay");
    sleep(500);
}
실행결과
2023-08-20 19:54:12.537  INFO 7229 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:54:14.042  INFO 7229 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:54:15.549  INFO 7229 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:54:17.053  INFO 7229 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:54:18.558  INFO 7229 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:54:20.067  INFO 7229 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:54:21.572  INFO 7229 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:54:23.074  INFO 7229 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay

거의 1.5초 마다 실행이 된다. 즉, 마지막 실행이 완료되고 1초 후에 실행이 된다.

fixedRate + 500ms(delay)
@Scheduled(fixedRate = 1000)
public void fixedRate() {
    log.info("fixed rate");
    sleep(500);
}
실행결과
2023-08-20 19:55:41.032  INFO 8086 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:55:42.038  INFO 8086 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:55:43.034  INFO 8086 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:55:44.030  INFO 8086 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:55:45.031  INFO 8086 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:55:46.032  INFO 8086 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:55:47.033  INFO 8086 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:55:48.034  INFO 8086 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 19:55:49.035  INFO 8086 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate

거의 1초 마다 실행이 된다.

Fixed Delay vs Fixed Rate의 실행시간이 긴 경우

1초 마다 실행이 되는데 2초의 delay가 있는 경우

fixedDelay + 2000ms(delay)

@Scheduled(fixedDelay = 1000)
    public void fixedDelayWithLongDelay() {
        log.info("fixed delay");
        sleep(2000);
    }
실행결과
2023-08-20 19:59:20.563  INFO 8647 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:59:23.569  INFO 8647 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:59:26.574  INFO 8647 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:59:29.577  INFO 8647 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:59:32.582  INFO 8647 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:59:35.584  INFO 8647 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:59:38.591  INFO 8647 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay
2023-08-20 19:59:41.599  INFO 8647 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay

3초 마다 실행이 된다. 실행이 1초 간격 + 2초 대기이므로 3초 마다 실행이 된다.

fixedRate + 2000ms(delay)

@Scheduled(fixedRate = 1000)
public void fixedRateWithLongDelay() {
    log.info("fixed rate");
    sleep(2000);
}
실행결과
2023-08-20 20:02:10.165  INFO 9251 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 20:02:12.168  INFO 9251 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 20:02:14.172  INFO 9251 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 20:02:16.176  INFO 9251 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 20:02:18.177  INFO 9251 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 20:02:20.182  INFO 9251 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate
2023-08-20 20:02:22.183  INFO 9251 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate

2초마다 실행이 된다. 1초 간격 + 2초 대기인데 1초마다 실행이 되어야 하지만 2초 대기가 되니 2초마다 실행이 되는 것이다.

이는 싱글쓰레드에서 실행이 되므로 발생하는 문제이다. 쓰레드 이름을 찍어보면 동일한 쓰레드 이름이 표시되는 것을 확인할 수 있다.

@Scheduled(fixedRate = 1000)
public void fixedRateWithLongDelay() {
    log.info("fixed rate " + Thread.currentThread().getName());
    sleep(2000);
}
실행결과
2023-08-20 20:21:21.336  INFO 13298 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate scheduling-1
2023-08-20 20:21:23.341  INFO 13298 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate scheduling-1
2023-08-20 20:21:25.343  INFO 13298 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate scheduling-1
2023-08-20 20:21:27.346  INFO 13298 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate scheduling-1
2023-08-20 20:21:29.348  INFO 13298 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate scheduling-1
2023-08-20 20:21:31.351  INFO 13298 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate scheduling-1

ThreadPool 사용하기

스프링 스케쥴러는 기본적으로 싱글쓰레드로 실행이 된다. 그래서 멀티쓰레드로 구성하기 위해서는 아래와 같은 설정이 필요하다.

@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
    private final static int POOL_SIZE = 10;

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

        threadPoolTaskScheduler.setPoolSize(POOL_SIZE);
        threadPoolTaskScheduler.setThreadNamePrefix("multi-scheduler-");
        threadPoolTaskScheduler.initialize();

        taskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
    }
}

10개의 쓰레드로 실행을 해보자.

fixedDelay + 2000ms(delay)

@Scheduled(fixedDelay = 1000)
public void fixedDelayWithLongDelay() {
    log.info("fixed delay " + Thread.currentThread().getName());
    sleep(2000);
}
실행결과
2023-08-20 20:35:41.685  INFO 16463 --- [lti-scheduler-1] c.e.scheduler.service.SchedulerService   : fixed delay multi-scheduler-1
2023-08-20 20:35:44.688  INFO 16463 --- [lti-scheduler-2] c.e.scheduler.service.SchedulerService   : fixed delay multi-scheduler-2
2023-08-20 20:35:47.696  INFO 16463 --- [lti-scheduler-1] c.e.scheduler.service.SchedulerService   : fixed delay multi-scheduler-1
2023-08-20 20:35:50.696  INFO 16463 --- [lti-scheduler-3] c.e.scheduler.service.SchedulerService   : fixed delay multi-scheduler-3
2023-08-20 20:35:53.699  INFO 16463 --- [lti-scheduler-2] c.e.scheduler.service.SchedulerService   : fixed delay multi-scheduler-2
2023-08-20 20:35:56.704  INFO 16463 --- [lti-scheduler-4] c.e.scheduler.service.SchedulerService   : fixed delay multi-scheduler-4
2023-08-20 20:35:59.711  INFO 16463 --- [lti-scheduler-1] c.e.scheduler.service.SchedulerService   : fixed delay multi-scheduler-1

3초 마다 실행된다. 여러개의 쓰레드로 실행이 되어도 3초 마다 실행이 된다.

fixedRate + 2000ms(delay)

@Scheduled(fixedRate = 1000)
public void fixedRateWithLongDelay() {
    log.info("fixed rate " + Thread.currentThread().getName());
    sleep(2000);
}
실행결과
2023-08-20 20:37:28.246  INFO 16779 --- [lti-scheduler-3] c.e.scheduler.service.SchedulerService   : fixed rate multi-scheduler-3
2023-08-20 20:37:30.248  INFO 16779 --- [lti-scheduler-3] c.e.scheduler.service.SchedulerService   : fixed rate multi-scheduler-3
2023-08-20 20:37:32.253  INFO 16779 --- [lti-scheduler-3] c.e.scheduler.service.SchedulerService   : fixed rate multi-scheduler-3
2023-08-20 20:37:34.258  INFO 16779 --- [lti-scheduler-3] c.e.scheduler.service.SchedulerService   : fixed rate multi-scheduler-3
2023-08-20 20:37:36.261  INFO 16779 --- [lti-scheduler-3] c.e.scheduler.service.SchedulerService   : fixed rate multi-scheduler-3
2023-08-20 20:37:38.262  INFO 16779 --- [lti-scheduler-3] c.e.scheduler.service.SchedulerService   : fixed rate multi-scheduler-3
2023-08-20 20:37:40.262  INFO 16779 --- [lti-scheduler-3] c.e.scheduler.service.SchedulerService   : fixed rate multi-scheduler-3
2023-08-20 20:37:42.267  INFO 16779 --- [lti-scheduler-3] c.e.scheduler.service.SchedulerService   : fixed rate multi-scheduler-3

2초 마다 실행이 된다.

Async 사용하기

ThreadPool을 사용하면 여러 개의 쓰레드가 동시에 실행되어 정상적으로 동작할 것으로 생각이 되었지만 그렇지 않았다. (fixedRate=1000로 설정이 되었지만 1초마다 설정이 되지 않았다)

작업을 정해진 시간에 병렬 실행을 하기 위해서는 @Async를 사용하면 된다.

Async는 비동기로 실행하게 해주는 기능이며 @EnableAsync를 추가해야 정상작동한다.

@SpringBootApplication
@EnableScheduling
@EnableAsync
public class SpringSchedulerDemo {

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

fixedDelay + 2000ms(delay)

@Scheduled(fixedRate = 1000)
@Async
public void fixedRateWithAsync() {
    log.info("fixed rate " + Thread.currentThread().getName());
    sleep(2000);
}
실행결과
2023-08-21 06:19:49.572  INFO 97668 --- [         task-1] c.e.scheduler.service.SchedulerService   : fixed delay task-1
2023-08-21 06:19:50.573  INFO 97668 --- [         task-2] c.e.scheduler.service.SchedulerService   : fixed delay task-2
2023-08-21 06:19:51.575  INFO 97668 --- [         task-3] c.e.scheduler.service.SchedulerService   : fixed delay task-3
2023-08-21 06:19:52.578  INFO 97668 --- [         task-4] c.e.scheduler.service.SchedulerService   : fixed delay task-4
2023-08-21 06:19:53.581  INFO 97668 --- [         task-5] c.e.scheduler.service.SchedulerService   : fixed delay task-5
2023-08-21 06:19:54.584  INFO 97668 --- [         task-6] c.e.scheduler.service.SchedulerService   : fixed delay task-6
2023-08-21 06:19:55.585  INFO 97668 --- [         task-7] c.e.scheduler.service.SchedulerService   : fixed delay task-7

1초 마다 실행이 된다. 즉, 2초의 대기가 있어도 여러 개의 쓰레드가 비동기로 실행이 되어 매 1초 마다 실행이 된다.

fixedRate + 2000ms(delay)

@Scheduled(fixedRate = 1000)
@Async
public void fixedRateWithAsync() {
    log.info("fixed rate " + Thread.currentThread().getName());
    sleep(2000);
}
실행결과
2023-08-21 06:20:39.605  INFO 97684 --- [         task-1] c.e.scheduler.service.SchedulerService   : fixed rate task-1
2023-08-21 06:20:40.605  INFO 97684 --- [         task-2] c.e.scheduler.service.SchedulerService   : fixed rate task-2
2023-08-21 06:20:41.600  INFO 97684 --- [         task-3] c.e.scheduler.service.SchedulerService   : fixed rate task-3
2023-08-21 06:20:42.601  INFO 97684 --- [         task-4] c.e.scheduler.service.SchedulerService   : fixed rate task-4
2023-08-21 06:20:43.603  INFO 97684 --- [         task-5] c.e.scheduler.service.SchedulerService   : fixed rate task-5
2023-08-21 06:20:44.607  INFO 97684 --- [         task-6] c.e.scheduler.service.SchedulerService   : fixed rate task-6
2023-08-21 06:20:45.602  INFO 97684 --- [         task-7] c.e.scheduler.service.SchedulerService   : fixed rate task-7
2023-08-21 06:20:46.602  INFO 97684 --- [         task-8] c.e.scheduler.service.SchedulerService   : fixed rate task-8

1초 마다 실행이 된다. 이 경우도 동일하다. 2초의 대기가 있어도 여러 개의 쓰레드가 비동기로 실행이 되어 1초 마다 실행이 된다.

Initial Delay로 작업 스케쥴링

@Scheduled(initialDelay = 5000, fixedRate = 1000)
public void fixedRateWithInitialDelayLongDelay() {
    log.info("fixed rate " + Thread.currentThread().getName());
}

이 작업은 initialDelay값 후에 최초 실행이 될 것이다.

Cron 표현식 사용

@Scheduled(cron = "0 15 10 15 * ?")
public void cron() {
    log.info("cron " + Thread.currentThread().getName());
}

이 작업은 매달 15일, 10:15 AM에 실행된다.

스케쥴링 파라미터화 하기

스프링 표현식을 외부 파라미터화할 수 있다.

// fixedDelay
@Scheduled(fixedDelayString = "${fixedDelay.in.milliseconds}")
// fixedRate
@Scheduled(fixedRateString = "${fixedRate.in.milliseconds}")

Interval-in-cron=0 * * * * *

┌───────────── second (0-59)
 │ ┌───────────── minute (0 - 59)
 │ │ ┌───────────── hour (0 - 23)
 │ │ │ ┌───────────── day of the month (1 - 31)
 │ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
 │ │ │ │ │ ┌───────────── day of the week (0 - 7)
 │ │ │ │ │ │          (or MON-SUN -- 0 or 7 is Sunday)
 │ │ │ │ │ │
 * * * * * *

병렬 실행

기본적으로, 스프링은 작업을 실행하기 위해 싱글쓰레드를 사용한다. 여러 개의 @Scheduled 메소드를 사용하더라도 이전 작업이 완료될 때까지 대기해야 한다.

@Scheduled(fixedDelay = 1000)
public void fixedDelayWithLongDelay() {
    log.info("fixed delay " + Thread.currentThread().getName());
    sleep(2000);
}

@Scheduled(fixedRate = 1000)
public void fixedRateWithLongDelay() {
    log.info("fixed rate " + Thread.currentThread().getName());
    sleep(2000);
}

위의 2개의 작업이 하나의 쓰레드로 동작하면 정상적으로 동작하지 않는다.

실행결과
2023-08-21 06:53:46.561  INFO 98994 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate scheduling-1
2023-08-21 06:53:48.563  INFO 98994 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay scheduling-1
2023-08-21 06:53:50.564  INFO 98994 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate scheduling-1
2023-08-21 06:53:52.566  INFO 98994 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate scheduling-1
2023-08-21 06:53:54.572  INFO 98994 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate scheduling-1
2023-08-21 06:53:56.576  INFO 98994 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate scheduling-1
2023-08-21 06:53:58.582  INFO 98994 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate scheduling-1
2023-08-21 06:54:00.587  INFO 98994 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed delay scheduling-1
2023-08-21 06:54:02.592  INFO 98994 --- [   scheduling-1] c.e.scheduler.service.SchedulerService   : fixed rate scheduling-1

여러 개의 쓰레드를 사용하여 동작시켜 보자.

@Bean
public TaskScheduler  taskScheduler() {
    ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
    threadPoolTaskScheduler.setPoolSize(5);
    threadPoolTaskScheduler.setThreadNamePrefix("multi-scheduler-");
    return threadPoolTaskScheduler;
}
실행결과
2023-08-21 06:57:38.173  INFO 99125 --- [lti-scheduler-1] c.e.scheduler.service.SchedulerService   : fixed rate multi-scheduler-1
2023-08-21 06:57:38.173  INFO 99125 --- [lti-scheduler-2] c.e.scheduler.service.SchedulerService   : fixed delay multi-scheduler-2
2023-08-21 06:57:40.174  INFO 99125 --- [lti-scheduler-2] c.e.scheduler.service.SchedulerService   : fixed rate multi-scheduler-2
2023-08-21 06:57:41.179  INFO 99125 --- [lti-scheduler-3] c.e.scheduler.service.SchedulerService   : fixed delay multi-scheduler-3
2023-08-21 06:57:42.179  INFO 99125 --- [lti-scheduler-1] c.e.scheduler.service.SchedulerService   : fixed rate multi-scheduler-1
2023-08-21 06:57:44.180  INFO 99125 --- [lti-scheduler-2] c.e.scheduler.service.SchedulerService   : fixed rate multi-scheduler-2
2023-08-21 06:57:44.185  INFO 99125 --- [lti-scheduler-5] c.e.scheduler.service.SchedulerService   : fixed delay multi-scheduler-5
2023-08-21 06:57:46.182  INFO 99125 --- [lti-scheduler-3] c.e.scheduler.service.SchedulerService   : fixed rate multi-scheduler-3

멀티 쓰레드가 각 작업별로 실행이 되어 예상한 시간대로 동작을 한다.

스프링부트 설정

스케쥴러의 풀 사이즈를 증가시키기 위해서 아래 설정을 추가하면 된다.

spring:
  task.scheduling.pool.size: 5

ShedLock으로 분산환경에서 스케쥴링 인스턴스 사용

@Scheduler 어노테이션으로 스케쥴 잡을 사용하기는 쉬우나 분산 환경에서는 여러 인스턴스의 스케쥴링 동기화를 처리하기 어렵다.

ShedLock은 여러 인스턴스(분산 환경)에서 동시에 최대 한번만 실행되게 해주는 라이브러리다. 이 방식은 작업이 실행될 때 다른 작업이 실행되지 못하게 락(Lock) 매커니즘을 사용한다.

ShedLock은 여러 인스턴스에 외부 데이터 저장소를 사용한다. (Mongo, JDBC, Redis, Hazelcast, Zookeeper 등)

의존성 추가
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-spring</artifactId>
    <version>4.27.0</version>
</dependency>

<dependency>
  <groupId>net.javacrumbs.shedlock</groupId>
  <artifactId>shedlock-provider-jdbc-template</artifactId>
  <version>4.27.0</version>
</dependency>

<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>runtime</scope>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
lockProvider 추가
@Configuration
@EnableSchedulerLock(defaultLockAtLeastFor = "PT1S", defaultLockAtMostFor = "PT5S")
public class SchedulerLockJdbcConfig {

    @Bean
    public LockProvider lockProvider(DataSource dataSource) {
        return new JdbcTemplateLockProvider(
            JdbcTemplateLockProvider.Configuration.builder()
                .withJdbcTemplate(new JdbcTemplate(dataSource))
                .usingDbTime() // Works on Postgres, MySQL, MariaDb, MS SQL, Oracle, DB2, HSQL and H2
                .build()
        );
    }
}

defaultLockAtLeastFor은 메소드 간의 시간의 차이를 나타낸다. PT1S는 최초 1초간은 락을 걸겠다는 의미.

defaultLockAtMostFor은 실행하다가 노드가 죽었을 때 락을 얼마나 길게 유지할 것인지를 나타내는 시간. PT5S는 5초후에 락을 풀겠다는 의미



테이블 생성
DROP TABLE IF EXISTS shedlock;

CREATE TABLE shedlock(
  name VARCHAR(64) NOT NULL, 
  lock_until TIMESTAMP(3) NOT NULL,
  locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 
  locked_by VARCHAR(255) NOT NULL, 
  PRIMARY KEY (name)
);

@SchedulerLock 어노테이션을 적용함으로써 스케쥴링을 할 수 있다.

@Scheduled(fixedDelay = 3000)
@SchedulerLock(name = "myscheduledTask")
public void fixedRateWithShedLock() {
    log.info("shedLock " + Thread.currentThread().getName());
}

두 개의 Spring Application을 실행해보자.

[1번 서버]

# 없음

[2번 서버]

2023-08-21 07:48:18.833  INFO 1516 --- [lti-scheduler-1] c.e.scheduler.service.SchedulerService   : shedLock multi-scheduler-1
2023-08-21 07:48:21.844  INFO 1516 --- [lti-scheduler-1] c.e.scheduler.service.SchedulerService   : shedLock multi-scheduler-1
2023-08-21 07:48:24.848  INFO 1516 --- [lti-scheduler-2] c.e.scheduler.service.SchedulerService   : shedLock multi-scheduler-2
2023-08-21 07:51:37.222  INFO 1516 --- [ti-scheduler-10] c.e.scheduler.service.SchedulerService   : shedLock multi-scheduler-10
2023-08-21 07:51:40.225  INFO 1516 --- [ti-scheduler-10] c.e.scheduler.service.SchedulerService   : shedLock multi-scheduler-10
2023-08-21 07:51:43.227  INFO 1516 --- [ti-scheduler-10] c.e.scheduler.service.SchedulerService   : shedLock multi-scheduler-10
2023-08-21 07:51:46.229  INFO 1516 --- [ti-scheduler-10] c.e.scheduler.service.SchedulerService   : shedLock multi-scheduler-10

2번 서버에서만 3초마다 실행이 된다. 2번 서버를 죽이면 1번 서버에서 실행이 다시 된다.

참고

https://www.baeldung.com/spring-scheduled-tasks

https://reflectoring.io/spring-scheduler/

https://seolin.tistory.com/123

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유