2.Spring Scheduling

본 장에서는 TaskExecutor와 TaskScheduler에 대해 간단히 살펴볼 것이다. 또한 XML과 Annotation 기반에서 이들을 사용하는 방법에 대해 알아보도록 하자.

2.1.TaskExecutor

TaskExecutor는 java.util.concurrent.Executor를 extends하여 정의된 인터페이스로써 지정된 Task를 실행시키기 위한 execute(Runnable task) 메소드를 정의하고 있다. Spring에서는 다양한 TaskExecutor 구현체를 제공하고 있으며 각 구현체에 대해서는 Spring Documentation 내의 TaskExecutor types를 참조하도록 한다.

Spring에서 제공하는 TaskExecutor를 사용하여 특정 Task를 실행시키는 TaskExecutor를 개발하기 위해서는 XML 기반으로 Spring TaskExecutor의 속성을 정의한 후, 구현 대상이 되는 TaskExecutor에서 정의된 Spring TaskExecutor를 Inject하여 사용하면 된다. 해당 TaskExecutor 내에서는 Inject한 Spring TaskExecutor의 execute() 메소드를 호출함으로써 Task를 실행할 수 있으며 Task Execution을 위한 Rule은 자체적으로 구현해야 한다. 한편 Thread 형태로 구현된 Task는 Spring TaskExecutor 구현체의 특성에 맞게 Thread Pool에 관리될 것이다.

public class PrintTaskExecutor {
    private TaskExecutor executor;

    public PrintTaskExecutor(TaskExecutor taskExecutor) {
        this.executor = taskExecutor;
    }

    public void print() {
        for (int i = 0; i < 3; i++) {
            executor.execute(new Task(i));
        }
    }

    private class Task implements Runnable {
        private int no;

        public Task(int no) {
            this.no = no;
        }
   
        public void run() {
            System.out.println("execute a Task" + no + " at " + new Date()
                + " with TaskExecutor");
        }
    }
}

위 코드는 print() 메소드 내에서 Spring TaskExecutor를 활용하여 Inner 클래스로 정의된 Task를 실행하는 PrintTaskExecutor의 일부이다. PringTaskExecutor의 print() 메소드를 호출하면 Thread 유형의 내부 Task에 구현된 run() 메소드가 3회 실행되는 것을 확인할 수 있을 것이다.

다음은 위에서 언급한 PrintTaskExecutor에 대한 속성 정의 내용의 일부이다.

<task:executor id="executor" pool-size="4" queue-capacity="4" rejection-policy="ABORT"/>
	
<bean id="task" class="anyframe.sample.scheduling.task.executor.PrintTaskExecutor">
    <constructor-arg ref="executor"/>
</bean>

위에서 언급한 PrintTaskExecutor 샘플 코드는 본 섹션 내의 다운로드 - anyframe.sample.scheduling을 통해 다운로드받을 수 있다.

2.2.TaskScheduler

다음은 Spring 3에서 새롭게 제공하고 있는 org.springframework.scheduling.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);
}

TaskScheduler는 Execution 대상이 되는 Task를 특정 시점 이후에 한 번 실행하거나 fixedRate 또는 fixedDelay 정보를 기반으로 주기적으로 실행할 수 있는 메소드를 제공하고 있다.

2.3.XML based Scheduling

Spring 3에서는 앞서 언급한 TaskExecutor(<task:executor/>)나 TaskScheduler(<task:scheduler/>)에 대한 속성 정의를 위해 task라는 Namespace를 제공한다. 또한 이를 이용하면 간편하게 Task Scheduling(<task:scheduled-task/>)을 위한 속성을 정의할 수 있게 된다. task Namespace를 사용하기 위해서는 해당 XML 파일 내의 <beans> 정의시 spring-task.xsd를 선언해 주어야 한다.

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:task="http://www.springframework.org/schema/task"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/task 
        http://www.springframework.org/schema/task/spring-task.xsd">		
    ...
</beans>

다음은 <task:scheduler/>를 사용한 속성 정의의 일부이다. 다음과 같이 속성을 정의한 경우 정의된 Pool Size를 기반으로 ThreadPoolTaskScheduler 인스턴스가 생성될 것이다. 정의된 id는 Pool에 관리될 Task Thread의 Prefix로 사용된다.

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

다음은 <task:executor/>를 사용한 속성 정의의 일부이다. 다음과 같이 속성을 정의한 경우 ThreadPoolTaskExecutor 인스턴스가 생성될 것이다. 또한 정의된 id는 Pool에 관리될 Task Thread의 Prefix로 사용된다.

<task:executor id="executor" pool-size="4" queue-capacity="4" rejection-policy="ABORT"/>

<task:executor/>는 <task:scheduler/>에 비해 다양한 속성 정의를 지원한다. 다음에서는 정의 가능한 속성들에 대해 자세히 살펴보도록 하자.

AttributeDescription
pool-sizeThread Pool의 Size를 결정한다. 단일값으로 정의하였을 경우 Pool Size가 정의된 크기로 고정된다. min-max 형태로 정의하였을 경우 Pool Size의 범위가 지정된다.
queue-capacity

현재 실행중인 Thread의 개수가 지정된 최소 Pool Size보다 작을 경우, TaskExecutor는 실행 대상 Task에 대해 Free Thread를 사용한다. 점점 실행 대상 Task가 증가하여 현재 실행중인 Thread의 개수가 지정된 최소 Pool Size와 같아지는 경우, 실행 대상 Task는 Queue에 추가된다. 이 때 추가 가능한 Task의 개수는 queue-capacity와 동일하다. 정의된 Queue Capacity를 모두 사용하게 된다면 TaskExecutor는 실행 대상 Task에 대해 New Thread를 생성하여 Pool에 추가하게 된다. 현재 실행중인 Thread의 개수가 지정된 최대 Pool Size를 초과하는 경우 비로소 TaskExecutor는 해당 Task 실행을 거부하게 된다. 이와 같이 pool-size는 queue-capacity와 같이 고려되어야 하는 속성 정보이며 pool-size와 queue-capacity의 상관 관계에 대한 자세한 정보는 ThreadPoolExecutor API를 참조하도록 한다.

queue-capacity 값을 정의하지 않는 경우 한계값이 정해지지 않으나 이 경우 너무 많은 실행 대상 Task가 Queuing 됨으로 인해 OutOfMemoryErrors를 초래할 수 있음에 유의하도록 한다. 또한 Queue Capacity에 대한 최대값이 존재하지 않으므로 Queue가 Full이 되는 상태가 발생하지 않아 결국 최대 Pool Size 또한 의미가 없어지게 된다.

keep-alive최소 Pool Size 초과로 생성된 Inactive Thread에 대해 keep-alive 값으로 지정한 시간이 지난 후에 timeout된다. 만일 TaskExecutor의 pool-size가 범위로 정의되어 있고, queue-capacity가 정의되어 있지 않는다면, Pool Size가 최소 크기를 넘지 않았을 경우라도 해당 Pool에 포함된 Inactive Thread에 대해서 timeout을 적용하게 된다. (초 단위로 지정 가능)
rejection-policy기본적으로 Task 실행이 거부되었을 경우 TaskExecutor는 TaskRejectedException을 throw하게 된다. 그러나 rejection-policy 값을 다음과 같이 정의하는 경우 정의된 Policy에 의해 다른 결과를 보여줄 수 있다.
  • ABORT : AbortPolicy 적용. rejection-policy가 정의되지 않았을 경우 기본 적용되는 Policy로 Exception을 throw한다.

  • CALLER_RUNS : CallerRunsPolicy 적용. 해당 Application이 과부하 상태일 경우 TaskExecutor에 의해서가 아닌 Thread에서 직접 Task를 실행시킬 수 있게 한다.

  • DISCARD : DiscardPolicy 적용. 모든 Task가 반드시 실행되어야 한다라는 제약점이 없는 경우 적용 가능한 Policy로써 해당 Application이 과부하 상태일 경우 현재 Task의 실행을 Skip한다.

  • DISCARD_OLDEST : DiscardOldestPolicy 적용. 모든 Task가 반드시 실행되어야 한다라는 제약점이 없는 경우 적용 가능한 Policy로써 해당 Application이 과부하 상태일 경우 Queue의 Head에 있는 Task의 실행을 Skip한다.

다음은 task Namespace의 가장 강력한 특징인 <task:scheduled-task/>를 사용한 속성 정의의 일부이다. <task:scheduled-task/>는 기본적으로 'scheduler'라는 속성을 가지고 있는데 이것은 내부에 정의된 Task를 Scheduling하기 위한 TaskScheduler Bean을 정의하기 위한 것이다. <task:scheduled-task/>는 하위에 다수의 <task:scheduled/>를 포함할 수 있으며 <task:scheduled/>의 'ref'와 'method'는 실행 대상이 되는 Bean과 해당 Bean 내에 포함된 실행 대상 메소드를 정의하기 위한 속성이다.

<task:scheduled-tasks scheduler="scheduler">
    <task:scheduled ref="task" method="printWithFixedDelay" fixed-delay="5000"/>
    <task:scheduled ref="task" method="printWithFixedRate" fixed-rate="10000"/>
    <task:scheduled ref="task" method="printWithCron" cron="*/8 * * * * MON-FRI"/>
</task:scheduled-tasks>

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

<task:scheduled/>는 'ref', 'method' 외에 Scheduling을 위해 필요한 속성을 가지는데 각각에 대해 알아보면 다음과 같다.

AttributeDescription
cron

Cron Expression을 이용하여 Task 실행 주기 정의.

Cron Expression은 6개의 Field로 구성되며 각 Field는 순서대로 second, minute, hour, day, month, weekday를 의미한다. 각 Field의 구분은 Space로 한다. 또한 month와 weekday는 영어로 된 단어의 처음 3개의 문자로 정의할 수 있다.

  • 0 0 * * * * : 매일 매시 시작 시점

  • */10 * * * * * : 10초 간격

  • 0 0 8-10 * * * : 매일 8,9,10시

  • 0 0/30 8-10 * * * : 매일 8:00, 8:30, 9:00, 9:30, 10:00

  • 0 0 9-17 * * MON-FRI : 주중 9시부터 17시까지

  • 0 0 0 25 12 ? : 매년 크리스마스 자정

* org.springframework.scheduling.support.CronSequenceGenerator API 참조

fixed-delay이전에 실행된 Task의 종료 시간으로부터의 fixed-delay로 정의한 시간만큼 소비한 이후 Task 실행. (Milliseconds 단위로 정의)
fixed-rate이전에 실행된 Task의 시작 시간으로부터 fixed-rate로 정의한 시간만큼 소비한 이후 Task 실행. (Milliseconds 단위로 정의)

위에서 언급한 PrintTaskExecutor 샘플 코드는 본 섹션 내의 다운로드 - anyframe.sample.scheduling을 통해 다운로드받을 수 있다.

2.4.Annotation based Scheduling & Asynchronous Execution

Spring 3에서는 Task Scheduling(@Scheduled)과 Aynchronous Task Execution(@Async)을 위한 Annotation을 제공한다. 이 Annotation들을 인식할 수 있도록 하기 위해서는 다음과 같은 속성 정의가 추가되어야 한다.

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

다음에서는 @Scheduled와 @Async Annotation에 대해 살펴보기로 하자.

2.4.1.Scheduling

@Scheduled는 메소드 단위로 사용 가능하며 실행 주기 정의를 위한 'fixedDelay', 'fixedRate', 'cron'과 같은 속성들을 제공하고 있다. 각 속성의 의미는 XML based Scheduling에서 언급한 <task:scheduled/> 속성과 동일한 의미를 가진다. @Scheduled 메소드에 대한 실행은 TaskScheduler가 담당한다.

@Scheduled(fixedDelay=5000)
public void printWithFixedDelay() {
    System.out.println("execute printWithFixedDelay() of Annotated PrintTask at " 
        + new Date());
}

@Scheduled(fixedRate=10000)
public void printWithFixedRate() {
    System.out.println("execute printWithFixedRate() of Annotated PrintTask at " 
        + new Date());
}	

@Scheduled(cron="*/8 * * * * MON-FRI")
public void printWithCron() {
    System.out.println("execute printWithCron() of Annotated PrintTask at " 
        + new Date());
}

@Scheduled Annotation을 부여한 메소드는 입력 인자를 갖지 않고, Return 값이 없어야 함에 유의하도록 한다. 또한 메소드 로직 실행을 위해 다른 Bean을 참조로 해야 한다면 Dependency Injection에 의해 처리하도록 한다.

2.4.2.Asynchronous Execution

@Async는 메소드 단위로 사용 가능하며 비동기적으로 특정 메소드를 실행하고자 할 때 사용할 수 있다. @Async 메소드에 대한 실제적인 실행은 TaskExecutor에 의해 처리된다.

@Async
public void printWithAsync() throws Exception {
    System.out.println("execute printWithAsync() of AsyncPrintTask at "	
        + new Date());
    Thread.sleep(5000);
}

@Async
public void printWithArg(int i) throws Exception {
    System.out.println("execute printWithArg(" + i + ") of AsyncPrintTask at " 
        + new Date());
    Thread.sleep(5000);
}

@Async
public Future<String> returnVal(int i) throws Exception {
    System.out.println("execute returnVal() of AsyncPrintTask");
    Date current = new Date();
    Thread.sleep(5000);
    return new AsyncResult<String>(Integer.toString(i));
}

위 코드에서와 같이 @Async 메소드는 @Scheduled 메소드와 다르게 입력 인자나 리턴값을 가질 수 있다. @Scheduled의 경우에는 Spring Container에 의해 관리되는 반면에 @Async는 Caller에 의해 직접 호출되기 때문이다. 단, 리턴값의 경우 Future 타입 형태만 가능하며 Caller는 비동기적으로 실행 종료된 메소드의 결과를 Future 객체의 get() 메소드를 통해 알아낼 수 있다.

위에서 언급한 PrintTaskExecutor 샘플 코드는 본 섹션 내의 다운로드 - anyframe.sample.scheduling을 통해 다운로드받을 수 있다.




출처 - http://dev.anyframejava.org/docs/anyframe/plugin/scheduling/4.5.2/reference/html/ch02.html








Spring 2.0은 실행자(Executor)를 다루기 위해 새로운 추상화를 소개한다. 실행자(Executor)는 쓰레드 풀링의 개념을 위한 Java 5의 이름이다. 뜻밖의(odd) 명명은 참조하는 구현물이 실제로 풀(pool)인것을 보증하지 않는 사실에 기반한다. 사실, 많은 경우, 실행자(executor)는 쓰레드 하나로 이루어진다. Spring의 추상화는 1.3, 1.4, 5와 Java EE환경간의 상세한 구현을 감추는 만큼 Java 1.3, 1.4환경에 쓰레드 풀링을 적용하도록 도와준다.

  1. TaskExecutor 인터페이스
    Spring의 TaskExecutor 인터페이스는 java.util.concurrent.Executor에 일치한다. 사실, 이것이 존재하는 가장 큰 이유는 쓰레드 풀을 사용할때 Java 5에 필요한 추상화이다. 인터페이스는 의미론적인 수행과 쓰레드 풀의 설정을 위한 작업을 받는 하나의 메소드인 execute(Runnable task)를 가진다.
  2. TaskExecutor를 사용하는 곳
    TaskExecutor는 필요한 쓰레드 풀링을 위한 추상화에 다른 Spring컴포넌트를 주기 위해 생성되었다. ApplicationEventMulticaster와 같은 컴포넌트인 JMS의 AbstractMessageListenerContainer와 Quartz통합은 모두 풀 쓰레드를 위해 TaskExecutor 추상화를 사용한다. 어쨌뜬, bean이 쓰레드 풀링이 필요하다면, 자체적인 필요를 위해 이 추상화를 사용하는 것이 가능하다.
  3. TaskExecutor 타입들
    Spring배포판에 포함된 TaskExecutor의 미리 빌드된 많은 구현물이 있다. 모든 가능성에서, 자체적으로 구현할 필요는 없을것이다.
    • SimpleAsyncTaskExecutor
      이 구현물은 어떤 쓰레드도 재사용하지 않는다. 각각의 호출에 새로운 쓰레드를 시작한다. 어쨌뜬, 슬롯이 자유로워질때까지 제한을 넘어선 호출은 블럭할 동시 제한을 지원한다. 진정한 풀링을 찾는다면, 페이지 아래로 스크롤하라.
    • SyncTaskExecutor
      이 구현물은 호출을 비동기적으로 수행하지 않는다. 대신, 각각의 호출은 호출 쓰레드로 대체된다. 간단한 테스트케이스와 같이 필요하지 않은 멀티쓰레드 상황에서 사용된다.
    • ConcurrentTaskExecutor
      이 구현물은 Java 5 java.util.concurrent.Executor를 위한 래퍼(wrapper)이다. 대안으로 bean프라퍼티로 Executor 설정 파라미터를 나타내는 ThreadPoolTaskExecutor가 있다. ConcurrentTaskExecutor를 사용할 필요는 드물지만 ThreadPoolTaskExecutor가 필요한 것만큼 충분히 견고하지 않다면, ConcurrentTaskExecutor이 대안이 된다.
    • SimpleThreadPoolTaskExecutor
      이 구현물은 실제로 Spring의 생명주기 콜백을 듣는 Quartz의 SimpleThreadPool의 하위클래스이다. 이것은 Quartz와 Quartz가 아닌 컴포넌트간에 공유될필요가 있는 쓰레드 풀을 가질때 사용된다.
    • ThreadPoolTaskExecutor
      이 구현물은 Java 5 환경에서 사용될수 있지만 그 환경에서 가장 공통적으로 사용되는 것이다. java.util.concurrent.ThreadPoolExecutor를 설정하고 TaskExecutor에 그것을 포장하기 위한 bean프라퍼티를 나타낸다. ScheduledThreadPoolExecutor처럼 향상된 어떤것이 필요하다면, 대신 ConcurrentTaskExecutor를 사용하도록 추천한다.
    • TimerTaskExecutor
      이 구현물은 지원 구현물로 하나의 TimerTask를 사용한다. 이것은 비록 쓰레드에서 동기적이더라도 메소드 호출이 개별 쓰레드에서 수행되어 SyncTaskExecutor와 다르다.
    • WorkManagerTaskExecutor
      이 구현물은 지원 구현물로 CommonJ WorkManager을 사용하고 Spring 컨텍스트에서 CommonJ WorkManager참조를 셋팅하기 위한 중심적이고 편리한 클래스이다. SimpleThreadPoolTaskExecutor와 유사하게, 이 클래스는 WorkManager인터페이스를 구현하고 WorkManager만큼 직접 사용될수 있다.
  4. TaskExecutor 사용하기
    Spring의 TaskExecutor 구현물은 간단한 JavaBean처럼 사용된다. 아래의 예제에서, 우리는 메시지의 세트를 비동기적으로 프린트하기 위한 ThreadPoolTaskExecutor을 사용하는 bean을 정의한다.
    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));
        }
      }
    
    }

    당신이 볼수 있는것처럼, 풀에서 쓰레드를 가져오고 스스로 수행하는 것보다, 큐에 Runnable를 추가하고 TaskExecutor는 작업이 수행될때 결정할 내부 규칙을 사용한다.

TaskExecutor가 사용할 규칙을 설정하기 위해, 간단한 bean프라퍼티로 나타낸다.

/WEB-INF/classes/applicationContext.xml
<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>

TaskExecutor 사용 예제에서는 TaskExecutorExample 클래스안에 MessagePrinterTask 객체를 inner class로 구현하였지만, 본 개발에서는 UserSyncScheduler와 userSyncTaskExecutor라는 것을 분리하여 각각의 class로 TaskExecutor와 Scheduler역할을 하는 객체를 분리하여 구현한다. 자세한 내용은 다음의 환경세팅 파일을 보듯이 작성을 하고, 실제 구현한 소스 파일를 참조하기 바란다.

Scheduler와 조합한 ThreadPoolTaskExecutor /WEB-INF/classes/applicationContext.xml
<bean id="userSyncTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
  <property name="corePoolSize" value="5" />
  <property name="maxPoolSize" value="10" />
  <property name="queueCapacity" value="25" />
</bean>

<bean id="userSyncGetterJob" class="org.springframework.scheduling.quartz.JobDetailBean">
    <property name="jobClass" value="com.nate.sims.async.scheduling.UserSyncScheduler" />
    <property name="jobDataAsMap">
        <map>
            <entry key="service">
                <ref bean="aSyncService"/><!-- aSyncService set 해준다. -->
            </entry>
            <entry key="executor">
                <ref bean="userSyncTaskExecutor"/><!-- userSyncTaskExecutor set 해준다. -->
            </entry>
        </map>
    </property>
</bean>

<!-- 스케쥴타임설정 -->
<bean id="cronUserSyncTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
    <property name="jobDetail" ref="userSyncGetterJob"/>
    <property name="cronExpression" value="0 * * * * ?" />
</bean>

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


출처 - http://blog.naver.com/PostView.nhn?blogId=ez2sarang&logNo=40060102403



Posted by linuxism
,