Programming/Spring Boot

@Scheduled 동작 시 timezone 설정 관련하여 발생한 이슈 정리

Jan92 2023. 1. 6. 00:23

@EnableScheduling

spring boot 프로젝트에서 스케줄러 작업을 하던 중 발생한 이슈를 정리한 내용입니다.

이슈는 어플리케이션 기동 시 설정한 'TimeZone이 Scheduler 동작 시 적용되지 않는 것'에서 시작되었으며, 이슈에 대한 원인으로는 Scheduler 동작을 위한 클래스들이 Bean 등록되는 시점과 TimeZone을 설정하는 코드가 실행되는 시점의 차이 때문이었는데요.

아래 코드와 내용을 통해 자세하게 살펴보겠습니다.

(CommandLineRunner interface를 통한 timezone 설정과 @PostConstruct annotation을 통한 timezone 설정의 실행 시점 차이)

 

 

문제가 된 코드

@EnableScheduling
@SpringBootApplication
public class ProjectApplication {

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

  @Bean
  public CommandLineRunner init() {
    return args -> {
      TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
    };
  }
}

(메인 Application.class)

 

@Component
public class ProjectScheduler {

  @Scheduled(cron = "0 0 15 * * *")
  public void testScheduler() {
    ...
  }
}

(실행되는 스케줄러가 있는 클래스)

 

기존 프로젝트에서 위 코드와 같이 CommandLineRunner를 통해 서버 구동 시점에서 TimeZone을 UTC로 설정했는데요.

한국 시간(KST)으로 매일 자정에 스케줄러를 실행하고 싶었고, KST는 UTC +9 시간이기 때문에 cron = "0 0 15 * * *"으로 설정하면 정상적으로 작동할 것으로 예상했습니다.

 

하지만 실제로 스케줄러가 동작한 시간은 KST 기준 15:00:00 시점이었는데요. 스케줄러가 실행되는 시간은 "KST" 기준이었지만, 로그로 어플리케이션 서버에 설정된 TimeZone을 확인해 보면 "UTC"가 정상적으로 설정이 되어 있었습니다.

 

@Scheduled(cron = "0 0 15 * * *", zone = "UTC")

(다음과 같이 zone 옵션을 명시해주면 정상적으로 동작하지만, 원인을 파악하기 위해서 조금 더 찾아보았습니다.)

 

 

 

@PostConstruct 방식은 정상적으로 동작

@EnableScheduling
@SpringBootApplication
public class ProjectApplication {

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

  @PostConstruct
  public void setTimeZone() {
    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
  }
}

원인을 찾아보던 중 서버 기동 시점에서 CommandLineRunner 인터페이스를 사용하는 방식이 아닌, 위 예시와 같은 @PostConstruct 방식으로 TimeZone을 설정했을 때는 스케줄러가 정상적으로 "UTC" 기준으로 동작하는 것을 확인했는데요.

 

이러한 차이가 발생한 원인을 먼저 말씀드리면, 어플리케이션 서버가 기동 될 때 'Scheduler 동작을 위한 클래스들이 Bean으로 등록되는 시점의 차이'가 이유였습니다.

 

***

CommandLineRunner interface를 통한 TimeZone 설정 방식의 경우, 해당 설정 코드가 동작하기 전에 Scheduler 동작을 위한 class들이 Bean으로 등록되어 버립니다.

즉, 타임존이 UTC로 설정되기 전에 Default 설정(여기서는 KST)이 timezone으로 설정되어 빈이 등록되는 것입니다.

하지만 @PostConstruct 방식의 경우 TimeZone이 설정된 이후 스케줄러 동작을 위한 클래스들이 빈으로 등록되기 때문에 스케줄러에 설정된 timezone이 UTC로 설정되어 작동하게 되는 것입니다.

 

 

 

Bean 등록 순서 및 과정 살펴보기

위 내용을 조금 더 자세하게 알아보기 위해 스케줄러가 동작되기 위한 클래스들이 bean 등록되는 과정을 살펴보겠습니다.

 

taskScheduler()

먼저 TaskSchedulingAutoConfiguration class의 taskScheduler() 메서드입니다. 스프링 프레임워크는 TaskScheduler 인터페이스로 스케줄링에 대한 추상화를 지원하는데요.

해당 메서드가 동작하며 반환되는 객체는 스케줄러에 대한 어뎁터인 ThreadPoolTaskScheduler 객체가 반환되어 Bean으로 등록됩니다.

 

 

setTaskScheduler()

이어서 ScheduledAnnotationBeanPostProcessor class의 finishRegistration() 메서드 내부에서 동작하는 setTaskScheduler()입니다.

주석에서도 볼 수 있듯이 이 부분에서 빈으로 등록된 TaskScheduler 구현체를 찾는데, 이때 위에서 등록된 ThreadPoolTaskScheduler 객체를 찾아와 등록하게 되는 것입니다.

 

 

ThreadPoolTaskScheduler

setTaskScheduler() 메서드에 디버그 포인트를 찍고, 돌아가는 시점에서 살펴보면 이때 search 되는 TaskScheduler 객체에 대한 정보를 볼 수 있는데요.

이 부분에서 ThreadPoolTaskScheduler에 설정된 SystemClock 정보를 확인할 수 있습니다.

 

 

***

다시 결론을 이야기하자면 CommandLineRunner 방식은 TaskScheduler가 생성되고 registrer.setTaskScheduler() 메서드가 동작하고 난 뒤에 TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 코드가 동작하는 것이고,

@PostConstruct 방식은 timezone 설정이 동작하고 난 뒤에 TaskScheduler 생성 및 setTaskScheduler() 메서드가 동작하기 때문에 이러한 차이가 발생된 것이었습니다.

 

 

/*

확인하는 방법은 메인 Application.class에서 두 가지 방식에 대한 코드를 모두 추가하고, setDefault() 메서드 동작 부분에 디버그 포인트를 설정합니다. 그리고 TaskSchedulingAutoConfiguration class의 taskScheduler() 메서드에 디버그 포인트를 지정하고, ScheduledAnnotationBeanPostProcessor class의 finishRegistration() 메서드 내부에 있는 registrar.setTaskScheduler() 메서드에 디버그 포인트를 지정합니다.

 

bean이 생성되는 부분에 대한 확인은 AbstractAutowireCapableBeanFactory 추상 클래스의 createBean() 메서드에 디버그 포인트를 설정해서 확인할 수 있습니다.

*/