@scheduled 사용 시 주의점과 활용법
@Scheduled
어노테이션은 기본적으로 단일 스레드로 동작한다. 이 점을 간과하면 여러 스케줄러를 사용할 때 각 스케줄러가 병렬로 처리되지 않고 대기 상태에 빠질 수 있다.
실제 나는 이 부분에 대해서 제대로 체크를 하지못해 스케줄러가 비효율적으로 동작하는 경험을 하게 되었다.
이 게시글에서는 @Scheduled 어노테이션에 대한 기본 동작과 주의해야할 점 그리고 활용법에 대한 내용을 정리하고자 한다.
[01] @Scheduled 사용 시 발생할 수 있는 문제
문제 상황
처음에는 당연히 각 스케줄러가 독립적으로 실행될 것이라 생각하고, 간단한 for문을 활용한 테스트를 진행하였고 문제가 없다고 판단하였다.
하지만 데이터 양이 많아지자, 첫 번째 스케줄러의 수행 시간이 길어지면서 나머지 스케줄러는 대기 상태에 빠지는 문제가 발생하였다.
[02] 문제의 원인
@Scheduled 어노테이션이란?
@Scheduled 어노테이션은 스프링 프레임워크(Spring Framework)에서 제공하는 기능 중 하나로, 메서드를 특정 시간이나 주기에 따라 실행되도록 스케줄링하는 데 사용된다. 주로 스프링의 스케줄링 태스크를 실행하여 백그라운드에서 주기적으로 작업을 수행할 수 있게 한다.
단일 스레드 동작
스프링 프레임워크의 공식 문서에 따르면, @Scheduled 어노테이션을 사용할 때 명시적으로 설정하지 않으면 기본적으로 단일 스레드로 동작하는 TaskScheduler를 사용하게 된다.
스프링 컨텍스트 내에 별도로 TaskScheduler 빈을 정의하지 않았을 때, 로컬 단일 스레드 기본 스케줄러가 생성된다.
- If you do not provide a ‘pool-size’ attribute, the default thread pool will only have a single thread.
‘poo-size’ 속성을 명시하지 않으면 기본 스레드 풀에는 단일 스레드만 포함됩니다.
[03] 테스트 : 단일 스레드 동작 확인
@Scheduled의 단일 스레드 동작 확인해보기
// testA 메서드: 1초 간격으로 10초 쉬고 로그 출력
@Scheduled(fixedDelay = 1000)
public void testA() {
try {
Thread.sleep(10000);
logger.info("testA Scheduler Start!!");
}catch (Exception e){
logger.error("testA Scheduler error :: {}", e);
}
}
// testB 메서드: 1초 간격으로 1초 쉬고 로그 출력
@Scheduled(fixedDelay = 1000)
public void testB() {
try {
Thread.sleep(1000);
logger.info("testB Scheduler Start!!");
}catch (Exception e){
logger.error("testB Scheduler error :: {}", e);
}
}
예상과 다르게 testA 스케줄러가 동작하는 동안 1초 간격으로 실행되어야 하는 testB 메서드는 대기하고 있었다.
[04] @Scheduled의 멀티 스레드 동작을 위해 TaskScheduler 설정하기
@Configuration
@EnableScheduling
public class SchedulerConfig {
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(10); // 스레드 풀 크기 설정
taskScheduler.setThreadNamePrefix("scheduled-task-");
return taskScheduler;
}
}
위와 같이 ThreadPoolTaskScheduler 스레드 개수를 명시하였다.
스레드 풀 크기 설정 기준
-
CPU 코어 수: CPU 코어 수에 맞추어 스레드 풀 크기를 설정하는 것이 좋다고 한다. 예를 들어, CPU 코어 수가 8개인 경우, CPU 바운드 작업에 대해 8개 정도의 스레드가 적절하다.
-
어떤 작업을 진행하는지도 중요하다.
- I/O 바운드 작업: 작업이 주로 파일 읽기/쓰기, 네트워크 통신 등 I/O 작업을 많이 수행하는 경우, 스레드가 실제로 작업을 수행하는 시간보다 대기 시간이 많기 때문에 이 경우 스레드 풀 크기를 크게 설정해도 괜찮다.
- CPU 바운드 작업: 작업이 주로 계산 작업을 많이 수행하는 경우, 스레드가 CPU를 많이 사용된다. 이 경우 너무 많은 스레드를 사용하면 오히려 성능이 저하될 수 있기 때문에 일반적으로 CPU 코어 수를 기준으로 스레드 풀 크기를 설정하는 것이 좋다.
@Scheduled 멀티스레드 동작 확인해보기
- testA 스케줄러가 동작하는 동안 대기하는 것이 아니라, 1초 간격으로 실행되어야 하는 testB 메서드도 실행됩니다.
[05] 하나의 스케줄러를 비동기로 연속 실행하기
예를 들어, 아래와 같은 비동기 방식으로 변경을 원하고자 한다.
- 기존: 시작 -> 스케줄러 완료 -> 1초 대기 -> 재실행
- 비동기: 시작 -> 1초 대기 -> 재실행
@Autowired
private ThreadPoolTaskScheduler taskScheduler;
@Scheduled(fixedDelay = 1000)
public void testA() {
taskScheduler.execute(() -> {
try {
Thread.sleep(10000);
logger.info("testA Scheduler Start!!");
} catch (Exception e) {
logger.error("testA Scheduler error :: {}", e);
}
});
}
@Scheduled(fixedDelay = 1000)
public void testB() {
try {
Thread.sleep(1000);
logger.info("testB Scheduler Start!!");
} catch (Exception e) {
logger.error("testB Scheduler error :: {}", e);
}
}
testA 메서드만 taskScheduler.execute 메서드를 사용하여 실행해보았다.
11초 간격으로 실행되어야 하는 testA 메서드는 비동기로 실행됩니다.
첫 번째와 두 번째 메서드의 동작 차이
- 작업 실행 방식: 첫 번째 메서드는 taskScheduler를 사용하여 작업을 비동기적으로 새로운 스레드에서 실행한다.
- 비동기 실행: taskScheduler.execute 메서드를 사용하여 작업을 비동기적으로 실행하므로, @Scheduled 메서드는 곧바로 반환된다.
- 첫 번째 메서드: taskScheduler.execute를 사용하여 작업을 비동기적으로 실행함으로써, 스케줄러의 기본 스레드 외의 다른 스레드에서 작업을 수행한다. 이는 작업이 오래 걸리거나 블로킹될 경우에도 다른 스케줄된 작업들이 영향을 받지 않도록 한다.
- 두 번째 메서드: @Scheduled 어노테이션으로 지정된 스케줄러의 기본 스레드에서 작업을 직접 실행한다.
[06]요약
@Scheduled 어노테이션을 무턱대고 사용하면 예상치 못한 동작으로 인해 낭패를 볼 수 있다. 작업의 특성에 따라 적절한 스레드 풀 크기와 비동기 실행 방식을 선택하는 것이 중요합하며 이를 통해 스케줄러를 효율적으로 관리하고, 시스템 성능을 최적화할 수 있다.