BackEnd🌱/Spring

@Scheduled μ‚¬μš©ν•  λ•Œ μŠ€λ ˆλ“œ μ„€μ •

dkswnkk 2023. 11. 9. 21:52

κ°œμš”

μŠ€ν”„λ§ ν”„λ ˆμž„μ›Œν¬μ—μ„œ μ œκ³΅ν•˜λŠ” @Scheduled μ–΄λ…Έν…Œμ΄μ…˜μ€ λ©”μ„œλ“œμ— μŠ€μΌ€μ€„λ§ κΈ°λŠ₯을 λΆ€μ—¬ν•˜λŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€. 기본적으둜 @Scheduled μ–΄λ…Έν…Œμ΄μ…˜λ§Œμ„ μ‚¬μš©ν•˜λ©΄ μŠ€ν”„λ§μ€ 단일 μŠ€λ ˆλ“œμ—μ„œ μŠ€μΌ€μ€„λ§ μž‘μ—…λ“€μ„ 순차적으둜 μ²˜λ¦¬ν•˜λŠ”λ°, μ΄λŠ” ν•˜λ‚˜μ˜ μŠ€μΌ€μ€„λ§ μž‘μ—…μ΄ μ™„λ£Œλ˜μ–΄μ•Όλ§Œ λ‹€μŒ μŠ€μΌ€μ€„λ§ μž‘μ—…μ΄ 싀행될 수 μžˆλ‹€λŠ” 것을 μ˜λ―Έν•©λ‹ˆλ‹€.

λ”°λΌμ„œ 뢀가적인 μ„€μ • 없이 μ—¬λŸ¬ 개의 μŠ€μΌ€μ€„λŸ¬λ₯Ό μž‘μ„±ν•˜λ©΄, 일뢀 μž‘μ—…μ΄ μ˜ˆμƒμΉ˜ λͺ»ν•œ μ‹œκ°„μ— λ™μž‘ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

μ˜ˆμ‹œ 상황을 ν•œλ²ˆ λ³΄κ² μŠ΅λ‹ˆλ‹€.

@Configuration
@EnableScheduling
public class ScheduledTasks {
    private final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);

    @Scheduled(fixedRate = 1000) // 1μ΄ˆλ§ˆλ‹€ taskAλ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.
    public void taskA() throws InterruptedException {
        Thread.sleep(10000); // 10초 λ™μ•ˆ μΌμ‹œ μ€‘λ‹¨ν•©λ‹ˆλ‹€.
        log.info("taskA - {} - {}", LocalDateTime.now(), Thread.currentThread().getName());
    }

    @Scheduled(fixedRate = 1000) // 1μ΄ˆλ§ˆλ‹€ taskBλ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.
    public void taskB() {
        // taskB의 λ‘œμ§μ„ μ‹€ν–‰ν•©λ‹ˆλ‹€.
        log.info("taskB - {} - {}", LocalDateTime.now(), Thread.currentThread().getName());
    }

μ‹€ν–‰ κ²°κ³Ό

μœ„μ˜ μ˜ˆμ‹œμ—μ„œ taskA와 taskBλŠ” 각각 1μ΄ˆλ§ˆλ‹€ μ‹€ν–‰λ˜λ„λ‘ μ„€μ •λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜ μ˜ˆμƒκ³Ό 달리 μ‹€μ œλ‘œ 둜그λ₯Ό 확인해 보면, taskBκ°€ 10μ΄ˆμ— ν•œ 번만 μ‹€ν–‰λ˜λŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

μ΄λŠ” @Scheduled μ–΄λ…Έν…Œμ΄μ…˜μ˜ κΈ°λ³Έ 섀정이 단일 μŠ€λ ˆλ“œμ—μ„œ μž‘μ—…μ„ μˆ˜ν–‰ν•˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€. 이에 따라, taskA와 taskBλŠ” λ™μΌν•œ μŠ€λ ˆλ“œμ—μ„œ 순차적으둜 μ‹€ν–‰λ˜λ©°, taskAλŠ” 10초 λ™μ•ˆ μΌμ‹œ μ€‘λ‹¨λ˜λ―€λ‘œ, taskBλŠ” taskA의 μˆ˜ν–‰μ΄ 끝날 λ•ŒκΉŒμ§€ κΈ°λ‹€λ €μ•Ό ν•©λ‹ˆλ‹€. μ‹€μ œλ‘œ λ‘œκ·Έμ—μ„œ 'scheduling-1'μ΄λΌλŠ” λ™μΌν•œ μŠ€λ ˆλ“œμ—μ„œ 두 μž‘μ—…μ΄ μ‹€ν–‰λ˜λŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

이 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄μ„œλŠ” 뢀가적인 섀정이 ν•„μš”ν•œλ°, 크게 λ‹€μŒκ³Ό 같이 μ„Έ 가지 방법이 μ‘΄μž¬ν•©λ‹ˆλ‹€.

  1. Async 처리
  2. SchedulingConfigurer κ΅¬ν˜„
  3. TaskScheduler 빈 등둝

 
 

1. Async 처리

μŠ€ν”„λ§μ˜ @Async μ–΄λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•˜λ©΄ λ©”μ„œλ“œκ°€ λ³„λ„μ˜ μŠ€λ ˆλ“œμ—μ„œ λΉ„λ™κΈ°μ μœΌλ‘œ μ‹€ν–‰λ©λ‹ˆλ‹€. 이λ₯Ό 톡해 μ—¬λŸ¬ μž‘μ—…λ“€μ„ λ™μ‹œμ— μ²˜λ¦¬ν•  수 있게 λ©λ‹ˆλ‹€.

@Configuration
@EnableAsync
@EnableScheduling
public class ScheduledTasks {
    private final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);

    @Async
    @Scheduled(fixedRate = 1000)
    public void taskA() throws InterruptedException {
        Thread.sleep(10000);
        log.info("taskA - {} - {}", LocalDateTime.now(), Thread.currentThread().getName());
    }

    @Async
    @Scheduled(fixedRate = 1000)
    public void taskB() {
        log.info("taskB - {} - {}", LocalDateTime.now(), Thread.currentThread().getName());
    }
}

μ‹€ν–‰ κ²°κ³Ό

μœ„μ˜ μ½”λ“œμ—μ„œ, taskA와 taskBλŠ” 각각 1μ΄ˆλ§ˆλ‹€ λ³„λ„μ˜ μŠ€λ ˆλ“œμ—μ„œ λΉ„λ™κΈ°λ‘œ μ‹€ν–‰λ˜λ„λ‘ μ„€μ •λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. μ΄λ ‡κ²Œ μ„€μ •ν•˜λ©΄ taskA의 싀행이 taskB에 영ν–₯을 주지 μ•Šκ²Œ λ©λ‹ˆλ‹€. μ‹€ν–‰ 둜그λ₯Ό 확인해 보면, μ‹€μ œλ‘œ taskA와 taskBκ°€ 각각 1μ΄ˆλ§ˆλ‹€ λ…λ¦½μ μœΌλ‘œ λ™μž‘ν•˜λŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

μ΄λŠ” @Async μ–΄λ…Έν…Œμ΄μ…˜μ˜ κΈ°λ³Έ 섀정이 SimpleAsyncTaskExecutorλ₯Ό μ‚¬μš©ν•˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€. νŠΉνžˆ taskA의 경우 이전 μ„€μ •μ—μ„œλŠ” 10초 λ™μ•ˆ μΌμ‹œ μ€‘λ‹¨λ˜μ—ˆμ§€λ§Œ, 이번 μ„€μ •μ—μ„œλŠ” 1μ΄ˆλ§ˆλ‹€ μƒˆλ‘œμš΄ μŠ€λ ˆλ“œμ—μ„œ λ…λ¦½μ μœΌλ‘œ μˆ˜ν–‰λ˜λŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. μ΄λ ‡κ²Œ λΉ„λ™κΈ°λ‘œ μˆ˜ν–‰ν•˜λ©΄ λ³„λ„μ˜ μŠ€λ ˆλ“œμ—μ„œ λ…λ¦½μ μœΌλ‘œ μž‘μ—…μ„ μˆ˜ν–‰ν•˜λ―€λ‘œ, ν•œ μž‘μ—…μ˜ 지연이 λ‹€λ₯Έ μž‘μ—…μ— 영ν–₯을 λ―ΈμΉ˜λŠ” 것을 방지할 수 μžˆμŠ΅λ‹ˆλ‹€.
 
 

2. SchedulingConfigurer κ΅¬ν˜„

SchedulingConfigurer μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜λ©΄, μŠ€μΌ€μ€„λŸ¬μ˜ λ™μž‘μ„ λ”μš± μ„ΈλΆ€μ μœΌλ‘œ μ œμ–΄ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό 톡해 μŠ€μΌ€μ€„λŸ¬κ°€ μ‚¬μš©ν•˜λŠ” μŠ€λ ˆλ“œ ν’€μ˜ 크기λ₯Ό μ‘°μ •ν•˜κ±°λ‚˜ μŠ€λ ˆλ“œμ˜ 이름을 λ³€κ²½ν•˜λŠ” λ“±μ˜ μž‘μ—…μ„ μˆ˜ν–‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

@Configuration
@EnableScheduling
public class SchedulerConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();

        threadPoolTaskScheduler.setPoolSize(10);
        threadPoolTaskScheduler.setThreadNamePrefix("my-scheduler-");
        threadPoolTaskScheduler.initialize();

        taskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
    }
}

μœ„ μ˜ˆμ‹œμ—μ„œλŠ” SchedulingConfigurer μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜λŠ” SchedulerConfig ν΄λž˜μŠ€κ°€ μŠ€λ ˆλ“œ ν’€μ˜ 크기λ₯Ό 10으둜 μ„€μ •ν•˜κ³ , μŠ€λ ˆλ“œ μ΄λ¦„μ˜ 접두사λ₯Ό "my-scheduler-"둜 μ„€μ •ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. μ΄λ ‡κ²Œ ν•˜λ©΄, μŠ€μΌ€μ€„λ§ μž‘μ—…μ€ "my-scheduler-" 접두사λ₯Ό 가진 10개의 μŠ€λ ˆλ“œμ—μ„œ λ™μ‹œμ— 처리될 수 있게 λ©λ‹ˆλ‹€.

μ‹€ν–‰ κ²°κ³Ό

μ‹€ν–‰ κ²°κ³Όλ₯Ό 보면 taskA와 taskBκ°€ 각각 λ³„λ„μ˜ μŠ€λ ˆλ“œμ—μ„œ μˆ˜ν–‰λ˜λŠ” 것을 μ•Œ 수 μžˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ 비동기 처리λ₯Ό μ‚¬μš©ν–ˆμ„ λ•Œμ™€λŠ” 달리 taskA의 ν•œ 번의 싀행이 λλ‚˜μ•Ό κ·Έλ‹€μŒ taskA의 싀행이 μ‹œμž‘λ˜λŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

이λ₯Ό 톡해, λ™μΌν•œ μž‘μ—…μ— λŒ€ν•΄μ„œλŠ” 순차적인 싀행을 보μž₯ν•˜λ©΄μ„œλ„, λ‹€λ₯Έ μž‘μ—…λ“€μ— λŒ€ν•΄μ„œλŠ” 병렬적인 싀행을 κ°€λŠ₯ν•˜κ²Œ ν•˜λŠ” μŠ€μΌ€μ€„λ§ 섀정을 ꡬ성할 수 μžˆμŠ΅λ‹ˆλ‹€.
 

 

3. TaskScheduler 빈 등둝

TaskSchedulerλ₯Ό μŠ€ν”„λ§ 빈으둜 λ“±λ‘ν•˜λŠ” 방법은 μ„€μ • 방식이 κ°„λ‹¨ν•˜λ‹€λŠ” μž₯점이 μžˆμŠ΅λ‹ˆλ‹€.

이 방법도 SchedulingConfigurer을 κ΅¬ν˜„ν•˜λŠ” 것과 λ§ˆμ°¬κ°€μ§€λ‘œ μŠ€μΌ€μ€„λ§ κΈ°λŠ₯을 ν™œμ„±ν™”ν•˜κ³ , μŠ€λ ˆλ“œ ν’€μ˜ 크기와 μŠ€λ ˆλ“œ μ΄λ¦„μ˜ 접두사λ₯Ό μ„€μ •ν•˜λŠ” 것은 λ™μΌν•©λ‹ˆλ‹€. ν•˜μ§€λ§Œ μŠ€μΌ€μ€„λ§ 섀정을 μŠ€ν”„λ§ μ»¨ν…Œμ΄λ„ˆμ— μœ„μž„ν•˜μ—¬ μžλ™μœΌλ‘œ μ²˜λ¦¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

@Configuration
@EnableScheduling
public class SchedulerConfig {
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        
        scheduler.setPoolSize(10);
        scheduler.setThreadNamePrefix("my-scheduler-");
        scheduler.initialize();
        
        return scheduler;
    }
}

μœ„ μ½”λ“œμ—μ„œλŠ” ThreadPoolTaskSchedulerλ₯Ό μƒμ„±ν•˜κ³  μŠ€λ ˆλ“œ ν’€μ˜ 크기λ₯Ό 10으둜 μ„€μ •ν•˜λ©°, μŠ€λ ˆλ“œ μ΄λ¦„μ˜ 접두사λ₯Ό "my-scheduler-"둜 μ„€μ •ν•œ ν›„ μŠ€ν”„λ§ 빈으둜 λ“±λ‘ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. 이 섀정은 SchedulingConfigurer μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜λŠ” 방법과 λ™μΌν•œ κ²°κ³Όλ₯Ό μ œκ³΅ν•˜μ§€λ§Œ, μŠ€ν”„λ§ μ»¨ν…Œμ΄λ„ˆκ°€ μŠ€μΌ€μ€„λ§ 섀정을 μžλ™μœΌλ‘œ μ²˜λ¦¬ν•˜λ―€λ‘œ λ³„λ„μ˜ μ„€μ • 없이도 μ—¬λŸ¬ μŠ€μΌ€μ€„λ§ μž‘μ—…μ„ λ³‘λ ¬λ‘œ μ²˜λ¦¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

μ—¬κΈ°μ„œ "λ³„λ„μ˜ μ„€μ •"μ΄λž€, SchedulingConfigurerλ₯Ό κ΅¬ν˜„ν•˜λŠ” λ°©μ‹μ—μ„œ ν•„μš”ν•œ configureTasks λ©”μ„œλ“œλ₯Ό 직접 κ΅¬ν˜„ν•˜λŠ” λ“±μ˜ μž‘μ—…μ„ μ˜λ―Έν•©λ‹ˆλ‹€. TaskScheduler 빈 등둝 λ°©μ‹μ—μ„œλŠ” 이런 좔가적인 μž‘μ—… 없이도 μŠ€ν”„λ§μ΄ μŠ€μΌ€μ€„λŸ¬λ₯Ό μžλ™μœΌλ‘œ κ΄€λ¦¬ν•˜κ²Œ λ©λ‹ˆλ‹€.

μ‹€ν–‰ κ²°κ³Ό

μ‹€ν–‰ κ²°κ³Όλ₯Ό ν™•μΈν•˜λ©΄, SchedulingConfigurerλ₯Ό κ΅¬ν˜„ν–ˆμ„ λ•Œμ™€ λ™μΌν•œ κ²°κ³Όλ₯Ό λ‚˜νƒ€λ‚΄λŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

 

 

정리

μŠ€ν”„λ§μ˜ @Scheduled μ–΄λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•˜λ©΄ λ©”μ„œλ“œμ— μŠ€μΌ€μ€„λ§ κΈ°λŠ₯을 λΆ€μ—¬ν•  수 μžˆμ§€λ§Œ κΈ°λ³Έ μ„€μ •μœΌλ‘œλŠ” λͺ¨λ“  μž‘μ—…μ΄ ν•œ μŠ€λ ˆλ“œμ—μ„œ 순차적으둜 μ²˜λ¦¬λ©λ‹ˆλ‹€. 이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ Async 처리, SchedulingConfigurer κ΅¬ν˜„, TaskScheduler 빈 등둝 λ“±μ˜ 방법을 μ‚¬μš©ν•˜μ—¬ μŠ€μΌ€μ€„λ§ λ™μž‘μ„ μ„ΈλΆ€μ μœΌλ‘œ μ œμ–΄ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό 톡해 μ—¬λŸ¬ μž‘μ—…λ“€μ„ λ™μ‹œμ— μ²˜λ¦¬ν•˜κ±°λ‚˜, μŠ€μΌ€μ€„λ§ 섀정을 μžλ™μœΌλ‘œ μ²˜λ¦¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€.