3 minute read


@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);
  }
}

spring-4 예상과 다르게 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개 정도의 스레드가 적절하다.

  • 어떤 작업을 진행하는지도 중요하다.

    1. I/O 바운드 작업: 작업이 주로 파일 읽기/쓰기, 네트워크 통신 등 I/O 작업을 많이 수행하는 경우, 스레드가 실제로 작업을 수행하는 시간보다 대기 시간이 많기 때문에 이 경우 스레드 풀 크기를 크게 설정해도 괜찮다.
    2. CPU 바운드 작업: 작업이 주로 계산 작업을 많이 수행하는 경우, 스레드가 CPU를 많이 사용된다. 이 경우 너무 많은 스레드를 사용하면 오히려 성능이 저하될 수 있기 때문에 일반적으로 CPU 코어 수를 기준으로 스레드 풀 크기를 설정하는 것이 좋다.

@Scheduled 멀티스레드 동작 확인해보기

spring-5

  • 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 메서드를 사용하여 실행해보았다.

spring-6 11초 간격으로 실행되어야 하는 testA 메서드는 비동기로 실행됩니다.

첫 번째와 두 번째 메서드의 동작 차이

  1. 작업 실행 방식: 첫 번째 메서드는 taskScheduler를 사용하여 작업을 비동기적으로 새로운 스레드에서 실행한다.
  2. 비동기 실행: taskScheduler.execute 메서드를 사용하여 작업을 비동기적으로 실행하므로, @Scheduled 메서드는 곧바로 반환된다.
  3. 첫 번째 메서드: taskScheduler.execute를 사용하여 작업을 비동기적으로 실행함으로써, 스케줄러의 기본 스레드 외의 다른 스레드에서 작업을 수행한다. 이는 작업이 오래 걸리거나 블로킹될 경우에도 다른 스케줄된 작업들이 영향을 받지 않도록 한다.
  4. 두 번째 메서드: @Scheduled 어노테이션으로 지정된 스케줄러의 기본 스레드에서 작업을 직접 실행한다.

[06]요약

@Scheduled 어노테이션을 무턱대고 사용하면 예상치 못한 동작으로 인해 낭패를 볼 수 있다. 작업의 특성에 따라 적절한 스레드 풀 크기와 비동기 실행 방식을 선택하는 것이 중요합하며 이를 통해 스케줄러를 효율적으로 관리하고, 시스템 성능을 최적화할 수 있다.

Tags:

Categories:

Updated: