참조: http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/ch25s05.html

웹 애플리케이션을 띄울 때 구글 토크 봇을 로그인 시켜두려고 스케줄링을 이용하려 했습니다. 찾아보니까 애노테이션 기반으로 설정할 수 있는 기능이 추가됐더군요.

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

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

<task:scheduler id=”myScheduler” pool-size=”10″/>}

이렇게 task:annotation-driven 엘리먼트를 XML에 추가해주면 빈에 설정되어 있는 @Schedule과 @Async 애노테이션을 활성화 시켜줍니다.

@Schedule 애노테이션은 cron, fixedDelay, fixedRate 세 가지 속성 중 하나를 이용해서 설정해야 합니다. 반드시 이 셋 중에 하나는 설정되어 있어야 합니다.

@Async 애노테이션은 해당 메서드 호출을 비동기로 처리해주고 싶을 때 사용할 수 있습니다. 즉 이 애노테이션으로 스케줄링이 적용된 메서드를 호출하면 결과는 바로 리턴되고 실제 실행은 스프링의 TaskExecutor에 의해 별도의 Task 내부(이 녀석이 별도의 쓰레드겠죠)에서 실행됩니다.

막상 해보니 라이브러리 때문에 에러가 나더군요.

        <dependency>
            <groupId>edu.emory.mathcs.backport</groupId>
            <artifactId>com.springsource.edu.emory.mathcs.backport</artifactId>
            <version>3.1.0</version>
        </dependency>

그래서 필요한 라이브러리를 추가해주고 돌려보니까 잘 돌아갑니다.

그런데 해보고 나니까 굳이 반복 실행할 필요가 없는 메서드라;;; -_-;; @PostConstruct 애노테이션 붙여서 끝냈습니다.


출처 - http://whiteship.me/?tag=%EC%8A%A4%ED%94%84%EB%A7%81-3-0&paged=4


===================================================================================


Spring 3.0부터는 Quartz가 없이도 실행 스케쥴 설정이 가능합니다. 설정과 테스트 방법을 정리했습니다.

 

@Schedule 를 써서 크론 표현식 설정

  실행을 하고 싶은 메소드를 @Component, @Schedule annotaion을 통해 지정을 합니다.


@Component
public class BaseballScheduledJobLauncher extends JobLaunchSupport {

.....

    @Scheduled(cron="45 * * * * MON-FRI")
    public void baseballJob() {
        JobParameters params = createTimeParameter();
        run("baseballJob", params);
    }

   
    @Scheduled(cron="45 10 5 * * MON-FRI")
    public void baseballExportJob() {
        JobParameters params = createTimeParameter();
        run("baseballExportJob", params);
    }

}


 java 파일에 설정된 Annotation을 인식하기 위해서 Application context의 xml파일에 component-scan과 annotation으로 schedule을 설정하겠다는 선언을 추가합니다.  그리고 schedule을 실행할 thread pool의 크기도 지정합니다.


    <context:component-scan base-package="edu.batch.baseball.schedule"/>
    <task:scheduler id="myScheduler" pool-size="10"/>
    <task:annotation-driven scheduler="myScheduler"/>


 'task'와 'component'의 namespace가 xml에 추가되어 있어야 합니다.

테스트 코드

 테스트 코드를 단순하게 만들기 위해서 크론표현식을 추출하고 검사하는 코드는 SpringCronE-pressionTestUtils클래스로 분리했습니다.

 

package edu.batch.baseball.schedule;

import static edu.batch.support.launch.SpringCronE-pressionTestUtils.*;

import java.util.Arrays;
import java.util.List;

import org.junit.Test;

import edu.batch.baseball.schedule.BaseballScheduledJobLauncher;

public class BaseballScheduledJobLauncherTest {

    private static final String DATE_PATTERN = "yyyy/MM/dd hh:mm:ss";

    @Test
    public void testBaseballJobSchedule() {
        String initialTime = "2010/09/01 09:00:00";
        List<String> expectedTimeList = Arrays.asList(
                "2010/09/01 09:00:45",
                "2010/09/01 09:01:45",
                "2010/09/01 09:02:45");
        String cronE-pression = getCronE-pressionOfMethod(BaseballScheduledJobLauncher.class, "baseballJob");
        assertSchedule(cronE-pression, initialTime, expectedTimeList,DATE_PATTERN);
    }
}


 

 SpringCronE-pressionTestUtils

첨부파일 : SpringCronE-pressionTestUtils.java

 

package edu.batch.support.launch;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import java.lang.reflect.Method;
import java.text.ParseException;
import java.util.Date;
import java.util.List;

import org.apache.commons.lang.time.DateFormatUtils;
import org.apache.commons.lang.time.DateUtils;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.scheduling.support.SimpleTriggerContext;

public class SpringCronE-pressionTestUtils {

 public static String getCronE-pressionOfMethod(Class<?> targetClass,
   String methodName)  {
  Method scheduledMethod;
  try {
   scheduledMethod = targetClass.getDeclaredMethod(methodName,
     new Class[] {});
  } catch (SecurityException e) {
   throw new IllegalArgumentException("cannot access the method : " + methodName, e);
  } catch (NoSuchMethodException e) {
   throw new IllegalArgumentException(e);
  }
  Scheduled scheduleInfo = scheduledMethod.getAnnotation(Scheduled.class);
  String cronE-pression = scheduleInfo.cron();
  return cronE-pression;
 }

 

public static void assertSchedule(String cronE-pression, String initialTime,
   List<String> expectedTimeList, String datePattern) {
  CronTrigger trigger = new CronTrigger(cronE-pression);
  Date startTime;
  try {
   startTime = DateUtils.parseDate(initialTime,
     new String[] { datePattern });
  } catch (ParseException e) {
   throw new IllegalArgumentException("wrong date format", e);
  }
  SimpleTriggerContext context = new SimpleTriggerContext();
  context.update(startTime, startTime, startTime);

  for (String exptectedTime : expectedTimeList) {
   Date nextExecutionTime = trigger.nextExecutionTime(context);
   String actualTime = DateFormatUtils.format(nextExecutionTime,
     datePattern);
   assertThat("executed on expected time", actualTime,
     is(exptectedTime));
   context.update(nextExecutionTime, nextExecutionTime,
     nextExecutionTime);
  }
 }

}


출처 - http://blog.benelog.net/2802946


===================================================================================


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을 통해 다운로드받을 수 있다.

2.5.Resources

  • 다운로드

    다음에서 sample 코드를 포함하고 있는 Eclipse 프로젝트 파일을 다운받은 후, 압축을 해제한다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후, mvn compile exec:java -Dexec.mainClass=...이라는 명령어를 실행시켜 결과를 확인한다. 각 Eclipse 프로젝트 내에 포함된 Main 클래스의 JavaDoc을 참고하도록 한다.

    • Eclipse 기반 실행

      Eclipse에서 압축 해제 프로젝트를 import한 후, src/main/java 폴더 하위의 Main.java를 선택하고 마우스 오른쪽 버튼 클릭하여 컨텍스트 메뉴에서 Run As > Java Application을 클릭한다. 그리고 실행 결과를 확인한다.


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




Posted by linuxism

댓글을 달아 주세요