티스토리 뷰

JVM/Spring

[Spring] @Scheduled, @EnableScheduling

글을 쓰는 개발자 2022. 10. 1. 18:20
반응형

사용법

@EnableScheduling 설정

반드시 @Scheduled를 사용할려면 @EnableScheduling을 다음과 같이 설정해야 한다.

@SpringBootApplication
@EnableScheduling
public class SpringplaygroundApplication {

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

}

 

@Scheduled 사용

@Scheduled는 다음 아래와 같이 3가지 설정을 통해 구현할 수 있으며, 원하는 내용에 어노테이션을 설정하면 된다.

cron

cron은 흔히 우리가 linux에서 설정하는 cron 형식과 유사하며 사용하는 방법은 다음 아래와 같다.

@Service
@Slf4j
public class ScheduleService {
    @Scheduled(cron = "10 * * * * *") //10초로 끝날 때마다 호출
    public void cronCall() {
        log.info("cron execute");
    }
}
2022-10-01 18:36:10.005  INFO 7847 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : cron execute
2022-10-01 18:37:10.004  INFO 7847 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : cron execute
2022-10-01 18:38:10.005  INFO 7847 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : cron execute
2022-10-01 18:39:10.005  INFO 7847 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : cron execute

 

CronExpression 에 정의 되어 있는 cron 예시

 

"@yearly", "0 0 0 1 1 *",
"@annually", "0 0 0 1 1 *",
"@monthly", "0 0 0 1 * *",
"@weekly", "0 0 0 * * 0",
"@daily", "0 0 0 * * *",
"@midnight", "0 0 0 * * *",
"@hourly", "0 0 * * * *"
@Service
@Slf4j
public class ScheduleService {
    @Scheduled(cron = "@hourly") // 매 정각마다
    public void cronCall() {
        log.info("cron execute");
    }
}

 

실제 위의 표현식은 제대로 동작을 한다.

2022-10-01 18:54:35.361  INFO 8103 --- [           main] c.p.s.SpringplaygroundApplication        : Started SpringplaygroundApplication in 1.115 seconds (JVM running for 1.315)
2022-10-01 19:00:00.004  INFO 8103 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : cron execute

 

cron 순서는 다음과 같다

* <li>second</li> 초
* <li>minute</li> 분
* <li>hour</li> 시간
* <li>day of month</li> 일
* <li>month</li> 월
* <li>day of week</li> 주

 

fixedDelay

fixedDelay는 이전 작업이 끝나고 일정시간 마다 도는 것을 설정하는 필드다.

@Service
@Slf4j
public class ScheduleService {
    @Scheduled(fixedDelay = 1L, timeUnit = TimeUnit.SECONDS)
    public void publicCall() throws InterruptedException {
        Thread.sleep(500L);
        log.info("public call fixedDelay = 1's");
    }
}

 

2022-10-01 18:46:39.827  INFO 7915 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : public call fixedDelay = 1's
2022-10-01 18:46:41.334  INFO 7915 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : public call fixedDelay = 1's
2022-10-01 18:46:42.840  INFO 7915 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : public call fixedDelay = 1's
2022-10-01 18:46:44.349  INFO 7915 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : public call fixedDelay = 1's
2022-10-01 18:46:45.859  INFO 7915 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : public call fixedDelay = 1's
2022-10-01 18:46:47.366  INFO 7915 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : public call fixedDelay = 1's

 

* Execute the annotated method with a fixed period between the end of the
* last invocation and the start of the nex

즉 마지막 동작이 끝나고 정해진 시간 뒤에 일어난다는 뜻이다.

 

예시에서 스레드를 0.5초 쉬게 한 이유도 정확히 이전 작업이 끝나고 1초 뒤에 작업을 한다는 것을 보여주기 위해 작성했다.

 

참고로 timeUnit은 기본은 MiliSeconds 이다.

 

그러면 public Method 에서만 동작할까?

 

그렇지 않다!

 

@Service
@Slf4j
public class ScheduleService {
    @Scheduled(fixedDelay = 1500L)
    private void privateCall() {
        log.info("private call fixedDelay = 1.5's");
    }
}

 

2022-10-01 18:49:20.443  INFO 8038 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : private call fixedDelay = 1.5's
2022-10-01 18:49:21.949  INFO 8038 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : private call fixedDelay = 1.5's
2022-10-01 18:49:23.451  INFO 8038 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : private call fixedDelay = 1.5's
2022-10-01 18:49:24.957  INFO 8038 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : private call fixedDelay = 1.5's
2022-10-01 18:49:26.463  INFO 8038 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : private call fixedDelay = 1.5's

다음과 같이 private method 에서도 동작하는 것을 볼 수가 있다.

 

fixedRate

fixedRate의 경우에는 시작을 기준으로 일정 간격마다 동작하는 방식이다.

@Service
@Slf4j
public class ScheduleService {
    @Scheduled(fixedRate = 1000L)
    public void fixedRateCall() throws InterruptedException {
        Thread.sleep(500L);
        log.info("public call fixedRate = 1's");
    }
}
2022-10-01 18:51:24.293  INFO 8080 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : public call fixedRate = 1's
2022-10-01 18:51:25.296  INFO 8080 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : public call fixedRate = 1's
2022-10-01 18:51:26.298  INFO 8080 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : public call fixedRate = 1's
2022-10-01 18:51:27.293  INFO 8080 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : public call fixedRate = 1's
2022-10-01 18:51:28.298  INFO 8080 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : public call fixedRate = 1's
2022-10-01 18:51:29.294  INFO 8080 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : public call fixedRate = 1's
2022-10-01 18:51:30.294  INFO 8080 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : public call fixedRate = 1's
2022-10-01 18:51:31.297  INFO 8080 --- [   scheduling-1] c.p.s.schedule.ScheduleService           : public call fixedRate = 1's

 

fixedRate는 fixedDelay랑 다르게 일정 주기성을 가지고 정확히 1초마다 돌아가는 것을 볼 수가 있다.

* Execute the annotated method with a fixed period between invocations.
* <p>The time unit is milliseconds by default but can be overridden via
* {@link #timeUnit}.

주석을 읽으면 아시다시피 정해진 주기에 맞게 동작을 한다고 적혀 있다.

 

 

동작원리

 

@EnableScheduling 으로 필요한 Bean들을 등록한다!

 

SchedulingConfiguration

@Configuration(
    proxyBeanMethods = false
)
@Role(2)
public class SchedulingConfiguration {
    public SchedulingConfiguration() {
    }

    @Bean(
        name = {"org.springframework.context.annotation.internalScheduledAnnotationProcessor"}
    )
    @Role(2)
    public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
        return new ScheduledAnnotationBeanPostProcessor();
    }
}

 

여기서 ScheduledAnnotationBeanPostProcessor를 빈으로 등록하는 과정을 거친다.

 

ScheduledAnnotationBeanPostProcessor

    public ScheduledAnnotationBeanPostProcessor() {
        this.registrar = new ScheduledTaskRegistrar();
    }

생성자에서 ScheduledTaskRegistrar()를 새로 생성하는 과정을 거치며

 

ScheduledTaskRegistrar

    public void setTaskScheduler(TaskScheduler taskScheduler) {
        Assert.notNull(taskScheduler, "TaskScheduler must not be null");
        this.taskScheduler = taskScheduler;
    }

taskScheduler를 주입받으며 생성하게 되는데 taskScheduler는

 

spring-autoconfigure-metadata.properties

org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration.ConditionalOnClass=org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler

에 정의되어 있는 ThreadPoolTaskScheduler를 빈으로 생성하고 이를 주입 받는 형태로 진행된다.

 

그럼 @Scheduled을 붙인 메소드는 어떻게 동작하게 될까? 

ScheduledAnnotationBeanPostProcessor 의 이름에서 느껴지겠지만 빈 후처리기를 통해 생성된 빈에 스케줄 관련 후처리를 진행한다.

 

    public Object postProcessAfterInitialization(Object bean, String beanName) {
        if (!(bean instanceof AopInfrastructureBean) && !(bean instanceof TaskScheduler) && !(bean instanceof ScheduledExecutorService)) {
            Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
            if (!this.nonAnnotatedClasses.contains(targetClass) && AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {
                Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass, (method) -> {
                    Set<Scheduled> scheduledAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(method, Scheduled.class, Schedules.class);
                    return !scheduledAnnotations.isEmpty() ? scheduledAnnotations : null;
                });
                if (annotatedMethods.isEmpty()) {
                    this.nonAnnotatedClasses.add(targetClass);
                    if (this.logger.isTraceEnabled()) {
                        this.logger.trace("No @Scheduled annotations found on bean class: " + targetClass);
                    }
                } else {
                    annotatedMethods.forEach((method, scheduledAnnotations) -> {
                        scheduledAnnotations.forEach((scheduled) -> {
                            this.processScheduled(scheduled, method, bean);
                        });
                    });
                    if (this.logger.isTraceEnabled()) {
                        this.logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName + "': " + annotatedMethods);
                    }
                }
            }

            return bean;
        } else {
            return bean;
        }
    }

을 보시면 @Scheduled 검사 여부에 따라 적합한 후보군이 있으면 이에 따라 

protected void processScheduled(Scheduled scheduled, Method method, Object bean)

위의 메소드를 호출하고 bean을 반환하는 작업을 진행하게 된다. 그렇게 되면 우리가 원하는 Schedule 설정이 동작하게 된다.

 

그리고 해당 메소드를 호출함으로써 실질적인 동작을 하게 된다.

@Override
public void afterPropertiesSet() {
   scheduleTasks();
}

그럼 scheduleTasks() 는 어디서 돌아갈까? 바로 아래글을 보자

 

그러면 실질적인 동작하는 클래스는 어디?

Task를 관리하는 클래스는 ScheduledTaskRegistrar 이다.

 

관리하는 Task 는 다음과 같다.

  • CronTask
  • FixedDelayTask
  • FixedRateTask
  • etc

등 다양한 Task 들이 존재하고 이에 대한 처리를  ScheduledTaskRegistrar 에서 하게 된다.

 

아까 위에서 scheduleTasks()에서 실질적인 동작들이 돌아간다고 말을 했는데 그 중 작은 부분을 보도록 하자

 

Date startTime = new Date(this.taskScheduler.getClock().millis() + task.getInitialDelay());
scheduledTask.future = this.taskScheduler.scheduleWithFixedDelay(task.getRunnable(), startTime, task.getInterval());

위 코드는 FixedDelay 를 설정했을 때 돌아가는 코드로서 @Scheduled 에 설정한 값들을 저장하고 그 값을 활용하여 ScheduleFuture에 등록함으로써 일정주기마다 동작하도록 설정하는 코드다.

 

그 이외에도 FixedRate, Cron도 이와 유사하게 동작하므로 실제로 보는 것을 추천한다.

 

 

어떤 상황에서 써야 할까?

필자가 생각했을 때는 스프링 배치로 돌기에는 너무 거창하고 그렇다고 주기적으로 돌긴 해야하는 상황에서는 사용하면 좋을 것 같다.

 

가령 일정 시간마다 데이터 최신화가 필요하지만 실패해도 상관없는 경우에 사용하거나, 일정시간마다 캐시를 Invalidation하는 작업을 하거나 등의 상황에서 사용하면 유용하고, 쉽게 사용할 수 있지 않을까 싶다.

 

반응형

'JVM > Spring' 카테고리의 다른 글

[Spring] @EventListener  (0) 2022.10.06
[Spring] @Async 비동기로 처리하는 방법  (0) 2022.10.01
[Spring] Scope (Prototype & Singleton)  (2) 2022.09.13
[Hikari CP] 光 살펴보기 - 4  (0) 2022.04.29
[Hikari CP] 光 살펴보기 - 3  (0) 2022.04.28
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함