본문 바로가기

Java/Spring

@Scheduled 사용법, 스케줄러 커스터마이징을 통한 제어(+스케줄러에 등록한 작업 중지하는 방법, 배치 효과, 정확한 주기 작업 사용법)

@Scheduled 사용법

주기적인 작업이 있을 때 @Scheduled 애노테이션을 사용하면 쉽게 적용할 수 있다. ex) linux의 crontab

1. @EnableScheduling Annotation을 적어서 스케줄링을 사용한다는 것을 알린다.

@EnableScheduling
@SpringBootApplication
public class SchedulerApplication {
	public static void main(String[] args) {
		SpringApplication.run(SchedulerApplication.class, args);
	}
}

2. 하위 패키지의 클래스에서 주기적으로 수행해야할 메서드 위에 @Scheduled Annotation을 붙인다.

@Scheduled(fixedRateString = "5", initialDelay = 3000)
private void scheduleTest() {
	logger.error("hello jeong-pro");
}

끝!

위와 같이 사용하게되면 3초의 대기시간(initialDelay) 후에 5ms(fixedRate)마다 "hello jeong-pro"라는 로그를 찍는 작업을 스케줄러가 수행해준다.

사용을 위해 해야할 작업은 알아봤고, 이제 @Scheduled 활용을 위한 속성(attribute)를 확인해본다.

속성

  • cron : cron표현식을 지원한다. "초 분 시 일 월 주 (년)"으로 표현한다. cron표현식에 쓸 수 있는 것들(특수문자 활용 포함)이 많은데 해당 내용이 핵심이 아니므로 다른 블로그에서 확인해보기를 바란다.
  • fixedDelay : milliseconds 단위로, 이전 작업이 끝난 시점으로 부터 고정된 시간을 설정한다. ex) fixedDelay = 5000
  • fixedDelayString : fixedDelay와 같은데 property의 value만 문자열로 넣는 것이다. ex) fixedDelay = "5000"
  • fixedRate : milliseconds 단위로, 이전 작업이 수행되기 시작한 시점으로 부터 고정된 시간을 설정한다. ex) fixedRate = 3000
  • fixedRateString : fixedDelay와 같은데 property의 value만 문자열로 넣는 것이다. ex) fixedRate = "3000"
  • initialDelay : 스케줄러에서 메서드가 등록되자마자 수행하는 것이 아닌 초기 지연시간을 설정하는 것이다.
  • initialDelayString : 위와 마찬가지로 문자열로 값을 표현하겠다는 의미다.
  • zone : cron표현식을 사용했을 때 사용할 time zone으로 따로 설정하지 않으면 기본적으로 서버의 time zone이다.

* fixedDelay vs fixedRate

간단하게 그림을 그려봤는데 결국 Rate는 작업 수행시간과 상관없이 일정 주기마다 메서드 호출을 시켜주는 것이고,

Delay는 (작업 수행 시간을 포함하여) 작업을 마친 후부터 주기 타이머가 돌아 메서드를 호출해주는 것이다.


springboot 2.0에서는 auto-configration이 있기 때문에 스프링부트가 알아서 스케줄러를 생성해준 것인데, 이 스케줄러의 경우 쓰레드풀이 아니기 때문에 많은 작업이 있을 때 효율적이지 못하다.

그래서 커스터마이징을 해주는 것이 좋다.

단순하게는 yml에서 아래와 같이 설정을 할 수도 있지만 configuration을 만들어서 사용해보겠다.

spring:
  task:
    scheduling:
      pool:
        size: 8
      thread-name-prefix: my-scheduler

아! 스레드풀로 사용되지 않는 것을 확인하려면 짧은 주기로 로그를 찍어보면 된다.

이름이 scheduling-1 이라는 쓰레드만 계속 돌아서 수행시켜줄 것이다.

@Configuration
public class SchedulerConfiguration {
	@Bean
	public TaskScheduler poolScheduler() {
		ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
		threadPoolTaskScheduler.setPoolSize(Runtime.getRuntime().availableProcessors() * 2);
		threadPoolTaskScheduler.setThreadNamePrefix("jeong-pro-threadpool");
		return threadPoolTaskScheduler;
	}
}

이런식으로 스케줄러를 빈으로 등록하기만 하면 알아서 위에서 생성한 스케줄러에 작업을 할당해준다.

실제로 ThreadNamePrefix가 잘 작동되는지 작업을 많이 줘서 살펴보면 된다.

앞에 부분이 짤렸는데 pro-threadpool하면서 숫자가 다른 것들이 보인다. 여러 쓰레드가 작업을 수행한 것이다.

이정도는 가볍게 할 수 있을 것이다.

비율은 고정인가?

이제는 비율을 과연 고정으로 써야하는가?에 대해 풀어갈 것이다. 지금은 @Scheduled를 쓸 때 항상 2000, "2000" 이런식으로 코드내에 고정적으로 들어갔다.

이 문제를 동적으로 해결하는 방법을 알아본다.

@Scheduled(fixedRateString = "${myscheduler.period}", initialDelay = 2000)
private void scheduleTest() {
	logger.error("hello jeong-pro");
}

위와 같이 .yml이나 .properties 파일에 있는 값을 가져와서 적용할 수도 있다.

코드와 설정파일로 분리를 해낸 것이다.

아예 운영중에 동적으로 변환하려면 기존의 스케줄러에 등록한 작업을 빼고 새로 등록할 작업을 등록하는데 @Scheduled를 굳이 쓸필요없이 위의 configuration에서 생성한 스케줄러 빈을 주입받아 그 곳에 직접 작업을 등록하는 방법을 쓸 수도 있다. (아래에서 해볼 예정)

커스터마이징

public class CustomThreadPoolTaskScheduler extends ThreadPoolTaskScheduler {
	private static final long serialVersionUID = 1L;

	@Override
	public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period) {
		if (period <= 0) {
			return null;
		}
		ScheduledFuture<?> future = super.scheduleAtFixedRate(task, period);
		return future;
	}

	@Override
	public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Date startTime, long period) {
		if (period <= 0) {
			return null;
		}
		ScheduledFuture<?> future = super.scheduleAtFixedRate(task, startTime, period);
		return future;
	}
}

우리는 자바를 사용하기 때문에 상속을 통해 메서드를 Override할 수 있다.

기존에 스케줄러에 주기가 0인 작업을 던져주면 예외를 발생시키게 코딩되어있다. ThreadPoolTaskScheduler 내부 코드를 확인해보면 된다.

근데 개인적인 요구사항으로 예를들어서 0으로 지정하면 작업을 수행 안하게 했다고 한다면, 위와 같이 0이하로 왔을 때 그냥 return null;로 끝내버리도록 단순하게 바꿀 수 있다는 것이다.

동적 주기 사용(빈 주입), 중간에 작업 중지시키기

@Service
public class SchedulerService {
	private static final Logger logger = LoggerFactory.getLogger(SchedulerService.class);
	private Map<String, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
	
	@Autowired
	private TaskScheduler taskScheduler;
	
	public void register() {
		ScheduledFuture<?> task = taskScheduler.scheduleAtFixedRate(()->logger.info("hello jeong-pro"), 1000);
		scheduledTasks.put("mySchedulerId", task);
	}
	
	public void remove() {
		scheduledTasks.get("mySchedulerId").cancel(true);
	}
	
	@Scheduled(fixedRateString = "${myscheduler.period}", initialDelay = 2000)
	private void scheduleTest() {
		logger.error("fix");
	}
}

서비스 코드를 만들었다.

@Autowired로 등록되어있는 스케줄러를 불러왔고 register() 메서드를 보면 알 수 있듯, 작업을 생성해서 scheduleAtFixedRate()로 작업을 등록해주면 똑같이 사용하는 것이다.

이것으로 동적으로 주기를 설정할 수 있고, 리턴 값으로 받는 ScheduledFuture<?>를 관리하도록 한다면(위에서는 Map으로 관리) 가지고 있다가 원하는 타이밍에 해당 작업을 스케줄러에서 취소할 수도 있다.

remove메서드를 확인해보면 ScheduledFuture를 받아와서 cancel(true)로 작업을 중지한다.

끝.