Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 26장 태스크(Task) 실행과 스케줄링

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.



26. 태스크(Task) 실행과 스케줄링

26.1 소개

스프링 프레임워크는 TaskExecutor 인터페이스와 TaskScheduler 인터페이스로 태스크의 비동기 시행과 스케줄링에 대한 추상화를 각각 제공한다. 스프링은 이 인터페이스를 사용해서 애플리케이션 서버 환경내에서 CommonJ로 위임하거나 스레드 풀을 지원하는 구현체도 제공하고 있다. 그래서 공통 인터페이스를 사용하는 이러한 구현체들을 사용해서 Java SE5, Java SE 6, Java EE 환경 간의 차이점을 추상화해버린다.

스프링은 JDK 1.3부터 추가된 Timer와 Quartz 스케줄러(http://www.opensymphony.com/quartz/)를 사용한 스케줄링을 지원하는 통합 클래스도 제공한다. 이 두가지 스케줄러 모두 FactoryBean에 Timer나 Trigger에 대한 참조를 각각 전달해서 구성한다. 또한, 이미 존재하는 대상 객체의 메서드를 호출할 수 있는(일반적인 MethodInvokingFactoryBean 작업과 유사하다.) Quartz 스케줄러와 Timer에 대한 편의 클래스도 사용할 수 있다.

26.2 스프링 TaskExecutor 추상화

스프링 2.0에는 executor를 다루는 새로운 추상화가 도입되었다. executor는 스레드 풀의 개념에 대해 Java 5에서 도입된 개념이다. 사용하는 구현체가 실제로 풀(pool)이라는 보장이 없기 때문에 "executor"라는 이름을 사용한다. executor는 싱글 스레드가 될 수도 있고 동기화가 될 수도 있다. 스프링의 추상화는 Java SE 1.4와 Java SE 5, Java EE 환경 사이의 자세한 구현부를 숨겨준다.

스프링의 TaskExecutor 인터페이스는 java.util.concurrent.Executor 인터페이스와 같다. 사실 이 인터페이스가 존재하는 이유는 스레드 풀을 사용할 때 Java 5에 대한 요구사항을 추상화하기 위해서다. TaskExecutor 인터페이스는 스레드 풀의 의미(semantic)와 구성에 기반을 둬서 실행하려고 태스크(task)를 받는 execute(Runnable task) 메서드를 가진다.

TaskExecutor는 원래 스레드 풀에 대한 추상화가 필요한 곳에 다른 스프링 컴포넌트를 제공하기 위해 만들어진 것이다. ApplicationEventMulticaster나 JSM의 AbstractMessageListenerContainer나 Quartz 통합 같은 컴포넌트들은 모두 스레드 풀에 TaskExecutor 추상화를 사용한다. 하지만 빈(bean)에서 스레드 풀의 동작이 필요하다면 자신의 요구사항에 맞게 이 추상화를 사용할 수 있다.

26.2.1 TaskExecutor의 종류

스프링 배포본에 포함된 다수의 TaskExecutor 구현체들이 있다. 아마 직접 구현할 일은 거의 없을 것이다.

  • SimpleAsyncTaskExecutor

    이 구현에는 어떤 스레드도 재사용하지 않고 호출마다 새로운 스레드를 시작한다. 하지만 이 구현체는 동시접속 제한(concurrency limit)을 지원해서 제한 수가 넘어서면 빈 공간이 생길 때까지 모든 요청을 막을 것이다. 실제 풀링(pooling)을 원한다면 뒷부분을 더 봐야 한다.
  • SyncTaskExecutor

    이 구현체는 호출을 비동기적으로 실행하지 않고 대신, 각 호출이 호출 스레드에 추가된다. 간단한 테스트 케이스처럼 멀티스레드가 필요하지 않은 상황에서 주로 사용한다.
  • ConcurrentTaskExecutor

    이 구현체는 Java 5 java.util.concurrent.Executor의 래퍼다. 또 다른 대안은 빈 프로퍼티로 Executor 설정 파라미터를 노출하는 ThreadPoolTaskExecutor다. ConcurrentTaskExecutor를 사용해야 하는 경우는 드물지만 ThreadPoolTaskExecutor가 원하는 만큼 안정적이지 않다면 ConcurrentTaskExecutor를 대신 사용할 수 있다.
  • SimpleThreadPoolTaskExecutor

    이 구현체는 실제로 Quartz SimpleThreadPool의 하위클래스로 스프링의 생명주기 콜백을 받는다. 이는 Quartz와 Quartz가 아닌 컴포넌트 간에 공유해야 하는 스레드 풀이 있는 경우에 보통 사용한다.
  • ThreadPoolTaskExecutor

    이 구현체는 자바 5 환경에서만 사용할 수 있지만, 자바 5 환경에서는 가장 일반적으로 사용한다. 이 구현체는 java.util.concurrent.ThreadPoolExecutor를 구성하는 빈 프로퍼티를 노출하고 이를 TaskExecutor로 감싼다. ScheduledThreadPoolExecutor같은 고급 기능이 필요하다면 ConcurrentTaskExecutor를 대신 사용하기를 권장한다.
  • TimerTaskExecutor

    이 구현체는 지원 구현체로 단일 TimerTask를 사용한다. SyncTaskExecutor와 다른 부분은 같은 스레드에서 동기로 동작하더라도 메서드 호출을 다른 스레드에서 한다는 점이다.
  • WorkManagerTaskExecutor

    이 구현체는 CommonJ WorkManager를 지원 구현체로 사용하고 스프링 컨텍스트에서 CommonJ WorkManager 참조를 구성하는 핵심 클래스다. SimpleThreadPoolTaskExecutor와 유사하게 이 클래스는 WorkManager 인터페이스를 구현했으므로 WorkManager처럼 직접 사용할 수 있다.
이 구현체는 java.util.concurrent 패키지의 어떤 백포트나 다른 버전과 함께 사용할 수 없다. Doug Lea의 구현체와 Dawid Kurzyniec의 구현체 모두 다른 패키지 구조를 사용해서 제대로 동작하지 않을 것이다.


CommonJ는 BEA와 IBM이 함께 개발한 명세다. 이 명세는 Java EE 표준은 아니지만, BEA와 IBM의 애플리케이션 서버 구현체의 표준이다.


26.2.2 TaskExecutor의 사용

스프링의 TaskExecutor 구현체를 간단한 JavaBean으로 사용한다. 다음 예제에서는 메시지를 비동기로 출력하려고 ThreadPoolTaskExecutor를 사용하는 빈을 정의했다.

import org.springframework.core.task.TaskExecutor;

public class TaskExecutorExample {
  private class MessagePrinterTask implements Runnable {

    private String message;

    public MessagePrinterTask(String message) {
      this.message = message;
    }

    public void run() {
      System.out.println(message);
    }
  }

  private TaskExecutor taskExecutor;

  public TaskExecutorExample(TaskExecutor taskExecutor) {
    this.taskExecutor = taskExecutor;
  }

  public void printMessages() {
    for(int i = 0; i < 25; i++) {
      taskExecutor.execute(new MessagePrinterTask("Message" + i));
    }
  }
}

여기서 보듯이 풀(pool)에서 스레드를 가져와서 직접 실행하는 대신 큐에 Runnable를 추가하고 TaskExecutor가 테스트를 언제 실행할지 결정하기 위해 내부의 규칙을 사용한다.

TaskExecutor가 사용할 규칙을 설정하려고 빈(bean) 프로퍼티를 노출했다.

<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
  <property name="corePoolSize" value="5" />
  <property name="maxPoolSize" value="10" />
  <property name="queueCapacity" value="25" />
</bean>

<bean id="taskExecutorExample" class="TaskExecutorExample">
  <constructor-arg ref="taskExecutor" />
</bean>


26.3 스프링의 TaskScheduler 추상화

TaskExecutor 추상화에 추가로 스프링 3.0에서는 미래의 어떤 시점에 실행할 태스크를 스케줄링하는 다양한 메서드를 가진 TaskScheduler를 도입되었다.

public interface TaskScheduler {
  ScheduledFuture schedule(Runnable task, Trigger trigger);

  ScheduledFuture schedule(Runnable task, Date startTime);

  ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period);

  ScheduledFuture scheduleAtFixedRate(Runnable task, long period);

  ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay);

  ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay);
}

Runnable과 Date만 받는 'schedule' 메서드가 가장 간단한 메서드다. 이 메서드는 지정한 시간 후에 태스크를 한번 실행할 것이다. 다른 모든 메서드는 반복해서 실행할 태스크를 스케줄링할 수 있다. fixed-rate 메서드와 fixed-delay 메서드는 간단하고 주기적인 실행을 위한 메서드이지만 훨씬 더 유연한 Trigger를 받을 수 있다.

26.3.1 Trigger 인터페이스

Trigger 인터페이스는 기본적으로 JSR-236(스프링 3.0에서는 아직 공식적으로 구현되지 않았다.)의 영향을 받았다. Trigger 인터페이스의 기본적인 개념은 과거의 실행 결과나 임의의 상황에 기반을 둬서 실행시간을 결정한다는 것이다. 이전 실행의 결과를 고려하는 경우 해당 정보는 TriggerContext내에 있다. Trigger 인터페이스 자체는 아주 간단하다.

public interface Trigger {
  Date nextExecutionTime(TriggerContext triggerContext);
}

여기서 TriggerContext가 가장 중요한 부분이다. TriggerContext는 모든 관련 데이터를 은닉화하고 나중에 필요할 때 확장할 수 있게 열려있다. TriggerContext는 인터페이스다. (SimpleTriggerContext 구현체를 기본값으로 사용한다.) Trigger 구현체에서 사용할 수 있는 메서드가 다음에 나와 있다.

public interface TriggerContext {
  Date lastScheduledExecutionTime();

  Date lastActualExecutionTime();

  Date lastCompletionTime();
}


26.3.2 Trigger 구현체

스프링은 Trigger 인터페이스의 두 구현체를 제공한다. CronTrigger가 가장 흥미로운데 이는 cron 표현식으로 태스크를 스케줄링할 수 있다. 예를 들어 다음 태스크는 주중 "업무시간"인 9~5시 사이의 매시간 15분에 실행된다.

scheduler.schedule(task, new CronTrigger("* 15 9-17 * * MON-FRI"));

또 다른 구현체인 PeriodicTrigger는 고정된 기간을 받고 선택적으로 초기 지연값과 지정한 기간을 고정된 비율로 해석할지 고정된 지연시간으로 해석할지 나타내는 불리언값을 받는다. TaskScheduler가 고정된 비율이나 고정된 지연시간으로 태스크를 스케쥴일하는 메서드를 이미 정의하고 있으므로 가능하면 TaskScheduler의 메서드를 직접 사용할 것이다. Trigger 추상화에 의존하는 컴포넌트내에서 PeriodicTrigger 구현체의 값을 사용할 수 있다. 예를 들어 정기적인 트리거, 크론 기반의 트리거, 임의의 트리거 구현을 상호교환해서 편리하게 사용할 수 있다. 이러한 컴포넌트는 의존성 주입을 할 수 있으므로 Triggers 등을 외부에서 설정할 수도 있다.

26.3.3 TaskScheduler 구현체

스프링의 TaskExecutor 추상화처럼 TaskScheduler는 스케줄링 작업에 의존하는 코드가 특정 스케줄러 구현체에 의존할 필요가 없다는 것이 주요한 장점이다. TaskScheduler의 유연성은 애플리케이션이 직접 스레드를 생성하지 않아야 하는 애플리케이션 서버 환경에서 동작하는 경우와 특히 관련이 있다. 이러한 경우를 위해서 스프링은 CommonJ TimerManager 인스턴스(보통 JNDI 검색으로 설정하는)에 위임하는 TimerManagerTaskScheduler를 제공한다.

아니면 더 간단한 방법으로 외부 스레드 관리가 필요치 않은 경우에 ThreadPoolTaskScheduler를 사용할 수 있다. 내부적으로 ThreadPoolTaskScheduler는 ScheduledExecutorService 인스턴스에 위임한다. 사실 ThreadPoolTaskScheduler는 스프링의 TaskExecutor 인터페이스를 구현했으므로 스케줄된 대로 가능한한 빨리 비동기로 실행하는데(혹은 반복된 실행) 단일 인스턴스를 사용할 수 있다.

26.4 태스크(Task) 네임스페이스

스프링 3.0부터 TaskExecutor와 TaskScheduler 인스턴스를 구성하는 XML 네임스페이스가 존재하고 트리거로 예약된 태스크를 구성하는 간편한 방법도 제공한다.

26.4.1 'scheduler' 엘리먼트

아래의 엘리먼트는 지정한 크기의 스레드풀을 가진 ThreadPoolTaskScheduler 인스턴스를 생성한다.

<task:scheduler id="scheduler" pool-size="10"/>

'id' 속성으로 제공한 값은 풀(pool)에서 스레드 이름의 접두사로 사용한다. 'scheduler' 엘리먼트는 비교적 직관적이다. 'pool-size' 속성을 지정하지 않았다면 기본 스레드풀은 하나의 스레드만 가질 것이다. scheduler에 다른 설정 옵션은 없다.

26.4.2 'executor' 엘리먼트

다음은 ThreadPoolTaskExecutor 인스턴스를 생성한다.

<task:executor id="executor" pool-size="10"/>

앞에서 본 scheduler와 마찬가지로 'id' 속성은 풀(pool)에서 스레드 이름의 접두사로 사용한다. 풀의 크기에 관심이 있다면 'executor' 엘리먼트는 'scheduler' 엘리먼트보다 많은 설정 옵션을 지원한다. ThreadPoolTaskExecutor의 스레드풀 자체를 더 상세하게 설정할 수 있다. 그냥 하나의 사이즈를 지정하는 것이 아니라 executor의 스레드풀은 core와 max 크기에 다른 값을 줄 수 있다. 하나의 값을 지정한다면 executor는 고정된 크기의 스레드 풀을 가진다.(core와 max 크기가 같은 경우) 하지만 'executor' 엘리먼트의 'pool-size' 속성도 "min-max" 형식의 범위도 받을 수 있다.

<task:executor id="executorWithPoolSizeRange"
     pool-size="5-25"
     queue-capacity="100"/>

이 설정에서는 'queue-capacity' 값도 지정했다. 스레드풀의 설정은 익스큐터 큐의 크기와도 관련된다. 풀의 크기와 큐의 크기 간의 관계에 대한 자세한 내용은 ThreadPoolExecutor 문서를 참고해라. 핵심 개념은 태스크를 제출했을 때 executor는 액티브 스레드의 수가 코어의 개수보다 적다면 놀고 있는 스레드를 먼저 사용한다는 것이다. 활성화된 스레드 수가 코어의 개수만큼이고 큐의 용량이 꽉 차지 않았다면 태스크를 큐에 추가할 것이다. 큐의 용량이 꽉 찼다면 익스큐터는 코어 개수 이상의 새로운 스레드를 생성할 것이다. 최대 사이즈에 이르렀다면 익스큐터는 해당 태스크를 거절한다.

풀의 모든 스레드가 바쁜 상태일 때 해당 큐에 태스크가 추가된다면 OutOfMemoryErrors가 발생할 수 있으므로 보통 큐가 언바인드되는 것은 원하는 설정이 아닐 것이다. 게다가 큐가 언바인드되면 최대 사이즈는 아무런 영향을 주지 않는다. 익스큐터는 코어 개수 이상의 새로운 스레드를 생성하기 전에 항상 큐를 먼저 시도하므로 코어 개수보다 많이 스레드 풀을 늘리려면 큐가 반드시 정해진 용량을 가져야 한다.(이것이 고정된 크기의 풀이 언바인드된 큐를 사용하는 경우에만 합리적인 이유이다.)

이제 풀의 크기 설정을 제공하는 경우에 고려해야 한 또 다른 요소인 keep-alive 설정을 볼 것이다. 우선 앞에서 언급했던 태스크를 거절한 경우를 생각해 보자. 보통 태스크가 거절되면 스레드풀 익스큐터는 TaskRejectedException를 던질 것이지만 거절 정책을 설정할 수 있다. 기본 거절 정책인 AbortPolicy 구현체를 사용하는 경우에 TaskRejectedException 예외를 던진다. 과부하상태에서 일부 태스크는 건너뛸 수 있는 애플리케이션에서는 DiscardPolicy나 DiscardOldestPolicy를 대신 설정할 수 있다. 과부하상태에서 제출된 태스크를 제어(throttle)해야 하는 애플리케이션에서는 CallerRunsPolicy도 좋은 선택이다. 이 정책은 예외를 던지거나 태스크를 버리지 않고 스레드가 테스트 자체를 실행하는 submit 메서드를 호출하도록 강제할 것이다. 이 정책의 개념은 이러한 호출자는 태스크를 실행하는 동안에는 바빠서 다른 태스크를 즉시 제출하지 못하리라는 것이다. 그러므로 스레드 풀과 큐의 제한을 유지하면서 부하를 제거하는 간단한 방법을 제공한다. 보통 이를 통해서 익스큐터가 "밀려있는" 태스크를 처리할 수 있으므로 큐나 풀 용량에 여유가 생긴다. 'executor' 요소의 'rejection-policy' 속성에 이러한 옵션 중 어느 것이라도 사용할 수 있다.

<task:executor id="executorWithCallerRunsPolicy"
    pool-size="5-25"
    queue-capacity="100"
    rejection-policy="CALLER_RUNS"/>


26.4.3 'scheduled-tasks' 요소

스프링 task 네임스페이스에서 가장 강력한 기능은 스프링 애플리케이션 컨텍스트에서 스케줄링 되는 태스크를 설정하는 기능이다. 이 기능도 JMS 네임스페이스에서 메시지주도 POJO를 설정하는 것과 같은 다른 스프링의 "메서드-인보커"와 비슷한 접근을 한다. 기본적으로 "ref" 속성은 스프링이 관리하는 어떤 객체라도 가리킬 수 있고 "method" 속성은 해당 객체에서 호출될 메서드명을 지정한다. 다음은 간단한 예제이다.

<task:scheduled-tasks scheduler="myScheduler">
  <task:scheduled ref="someObject" method="someMethod" fixed-delay="5000"/>
</task:scheduled-tasks>

<task:scheduler id="myScheduler" pool-size="10"/>

예제에서 보듯이 외부 요소가 schedule를 참조하고 각 task는 트리거 메타데이터의 설정을 가지고 있다. 앞의 예제에서 메타데이터는 고정된 지연시간을 가진 주기적인 트리거를 정의하고 있다. 이는 "고정된 비율(fixed-rate)"로 설정하거나 "cron"속성을 사용해서 더 세밀하게 제어할 수도 있다. 다음은 이러한 옵션을 사용한 예제이다.

<task:scheduled-tasks scheduler="myScheduler">
  <task:scheduled ref="someObject" method="someMethod" fixed-rate="5000"/>
  <task:scheduled ref="anotherObject" method="anotherMethod" cron="*/5 * * * * MON-FRI"/>
</task:scheduled-tasks>

<task:scheduler id="myScheduler" pool-size="10"/>


26.5 스케줄링과 비동기 실행에 대한 어노테이션 지원

스프링 3.0은 task 스케줄링과 비동기 메서드 실행에 대한 어노테이션도 지원한다.

26.5.1 @Scheduled 어노테이션

트리거 메타데이터와 함께 @Scheduled 어노테이션을 메서드에 추가할 수 있다. 예를 들어 다음 예제는 고정된 시간이 5초마다 호출되고 이 시간은 이전 호출이 완료된 시점부터 계산될 것이다.

@Scheduled(fixedDelay=5000)
public void doSomething() {
  // 주기적으로 실행될 것이다
}

고정된 비율로 실행하기를 원한다면 어노테이션에서 지정한 프로퍼티명만 바꿔주면 된다. 다음 예제는 각 호출의 연속적인 시작 시각의 간격으로 계산된 5초마다 실행될 것이다.

@Scheduled(fixedRate=5000)
public void doSomething() {
  // 주기적으로 실행될 것이다
}

간단한 주기 스케줄링으로 충분하지 않다면 cron 표현식을 사용할 수 있다. 다음은 평일에만 실행될 것이다.

@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
  // 평일에만 실행될 것이다
}

스케쥴되는 메서드는 반환 타입이 반드시 void이고 아무런 인자도 받지 말아야 한다. 애플리케이션 컨텍스트의 다른 객체와 메서드가 상호작용해야 한다면 의존성 주입으로 제공하는 것이 일반적이다.

Note
각 인스턴스에 대한 콜백을 스케줄링하려는 것이 아니라면 런타임시에 같은 @Scheduled 어노테이션 클래스의 여러 인스턴스를 초기화하지 않도록 해야 한다. 이와 관련해서 @Scheduled 어노테이션이 붙고 컨테이너에 일반적인 스프링 빈으로 등록된 bean 클래스에 @Configurable를 사용하지 말아야 한다. 그렇지 않으면 두 번 초기화되는 상황이 발생해서(한번은 컨테이너에서 발생하고 한번은 @Configurable 관점(aspect)에서 발생한다) 각 @Scheduled 메서드는 두 번 호출되게 된다.


26.5.2 @Async 어노테이션

@Async 어노테이션은 메서드에 사용해서 메서드를 비동기로 호출할 수 있다. 즉, 콜러(caller)는 호출하자마자 바로 결과를 받고 실제 메서드 실행은 스프링 TaskExecutor에 제출된 task에서 일어날 것이다. 가장 간단한 경우는 void를 반환하는 메서드에 @Async 어노테이션을 적용하는 것이다.

@Async
void doSomething() {
  // 이 메서드는 비동기로 실행될 것이다
}

@Scheduled 어노테이션이 붙은 메서드와는 달리 @Async 어노테이션이 붙은 메서드는 컨테이너가 관리하는 스케쥴된 task가 아니라 런타임시에 콜러가 "일반적인" 방법으로 호출하기 때문에 인자를 받을 수 있다. 예를 들어 다음 코드는 유효한 @Async 어노테이션을 사용한 애플리케이션이다.

@Async
void doSomething(String s) {
  // 이 메서드는 비동기로 실행될 것이다
}

값을 반환하는 메서드로 비동기로 호출할 수 있지만 이러한 메서드는 반환 값이 Future 타입이어야 한다. 이는 비동기 실행의 이점을 가지므로 콜러(caller)는 Future에서 get()를 호출하기 전에 다른 작업을 할 수 있다.

@Async
Future<String> returnSomething(int i) {
  // 이 메서드는 비동기로 실행될 것이다
}

@Async는 @PostConstruct처럼 생명주기 콜백과 동시에 사용할 수 없다. 현재 스프링 빈을 비동기로 초기화하려면 타겟의 @Async 어노테이션이 붙은 메서드를 호출하는 스프링 빈을 초기화하는 작업을 분리해서 사용해야 한다.

public class SampleBeanImpl implements SampleBean {
  @Async
  void doSomething() { … }
}

public class SampleBeanInititalizer {
  private final SampleBean bean;

  public SampleBeanInitializer(SampleBean bean) {
    this.bean = bean;
  }

  @PostConstruct
  public void initialize() {
    bean.doSomething();
  }
}

26.5.3 요소

@Scheduled과 @Async 어노테이션을 둘 다 활성화하려면 설정에서 task 네임스페이스에 'annotation-driven' 요소를 추가해라.

<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>

<task:executor id="myExecutor" pool-size="5"/>

<task:scheduler id="myScheduler" pool-size="10"/>

executor 참조는 @Async 어노테이션이 붙은 메서드에 대응하는 태스크를 처리하기 위해 제공하고 scheduler 참조는 @Scheduled 어노테이션이 붙은 메서드를 관리하기 위해서 제공한다.

26.6 OpenSymphony Quartz 스케쥴러의 사용

Quartz는 모든 종류의 작업 스케줄링을 인식하는데 Trigger, Job, JobDetail 객체를 사용한다. Quartz의 기본 개념에 대해서는 http://www.opensymphony.com/quartz를 참고해라. 스프링은 스프링에 기반한 애플리케이션에서 Quartz를 간단히 사용할 수 있도록 클래스 다수를 제공한다.

26.6.1 JobDetailBean의 사용

JobDetail 객체에는 작업을 실행하는 데 필요한 모든 정보가 담겨있다. 스프링 프레임워크는 합리적인 기본값으로 실제 자바 빈의 JobDetail를 만드는 JobDetailBean를 제공한다. 예제를 살펴보자.

<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailBean">
  <property name="jobClass" value="example.ExampleJob" />
  <property name="jobDataAsMap">
    <map>
      <entry key="timeout" value="5" />
    </map>
  </property>
</bean>

job detail bean에는 작업(ExampleJob)에 필요한 모든 정보가 담겨있다. timeout은 job data map에서 지정한다. job data map은 JobExecutionContext로(실싱히세 전달되는) 사용할 수 있지만 JobDetailBean도 job data map의 프로퍼티에서 실제 job의 프로퍼티로 매핑한다. 그러므로 이 예제에서 ExampleJob에 timeout라는 프로퍼티가 있다면 JobDetailBean는 자동으로 이를 적용할 것이다.

package example;

public class ExampleJob extends QuartzJobBean {
  private int timeout;

  /**
   * Setter는 ExampleJob이 JobDetailBean의 값(5)으로
   * 인스턴스화된 후에 호출된다.
   */
  public void setTimeout(int timeout) {
    this.timeout = timeout;
  }

  protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
    // 실제 작업을 한다
  }
}

job detail bean의 부가적인 모든 설정도 당연히 사용할 수 있다.

Note: name와 group 프로퍼티를 사용해서 작업의 이름과 그룹을 변경할 수 있다. 기본적으로 작업의 이름은 작업의 bean detail 빈의 이름과 일치한다.(위의 예제에서는 exampleJob이다.)

26.6.2 MethodInvokingJobDetailFactoryBean의 사용

특정 객체의 메서드를 그냥 호출해야 할 때가 있는데 MethodInvokingJobDetailFactoryBean를 사용해서 할 수 있다.

<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
  <property name="targetObject" ref="exampleBusinessObject" />
  <property name="targetMethod" value="doIt" />
</bean>

위 예제는 exampleBusinessObject에서 doIt 메서드가 호출될 것이다.(다음 예제 참고)

public class ExampleBusinessObject {
  // properties와 collaborators

  public void doIt() {
    // 여기서 실제 작업을 한다
  }
}
<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>

MethodInvokingJobDetailFactoryBean를 사용하면 실행하기만 할 메서드를 위한 한 줄짜리 작업을 생성할 필요 없이 실제 비즈니스 객체를 생성하고 detail 객체와 연결하기만 하면 된다.

기본적으로 Quartz Job은 상태를 가지지 않고 작업끼리 서로 간섭할 가능성이 있다. 같은 JobDetail에 대한 두 가지 트리거를 지정했다면 첫 번째 작업이 끝나기 전에 두 번째 작업이 시작할 가능성이 있다. JobDetail 클래스가 Stateful 인터페이스를 구현하면 이런 일은 일어나지 않고 두 번째 작업은 첫 번째 작업이 끝나기 전에는 시작되지 않는다. MethodInvokingJobDetailFactoryBean로 동시성이 없는 작업을 만들려면 concurrent 플래그를 false로 설정해라.

<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
  <property name="targetObject" ref="exampleBusinessObject" />
  <property name="targetMethod" value="doIt" />
  <property name="concurrent" value="false" />
</bean>
Note
기본적으로 작업은 동시성을 가진 채 실행될 것이다.


26.6.3 트리커와 SchedulerFactoryBean를 사용하는 작업의 연결

job details와 job을 생성했고 특정 객체의 메서드를 호출할 수 있는 편리한 빈도 살펴봤다. 물론 아직 작업 자체를 스케줄링해야 한다. 이는 트리거와 SchedulerFactoryBean을 사용해서 스케줄링한다. Quartz에서는 다수의 트리거를 사용할 수 있고 스프링은 간편한 기본값을 가진 두 트리거 하위클래스 CronTriggerBean와 SimpleTriggerBean를 제공한다.

트리거를 스케줄링해야 하는데 스프링은 설정할 프로퍼티로 트리거를 노출하는 SchedulerFactoryBean를 제공한다. SchedulerFactoryBean은 이러한 트리거로 실제 작업을 스케줄링한다.

다음의 두 예제를 보자.

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
  
  <property name="jobDetail" ref="jobDetail" />
  
  <property name="startDelay" value="10000" />
  
  <property name="repeatInterval" value="50000" />
</bean>

<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
  <property name="jobDetail" ref="exampleJob" />
  
  <property name="cronExpression" value="0 0 6 * * ?" />
</bean>

이제 두 가지 트리거를 설정했다. 한 트리거는 10초 후에 시작해서 50초마다 실생하고 다른 트리거는 매일 오전 6시에 실행한다. 이 예제가 동작하려면 SchedulerFactoryBean를 구성해야 한다.

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
  <property name="triggers">
    <list>
      <ref bean="cronTrigger" />
      <ref bean="simpleTrigger" />
    </list>
  </property>
</bean>

이 외에도 job details가 사용하는 calendars나 Quartz를 커스터마이징할 수 있는 프로퍼티 등 SchedulerFactoryBean에서 설정할 프로퍼티가 많이 있다. 자세한 내용은 SchedulerFactoryBean Javadoc를 참고해라.

26.7 JDK Timer 지원 사용하기

스프링에서 작업을 스케줄링하는 또 다른 방법은 JDK Timer 객체를 사용하는 것이다. 커스텀 트리거를 생성하거나 메서드를 호출하는 타이머를 사용할 수 있다. TimerFactoryBean를 사용해서 타이머를 연결한다.

26.7.1 커스텀 타이머 생성

TimerTask를 사용해서 Quartz 작업과 유사한 커스텀 타이머 태스크를 생성할 수 있다.

public class CheckEmailAddresses extends TimerTask {
  private List emailAddresses;

  public void setEmailAddresses(List emailAddresses) {
    this.emailAddresses = emailAddresses;
  }

  public void run() {
    // 모든 이메일 주소를 반복하면서 저장한다
  }
}

간단히 연결할 수 있다.

<bean id="checkEmail" class="examples.CheckEmailAddress">
  <property name="emailAddresses">
    <list>
      <value>test@springframework.org</value>
      <value>foo@bar.com</value>
      <value>john@doe.net</value>
    </list>
  </property>
</bean>

<bean id="scheduledTask" class="org.springframework.scheduling.timer.ScheduledTimerTask">
  
  <property name="delay" value="10000" />
  
  <property name="period" value="50000" />
  <property name="timerTask" ref="checkEmail" />
</bean>

period 프로퍼티를 0으로(혹은 음수로) 바꾸면 태스크를 딱 한 번만 실행하도록 할 수 있다.

26.7.2 MethodInvokingTimerTaskFactoryBean의 사용

Quartz 지원처럼 Timer지원에도 메서드를 주기적으로 호출할 수 있는 컴포넌트가 있다.

<bean id="doIt" class="org.springframework.scheduling.timer.MethodInvokingTimerTaskFactoryBean">
  <property name="targetObject" ref="exampleBusinessObject" />
  <property name="targetMethod" value="doIt" />
</bean>

위의 예제에서는 exampleBusinessObject의 doIt메서드가 호출된다.(다음 예제 참고)

public class BusinessObject {
  // properties와 collaborators

  public void doIt() {
    // 여기서 실제 작업을 한다
  }
}

ScheduledTimerTask예제에서 timerTask의 참조를 doIt 빈으로 바꾸면 정해진 스케줄로 doIt 메서드를 실행할 것이다.

26.7.3 요약: TimerFactoryBean을 사용해서 태스크 설정하기

TimerFactoryBean는 Quartz SchedulerFactoryBean와 비슷하게 실제 스케줄링을 구성할 수 있다. TimerFactoryBean는 Timer를 구성하고 참조한 태스크를 스케줄링할 수 있다. 데몬 스레드를 사용할지를 지정할 수 있다.

<bean id="timerFactory" class="org.springframework.scheduling.timer.TimerFactoryBean">
  <property name="scheduledTimerTasks">
    <list>
      <!-- 앞의 예제 참고 -->
      <ref bean="scheduledTask" />
    </list>
  </property>
</bean>
2014/06/30 23:31 2014/06/30 23:31