반응형

스프링 TaskExecutor추상화

  • Executors는 스레드 풀 개념에 대한 JDK 명칭이다.
  • Executor 라고 이름을 정한 이유는 기본 구현이 실제로 스레드 풀이라는 보장이 없기 때문에 발생한다.
  • Executor 는 단일 스레드일 수도 있고 동기식일 수도 있다. 스프링의 추상화는 구현 세부 사항을 숨긴다.
  • Spring의 TaskExecutor 인터페이스는 java.util.concurrent.Executor 인터페이스와 동일하다.
    • 이 인터페이스에는 스레드 풀의 의미 및 구성을 기반으로 실행할 작업을 허용하는 단일 메서드(execute(Runnable task))를 가지고 있다.

 

  • TaskExecutor는 원래 필요한 경우 다른 Spring 구성 요소에 스레드 풀링에 대한 추상화를 제공하기 위해 만들어졌다.
    • ApplicationEventMulticaster, JMS의 AbstractMessageListenerContainerQuartz 통합과 같은 구성 요소는 모두 TaskExecutor 추상화를 사용하여 스레드를 풀링한다.
  • 그러나 Bean에 스레드 풀링 동작이 필요한 경우 필요에 따라 이 추상화를 사용하여 비동기 처리를 할 수도 있다.

 

스프링이 제공하는 TaskExecutor 구현체 유형

  • SyncTaskExecutor
    • 이 구현은 호출을 비동기적으로 실행하지 않는다. 대신 각 호출은 호출 스레드에서 발생하고 간단한 테스트 케이스처럼 멀티스레딩이 필요하지 않은 상황에서 주로 사용된다.
  • SimpleAsyncTaskExecutor
    • 이 구현은 스레드를 재사용하지 않는다. 오히려 호출할 때마다 새 스레드를 시작한다. 그러나 슬롯이 확보될 때까지 제한을 초과하는 모든 호출을 차단하는 동시성 제한을 지원한다.
    • 스프링은 기본적으로 @Async 애노테이션에 Executor가 명시적으로 지정되지 않으면 비동기 메서드를 실행하기 위해서 SimpleAsyncTaskExecutor 방식으로 동작한다.
  • ConcurrentTaskExecutor
    • 이 구현은 java.util.concurrent.Executor 인스턴스에 대한 어댑터이다.
    • Executor 구성 매개변수를 Bean으로 정의하여 구성하는 대안은 ThreadPoolTaskExecutor이 있다. 
    • ConcurrentTaskExecutor를 직접 사용할 필요는 거의 없지만 ThreadPoolTaskExecutor이 요구 사항에 비해 충분히 유연하지 않은 경우에는 ConcurrentTaskExecutor가 대안이 될 수 있다.
  • ThreadPoolTaskExecutor
    • 이 구현은 가장 일반적으로 사용되는 방식이다. java.util.concurrent.ThreadPoolExecutor를 구성하기 위한 Bean 속성을 노출하고 이를 TaskExecutor  로 감싼다.
    • 다른 종류의 java.util.concurrent.Executor에 적응해야 하는 경우 대신 ConcurrentTaskExecutor를 사용하는 것이 좋다.
  • DefaultManagedTaskExecutor
    • 이 구현은 JSR-236 호환 런타임 환경(예: Jakarta EE 애플리케이션 서버)에서 JNDI에서 얻은 ManagedExecutorService를 사용하여 해당 목적으로 CommonJ WorkManager를 대체한다.

 

스프링 프레임워크 6.1부터는 ThreadPoolTaskExecutor는 Spring의 라이프사이클 관리를 통해 일시정지/재개 기능과 우아한 종료 기능을 제공한다. 
또한
SimpleAsyncTaskExecutor에는 JDK 21의 가상 스레드와 일치하는 새로운 "virtualThreads" 옵션이 있으며 SimpleAsyncTaskExecutor 에 대한 정상적인 종료 기능도 가지고 있다.

 

스프링 TaskScheduler 추상화

  • 스프링은 TaskExecutor 추상화 외에도 Spring에는 미래의 특정 시점에 실행될 작업을 예약하기 위한 다양한 방법을 갖춘 TaskScheduler SPI가 있다.
  • TaskScheduler 인터페이스 정의
    public interface TaskScheduler {
    
    	Clock getClock();
    
    	ScheduledFuture schedule(Runnable task, Trigger trigger);
    
    	ScheduledFuture schedule(Runnable task, Instant startTime);
    
    	ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);
    
    	ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);
    
    	ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);
    
    	ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);

 

Trigger 인터페이스

  • Trigger 인터페이스는 JSR-236 스펙과 관련있다.
  • Trigger의 기본 아이디어는 실행 시간이 과거 실행 결과 또는 임의의 조건에 따라 결정될 수 있다는 것이다.
  • 이러한 결정이 이전 실행의 결과를 고려하는 경우 해당 정보는 TriggerContext 내에서 사용할 수 있다.
  • Trigger 인터페이스 정의
    public interface Trigger {
    
    	Instant nextExecution(TriggerContext triggerContext);
    }
    • TriggerContext가 가장 중요하고 이는 모든 관련 데이터를 캡슐화하며 필요한 경우 향후 확장이 가능하다. (TriggerContext는 인터페이스는 기본적으로 SimpleTriggerContext 구현이 사용된다.)
  • TriggerContext 인터페이스 정의
    public interface TriggerContext {
    
    	Clock getClock();
    
    	Instant lastScheduledExecution();
    
    	Instant lastActualExecution();
    
    	Instant lastCompletion();
    }

 

 

Trigger 구현

  • 스프링은 Trigger 인터페이스의 두 가지 구현을 제공한다.
    • CronTrigger : cron 표현식을 기반으로 작업 일정을 계획할 수 있다.
    • PeriodicTrigger : 고정 기간, 선택적 초기 지연 값 및 기간을 고정 비율 또는 고정 지연으로 해석해야 하는지 여부 등으로 작업 일정을 계획한다.
      • TaskScheduler 인터페이스는 이미 고정 속도 또는 고정 지연으로 작업을 예약하는 방법을 정의하므로 가능하면 이러한 방법을 직접 사용해야 한다.
      • PeriodicTrigger 구현의 가치는 Trigger 추상화에 의존하는 구성 요소 내에서 사용할 수 있다는 것이다. 예를 들어, PeriodicTrigger, CronTrigger , 심지어 사용자 정의 트리거 구현까지 서로 바꿔서 사용할 수 있도록 허용하는 것이 편리할 수 있다. 이러한 구성 요소는 종속성 주입을 활용하여 해당 트리거를 외부에서 구성할 수 있으므로 쉽게 수정하거나 확장할 수 있다.

 

TaskScheduler 구현

  • 스프링의 TaskExecutor 추상화와 마찬가지로 TaskScheduler 배열의 주요 이점은 애플리케이션의 스케줄링 요구가 배포 환경에서 분리된다는 것이다.
    • 이 추상화 수준은 애플리케이션 자체에서 스레드를 직접 생성해서는 안 되는 애플리케이션 서버 환경에 배포할 때 특히 관련이 있다.
    • 이러한 시나리오의 경우 Spring은 Jakarta EE 환경의 JSR-236 ManagedScheduledExecutorService에 위임하는 DefaultManagedTaskScheduler를 제공한다.

 

  • 외부 스레드 관리가 필요하지 않을 때마다 더 간단한 대안은 애플리케이션 내의 로컬 ScheduledExecutorService 설정이며 이는 스프링의 ConcurrentTaskScheduler를 통해 조정할 수 있다.
  • 편의상 스프링은 ThreadPoolTaskExecutor 라인을 따라 공통 빈 스타일 구성을 제공하기 위해 ScheduledExecutorService에 내부적으로 위임하는 ThreadPoolTaskScheduler도 제공한다. 이러한 점은 애플리케이션 서버 환경, 특히 Tomcat 및 Jetty의 로컬 임베디드 스레드 풀 설정에서 완벽하게 작동한다.

 

- 스프링 프레임워크 6.1부터는 ThreadPoolTaskScheduler는 스프링의 라이프사이클 관리를 통해 일시 중지/재개 기능과 우아한 종료 기능을 제공한다. 
또한 단일 스케줄러 스레드를 사용하지만 모든 예약된 작업 실행에 대해 새 스레드를 실행하는 JDK 21의 가상 스레드와 일치하는
SimpleAsyncTaskScheduler라는 새로운 옵션도 있다.
(모두 단일 스케줄러 스레드에서 작동하는 고정 지연 작업을 제외하고 이 가상 스레드 정렬 옵션의 경우 fixed rates 및 cron 트리거가 권장된다.)

 

 

스케줄링 및 비동기 실행을 위한 애노테이션 지원

  • 스프링은 작업 스케줄링과 비동기 메소드 실행 모두에 대한 애노테이션을 제공한다.
  • @Scheduled@Async 주석에 대한 지원을 활성화하려면 아래와 같이 @Configuration 클래스 중 하나에 @EnableScheduling@EnableAsync를 추가할 수 있다.
    @Configuration
    @EnableAsync //비동기 애노테이션 활성화 @Async
    @EnableScheduling //스케줄링 애노테이션 활성화 @Scheduled
    public class AppConfig {
    	...
    }

 

  • @Async 주석 처리를 위한 기본 AdviceMode는 “proxy” 이다.
    • 프록시를 통해서만 호출을 가로채는 것을 허용
같은 클래스 내의 로컬 호출은 차단될 수 없다. 보다 진보된 가로채기 모드를 위해서는 컴파일 타임 또는 로드 타임 위빙과 결합하여 Aspectj 모드로 전환하는 것을 고려해보자.

 

 

@Scheduled 애노테이션

  • 예약할 메서드에는 void 반환이 있어야 하며 인수를 허용해서는 안된다.
  • 동일한 메서드에서 여러 개의 예약된 선언이 발견되면 각 선언은 독립적으로 처리되며 각 선언에 대해 별도의 트리거가 실행된다.
  • 결과적으로, 그러한 공동 배치 일정은 병렬로 또는 즉시 연속적으로 여러 번 중복되고 실행될 수 있어서 지정한 크론 표현식 등이 실수로 겹치지 않는지 확인해야한다.

 

 

 

//고정된 지연 시간을 두고 5초(5000밀리초)마다 호출. 즉, 기간은 각 이전 호출의 완료 시간부터 측정된다.
@Scheduled(fixedDelay = 5000) 
public void doSomething() {
	// 주기적으로 실행되어야 하는 작업
}

//고정된 지연 시간을 두고 5초(5000밀리초)마다 호출. 즉, 기간은 각 이전 호출의 완료 시간부터 측정된다.
@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
	// 주기적으로 실행되어야 하는 작업
}

//5초마다 호출
@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
	// 주기적으로 실행되어야 하는 작업
}

//고정 지연 및 고정 속도 작업의 경우 다음 고정 속도 예제에 표시된 대로 메서드를 처음 실행하기 전에 대기할 시간을 표시하여 초기 지연을 지정할 수 있다.
@Scheduled(initialDelay = 1000, fixedRate = 5000)
public void doSomething() {
	// 주기적으로 실행되어야 하는 작업
}

//cron 표현식을 이용하여 작업 스케줄링할 수 있다. 평일에만 실행
//zone 속성을 사용하여 cron 표현식이 해석되는 시간대를 지정할 수도 있다.
@Scheduled(cron="*/5 * * * * MON-FRI") 
public void doSomething() {
	// 주기적으로 실행되어야 하는 작업
}

 

각 인스턴스에 대한 콜백을 예약하려는 경우가 아니면 런타임 시 동일한 @Scheduled 주석 클래스의 여러 인스턴스를 초기화하지 않는지 확인해야한다.


@Scheduled 애노테이션을 추가하고 컨테이너에 일반 스프링 빈으로 등록되는 Bean 클래스에 @Configurable을 사용하지 않는지 확인해야한다.

그렇지 않으면 두 번 초기화(컨테이너를 통해 한 번,
@Configurable 측면을 통해 한 번)를 받게 되며 결과적으로 각 @Scheduled 메서드가 두 번 호출된다.

@Async 애노테이션

  • 해당 메서드의 호출이 비동기적으로 발생하도록 메서드에 @Async 애노테이션을 사용할 수 있다.
    • 호출자는 호출 즉시 반환하지만 메서드의 실제 실행은 스프링 TaskExecutor에 제출된 작업에서 발생한다.
  • @Scheduled 주석이 달린 메서드와 달리 이러한 메서드는 컨테이너가 관리하는 예약된 작업이 아닌 런타임 시 호출자가 "일반적인" 방식으로 호출하기 때문에 인수를 허용할 수 있다.
    @Async
    void doSomething() {
    	// 비동기로 실행
    }
    
    @Async
    void doSomething(String s) {
    	// 비동기로 실행
    }
    
    @Async
    Future<String> returnSomething(int i) {
    	// 비동기로 실행
    }

 

  • @Async@PostConstruct 과 같이 빈 수명주기 콜백과 함께 사용할 수 없다.
    • 스프링 빈을 비동기식으로 초기화하려면 아래 예제와 같이 대상에서 주석이 달린 메서드를 호출하는 별도의 초기화 Spring Bean을 사용해야 한다.
      public class SampleBeanImpl implements SampleBean {
      
      	@Async
      	void doSomething() {
      		// ...
      	}
      
      }
      
      public class SampleBeanInitializer {
      
      	private final SampleBean bean;
      
      	public SampleBeanInitializer(SampleBean bean) {
      		this.bean = bean;
      	}
      
      	@PostConstruct
      	public void initialize() {
      		bean.doSomething();
      	}
      
      }
  • @Async 에 Executor 지정(스레드풀 지정)
    @Configuration
    @EnableAsync
    public class SpringAsyncConfig {
        @Bean(name = "threadPoolTaskExecutor")
        public Executor threadPoolTaskExecutor() {
    		ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    		taskExecutor.setCorePoolSize(3); // 기본 스레드 수
    		taskExecutor.setMaxPoolSize(20); // 최대 스레드 수
    		taskExecutor.setQueueCapacity(100); // Queue 사이즈
    		taskExecutor.setThreadNamePrefix("threadPoolTaskExecutor-");
    		return taskExecutor;
        }
    }
    @Async("threadPoolTaskExecutor")
    void doSomething(String s) {
    	// threadPoolTaskExecutor에 의해 비동기 실행
    }

 

크론 표현식(Cron Expressions)

┌───────────── second (0-59)
│ ┌───────────── minute (0 - 59)
│ │ ┌───────────── hour (0 - 23)
│ │ │ ┌───────────── day of the month (1 - 31)
│ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
│ │ │ │ │ ┌───────────── day of the week (0 - 7)
│ │ │ │ │ │          (0 or 7 is Sunday, or MON-SUN)
│ │ │ │ │ │
* * * * * *

 

참고

 

반응형

+ Recent posts