목차 >> Scheduler 
+- Spring Scheduler  
+- Quartz Job Scheduler  
+- Scheduler Luanch

17장 Scheduler

이 장의 목적은 Scheduler 관련 Framework의 관계와 연결방법에 대해 설명하는 것으로 한다. 각 단원마다 예제 및 Sample Code를 삽입하여 개발자가 쉽게 따라 개발할 수 있도록 하였다.
Glue Framework를 사용하여 Scheduler를 개발할 경우 필요한 내용들을 설명한다. 본 문서에서는 Spring Scheduler나 Quartz관련 부분은 꼭 필요한 부분만 설명하고 좀 더 구체적인 내용은 사이트를 참고하도록 한다.
Job Scheduler라 함은 특정 프로그램 모듈(Job)을 정주기로 또는 이벤트성으로 실행시켜 주는 시스템을 의미한다.
Glue Framework에서는 오픈소스인 Spring Scheduler 혹은 Quartz Job Scheduler를 이용하여 스케줄러 기능을 제공하며, 단순한 Job부터 복잡한 Job까지 모두 처리가능하며, J2EE, J2SE 환경의 어플리케이션에 사용 가능하다. 수행 할 Job은 POJO(Plain Old Java Object)로 직접 정의할 수도 있고 Glue에서 제공하는 Class를 사용해서 특정 서비스를 실행 시킬 수도 있다.

Spring Scheduler

Spring 3에서 부터는 간단하게 비동기 실행과 Task 스케쥴링을 TaskScheduler 인터페이스에 대한 추상화를 통해 제공하고 있다.
TaskScheduler는 Execution 대상이 되는 Task를 특정 시점 이후에 한 번 실행하거나 fixedRate 또는 fixedDelay 정보를 기반으로 주기적으로 실행할 수 있는 메소드를 제공하고 있다.

  • <task:scheduler/> : TaskScheduler(<task:scheduler/>)에 대한 속성 정의를 위해 task라는 Namespace를 제공한다. 또한 이를 이용하면 간편하게 Task Scheduling(<task:scheduled-task/>)을 위한 속성을 정의할 수 있게 된다. task Namespace를 사용하기 위해서는 해당 XML 파일 내의 <beans> 정의시 spring-task.xsd를 선언해 주어야 한다.
  • <task:scheduled-task/> : <task:scheduled-task/>는 기본적으로 'scheduler'라는 속성을 가지고 있는데 이것은 내부에 정의된 Task를 Scheduling하기 위한 TaskScheduler Bean을 정의하기 위한 것이다. <task:scheduled-task/>는 하위에 다수의 <task:scheduled/>를 포함할 수 있다.
  • <task:scheduled/> : <task:scheduled/>는 'ref'와 'method'는 속성를 갖고 실행주기를 정의한다. 'ref'는 실행 대상이 되는 Bean을 정의하고, 'method'는 실행 대상 Bean의 실행 대상 메소드를 정의한다.

다음은 이러한 것들이 정의된 예제이다.

<?xml version="1.0" encoding="UTF-8"?>
<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.2.xsd
                        http://www.springframework.org/schema/task
                        http://www.springframework.org/schema/task/spring-task.xsd">

    <task:scheduled-tasks scheduler="scheduler">
        <task:scheduled ref="glue-task-1" method="doGlueService" fixed-delay="50000"/>
        <task:scheduled ref="glue-task-1" method="doGlueService" fixed-rate="70000"/> 
        <task:scheduled ref="glue-task-1" method="doGlueService" cron="*/8 * * * * MON-FRI"/> 
        <task:scheduled ref="glue-task-2" method="doGlueService" fixed-delay="80000"/>
    </task:scheduled-tasks>
    <task:scheduler id="scheduler" pool-size="3" />

    <bean id="glue-task-1" class="com.poscoict.glueframework.scheduling.task.GlueTaskScheduler">
        <property name="ServiceName" value="task1-service"/>
    </bean>
    <bean id="glue-task-2" class="com.poscoict.glueframework.scheduling.task.GlueTaskScheduler">
        <property name="ServiceName" value="task2-service"/>
    </bean>
</beans>

예제의 <task:scheduler/>는 pool-size, id 속성이 사용되었다. 그래서 정의된 Pool Size를 기반으로 ThreadPoolTaskScheduler 인스턴스가 생성될 것이다. 정의된 id는 Pool에 관리될 Task Thread의 Prefix로 사용된다.
<task:scheduled/> 의 실행주기는 fixed-delay, fixed-rate, cron 속성을 통해 정의할 수 있다.

  • fixed-delay : 이전에 실행된 Task의 종료 시간으로부터의 fixed-delay로 정의한 시간만큼 소비한 이후 Task 실행. (Milliseconds 단위로 정의)
  • fixed-rate : 이전에 실행된 Task의 시작 시간으로부터 fixed-rate로 정의한 시간만큼 소비한 이후 Task 실행. (Milliseconds 단위로 정의)
  • cron : Cron Expression을 이용하여 Task 실행 주기 정의. Cron Expression은 6개의 Field로 구성되며 각 Field는 순서대로 second, minute, hour, day, month, weekday를 의미한다. 각 Field의 구분은 Space로 한다. 또한 month와 weekday는 영어로 된 단어의 처음 3개의 문자로 정의할 수 있다.
    Cron표현 의미
    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 참조

GlueTaskScheduler

<task:scheduled ref=. . . method=. . ./> 에 정의되는 Task Class로 Glue Framework에서는 특정 서비스를 시행 시킬수 있는 GlueTaskScheduler 클래스를 제공 하고 있다. 그러므로 XML에 Task Class로 com.poscoict.glueframework.scheduling.GlueTaskScheduler등록 하고 <task:scheduled../>에 method로 doGlueService를 설정하면 된다.

<task:scheduled-tasks scheduler="scheduler">
    <task:scheduled ref="glue-task-1" method="doGlueService" ... />
    ...
</task:scheduled-tasks>
<task:scheduler id="scheduler" ... />
<bean id="glue-task-1" class="com.poscoict.glueframework.scheduling.task.GlueTaskScheduler">
...

com.poscoict.glueframework.scheduling.GlueTaskScheduler 는 다음과 같은 3개 property를 제공한다.

  • ServiceName : 필수. Glue Service 실행을 위한 ServiceName
  • datatMap : 선택. 사용자 data로 GlueService에서 사용됨
  • logger : 선택. Task 실행 history 관리. GlueScheduledTaskLogger 인터페이스 구현체가 된다.

다음은 GlueTaskScheduler의 전체 property를 포함한 예제이다.

<bean id="task" class="com.poscoict.glueframework.scheduling.GlueTaskScheduler">
    <property name="ServiceName" value="task1-service"/>
    <property name="dataMap">
        <map>
            <entry key="deptno" value="10"/>
            <entry key="find" value="1"/>
        </map>
    </property>
    <property name="logger" ref="dbLogging"/>
</bean>

Task 실행 History

GlueTaskScheduler는 logger라는 property는 GlueScheduledTaskLogger 인터페이스 구현체를 필요로 한다. GlueScheduledTaskLogger 의 구현체로 GlueScheduledTaskDBLoggerImpl을 사용할 수 있으며, dataSource property를 필요로 한다.
dataSource의 autoCommit 속성은 true가 될수 있도록 한다. 그리고 dataSource는 task의 실행 History를 위해서만 사용되도록 한다.
다음은 GlueTaskScheduler의 logger참조bean의 설정 예이다.

<bean id="dbLogging" class="com.poscoict.glueframework.scheduling.store.GlueScheduledTaskDBLoggerImpl">
    <property name="dataSource" ref="logging-ds"/>
</bean>
<bean id="logging-ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver "/>
    <property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:XE"/>
    <property name="username" value="scott"/>
    <property name="password" value="tiger"/>
    <property name="defaultAutoCommit" value="true"/>
</bean>

GlueScheduledTaskDBLoggerImpl은 SCHEDULED_TASK_HISTORY 라는 table을 필요로 한다. glue-schedule 모듈에 oracle용 table 생성 script을 포함하고 있다.

그림 : ERD
ERD

다음은 SCHEDULED_TASK_HISTORY 의 컬럼 설명이다.

  • EXECUTION_ID : PRIMARY KEY(자동 증가)
  • SERVICE_NAME : 실행된 Glue Service명
  • START_TIME : Task 실행 시작 시간
  • END_TIME : Task 실행 종료 시간
  • STATUS : 상태(RUNNIG:실행 중 | COMPLETED: 완료 | ERROR:에러 발생)
  • LAST_UPDATED : db logging time
  • ERROR_MESSAGE : 에러 메시지

Quartz Job Scheduler

Glue Framework에서는 오픈소스인 Quartz Job Scheduler를 확장하여 스케줄러 기능을 제공한다. TaskScheduler기반의 Scheduler보다 설정 방법은 복잡하지만 TaskScheduler로 처리가 어려운 복잡한 스케줄링을 처리할 수 있다.

그림 : Glue Scheduler Conceptual Diagram
Glue Scheduler Conceptual Diagram

하나의 Scheduler Bean에 다수의 Trigger를 등록할 수 있다. Trigger에는 Job의 정주기 정보를 정의한다. (예, 매일 오후 7시에 실행한다.) 자세한 정의 방법은 다음 섹션의 개발 예제를 참고하기 바란다.
Diagram에서 보면 알 수 있듯이 Job Detail은 복수 개의 Trigger를 기반으로 작동될 수 있다. Job Class 역시 다수의 Job Detail에서 재사용될 수 있다. 즉, 비즈니스 로직을 구현한 Job Class는 다양한 정주기 패턴에 따라 계속적으로 적용이 가능하다.
(예를 들면, 같은 업무를 수행하는 Job Class를 매일 18시에 수행하도록 하고, 또 매 월 15일에는 9시에 한 번 더 실행이 되도록 구성을 할 수 있다는 것이다.)
실제 비즈니스 로직을 구현하는 Job Class는 GlueQuartzJobBean이며 executeJob 메소드를 구현해야 한다. Job Class이외의 Scheduler Bean, Trigger, JobDetail 정보는 모두 xml 설정 파일에 정의한다.
Conceptual Diagram을 xml로 다음과 같이 표현할 수 있다. 개발자가 실질적으로 코딩하는 부분은 Job Class 하나이지만, 이 클래스가 정상적으로 작동하기 위해서는 xml에 Job의 정주기 정보를 등록해야 한다. (Trigger 정보)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
    <bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="triggers">
            <list>
                <ref local="simpleTrigger1"/>
                <ref local="simpleTrigger2"/>
                <ref local="cronTrigger"/>
            </list>
        </property>
    </bean>
    <bean id="simpleTrigger1" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
        <property name="jobDetail" ref="jobDetail-A"/>
        <property name="repeatInterval" value="10000"/>
    </bean>
    <bean id="simpleTrigger2" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
        <property name="jobDetail" ref="jobDetail-B"/>
        <property name="repeatInterval" value="10000"/>
    </bean>
    <bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
        <property name="jobDetail" ref="jobDetail-A"/>
        <property name="cronExpression" value="0 40 18 * * ?"/>
    </bean>
    <bean id="jobDetail-A" class="org.springframework.scheduling.quartz.JobDetailBean">
        <property name="jobClass" value="com.poscoict.glueframework.scheduling.GlueQuartzJobBean"/>
        <property name="jobDataAsMap">
            <map>
                <entry key="ServiceName" value="job1-service"/>
            </map>
        </property>
    </bean>
    <bean id="jobDetail-B" class="org.springframework.scheduling.quartz.JobDetailBean">
        <property name="jobClass" value="com.poscoict.glueframework.scheduling.GlueQuartzJobBean"/>
        <property name="jobDataAsMap">
            <map>
                <entry key="ServiceName" value="job2-service"/>
            </map>
        </property>
    </bean>
</beans>

Scheduler, Trigger, Jobdetail 을 좀더 자세히 살펴보자.
Scheduler를 정의 하기 위해서는 com.poscoict.glueframework.scheduling.GlueJobScheduler 를 사용하여야 하며, GlueJobScheduler 는 다음과 같은 property를 갖는다.

프로퍼티 설명
startupDelay 스케줄러오브젝트가생성된후서비스를시작할때까지대기하는시간. 보통 10~20초로설정하는것을권장한다. (다른애플리케이션의 standby 상태를고려) 디폴트값은 0. (단위 second)
triggers Job Detail을수행시켜줄 Trigger를리스트형태로등록한다. Trigger의 Bean ID를기재한다.
jobDetails
globalJobListeners

Trigger 는 job의 실행주기를 정의한다. Trigger는job을 실행시켜주는 방아쇠(Trigger) 역할을 한다. Trigger는SimpleTrigger, CronTrigger두 종류가 제공되며 다음과 같은 property를 갖는다.

property 설명 대상
jobDetail Trigger가기동할 JobDetail의 ID를기재한다. SimpleTriger, CronTrigger
startDelay Job이기동된후대기시간을설정한다. 보통지정하지않는다. SimpleTriger, CronTrigger
repeatCount Job 수행횟수를제한한다. 디폴트값은무한대이다. SimpleTriger, CronTrigger
repeatInterval Job 수행간격을정의한다. (단위; milliseconds) SimpleTriger, CronTrigger
cronExpression Cron 표현식을정의한다. CronTrigger

JobDetail 은 실제 비즈니스로직을구현한 Job Class 정보와 Job 수행에 필요한 파라미터 정보를 관리한다.

property 설명
jobClass Full package로기재한다.
jobDataAsMap Job 수행에필요한파라미터를 map 형태로등록한다.
description Job 설명을기재한다.

GlueQuartzJobBean

JobDetail의 참조 bean은 다음과 같은 형태이다.

<bean id="JobB" class="org.springframework.scheduling.quartz.JobDetailBean">
    <property name="jobClass" 
        value="com.poscoict.glueframework.scheduling.GlueQuartzJobBean"/>
    <property name="description" value=" service 파일이용"/>
    <property name="jobDataAsMap">
        <map>
            <entry key="ServiceName" value="JobB-service"/>
            <entry key="job" value="manager"/>
        </map>
    </property>
</bean>

GlueQuartzJobBean 을 이용하는 경우는 특정 Glue service를 실행 시킬 수 있다. jobDataAsMap의 ServiceName에 실행 시킬 Glue service명을 정의 하면 된다.

Glue Sevice 실행 전과 후에 추가적인 로직을 넣고자 한다면, GlueQuartzJobBean을 상속해서 구현할 수 있다. 다음 2개 methde에서 service 수행 전후 필요한 일을 구현 한다.

public class JobD extends GlueQuartzJobBean
{
    protected void beforeExecuteJob(JobExecutionContext context)
    {
        //service 수행전 사전 작업이 있다면 구현한다. 
        //JobDetail의 jobDataAsMap에 담지 못한 Data를 추가로 담는다. 
        context.getJobDetail().getJobDataMap().put("name", value);
    }
    protected void afterExecuteJob(JobExecutionContext context)
    {
        //service 에 대한 수행이 완료된 후 필요한 작업이 있다면 
        //구현한다. 
    }
}

Job 실행 History

Job 실행 history는 scheduler bean의 globalJobListeners property를 이용한다.

<bean id="scheduler" 
      class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="triggers" . . ./>
    <property name="globalJobListeners">
        <list>
            <ref local="dblogging"/>
        </list>
    </property>
</bean>
<bean id="dblogging" 
     class="com.poscoict.glueframework.scheduling.store.GlueScheduledJobInfoLogger">
    <property name="dataSource" ref="logging-ds"/>
</bean>
<bean id="logging-ds" 
      class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver "/>
    <property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:XE"/>
    <property name="username" value="scott"/>
    <property name="password" value="tiger"/>
    <property name="defaultAutoCommit" value="true"/>
</bean>

GlueScheduledJobInfoLogger 를 이용해 Job 수행실적정보를 Database에 저장할 수 있다. GlueScheduledJobInfoLogger는 SCHEDULED_JOB_HISTORY 라는 table을 필요로 한다. glue-schedule 모듈에 oracle용 table 생성 script을 포함하고 있다.
dataSource의 autoCommit 속성은 true가 될수 있도록 한다. 그리고 dataSource는 job의 실행 History를 위해서만 사용되도록 한다.

그림 : ERD
ERD

Scheduler Luanch

Scheduler의 종류에 따라 편의상 Spring Scheduler(spring_scheduler.xml)와 Quartz Scheduler(quartz_scheduler.xml) 로 구분한다.
스케줄러를 기동하는 방법은 크게 세 가지가 있다.

  1. 테스트를 목적으로 Standalone JVM에서 기동하는 경우
  2. 서블릿 컨테이너에서 기동하는 경우
  3. Glue Http Server에서 기동하는 경우, 즉 서블릿 컨테이너 외부에서 Scheduler를 실행하는 경우

테스트 목적이면 1)번 방법을 사용하고, 그렇지 않다면 2)번, 3)번 중에 선택하여 스케줄러를 기동한다.
보통 CPU, Memory, Application Performance문제의 우려가 있기 때문에 3)번 방법을 권장한다.

Standalone JVM

Main 메소드를 가진 클래스에서 Scheduler 오브젝트를 생성하면 스케줄러가 자동으로 기동된다.
테스트하는 경우에만 사용할 것을 권장한다.
Spring Scheduler 일 경우는 다음과 같다.

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Run_SpringScheduler
{
    public static void main( String[] args )
    {
        ApplicationContext context 
            = new ClassPathXmlApplicationContext( "spring-scheduler.xml" );
    }
}

Quartz Scheduler의 경우는 다음과 같다.

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Run_SpringScheduler
{
    public static void main( String[] args )
    {
        ApplicationContext context 
            = new ClassPathXmlApplicationContext( "quartz_scheduler.xml" );
    }
}

서블릿 컨테이너

스케줄러를 위한 별도의 물리적인 서버가 가용하지 않을 때 사용하는 방법이다.
관련 Bean을 인스턴스화하는 Servlet Context Listener를 web.xml에 등록한다. 서블릿 켄테이너가 시작하면서 스케줄러가 자동적으로 Start 될 것이다.
다음은 spring scheduler를 사용할 경우이다.

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/spring_scheduler.xml</param-value>
</context-param>
<listener>
    <listener-class>
        org.springframework.web.context.ContextLoaderListener
    </listener-class>
</listener>

다음은 quartz job scheduler를 사용할 경우이다.

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/quartz_scheduler.xml</param-value>
</context-param>
<listener>
    <listener-class>
        org.springframework.web.context.ContextLoaderListener
    </listener-class>
</listener>

Glue Scheduler Server

mina 기반의 스케줄러만을 위한 별도의 Server이다. 서버가 기동되면, Quartz Job Scheduler에 한해 remote에서 요청되는 Job 실행 이벤트까지 처리가능하다.
java 사용법은 다음과 같다.

그림 : java Usage
java Usage

그리고 Glue에서는 Scheduler실행시 java -jar jarfile 을 사용할 것이다. 실행시 args에 따라 scheduler를 지정할 수 있다.
scheduler관련 options에는 다음과 같은 것이 있다. 그외 java option을 추가할 수 있다.

  • glue.scheduler.server.port : scheduler server에서 사용할 port. 8805 를 default로 사용함.
  • glue.scheduler.server.address : scheduler server에 할당된 ip

args는 2개를 필요로 하며 다음과 같다.

  • scheduler type : spring 또는 quartz. Default는 ‘quartz’ 임
  • scheduler xml file명 : xml file 명. ‘Default는 quartz_scheduler.xml’임

다음은 spring scheduler 일 경우의 실행방법이다.

java -jar application.jar spring spring_scheduler.xml

다음은 quartz job scheduler일 경우의 실행방법이다.

java -jar application.jar spring quartz_scheduler.xml

다음은 java option이 사용된 실행방법이다.

java -Dglue.scheduler.server.port=8805 -DCONFIG_PATH=C:/ application.jar spring quartz_scheduler.xml

Ant 설정

build.properties 를 다음과 같다. 빌드를 위한 기본 정보를 다음과 같이 설정한다.

JAR_NAME=application
build.dir=./build
class.dir=../classes
lib.dir=./build/lib
GlueSDK.dir=C://eclipse/users/GlueSDK

build.xml은 다음과 같다. jar파일 생성시 MENIFEST.MF 파일에 Main-Class와 ClassPath 가 포함되도록 작성한다.

<project name="GlueSample" default="user-application">
    <property file="build.properties"/>
    <target name="makeBuilDir">
        <mkdir dir="${build.dir}"/>
        <mkdir dir="${build.dir}/lib"/>
    </target>
    <path id="class.path">
        <fileset dir="${GlueSDK.dir}/lib/gluelib" >
            <include name="*.jar"/>
        </fileset>
        <fileset dir="${GlueSDK.dir}/lib/gluestd" >
            <include name="*.jar"/>
        </fileset>
    </path>
    <pathconvert property="class-path" pathsep=" " dirsep="\">
        <path refid="class.path"></path>
        <map from="${GlueSDK.dir}/lib/gluestd" to="lib"/>
        <map from="${GlueSDK.dir}/lib/gluelib" to="lib"/>
    </pathconvert>
    <target name="create-manifest" depends="">
        <tstamp>
            <format property="TODAY_KO" 
                    pattern="yyyyMMdd.HHmm" 
                    timezone="GMT+9" locale="ko"/>
        </tstamp>
        <manifest file="${build.dir}/MANIFEST.MF">
            <attribute name="Main-Class" 
        value="com.poscoict.glueframework.scheduling.server.GlueSchedulerHttpServer"/>
            <attribute name="Class-Path" value="${class-path}" />
        </manifest>
    </target>
    <target name="user-application" depends="makeBuilDir,create-manifest">
        <jar destfile="${build.dir}/${JAR_NAME}.jar" 
             manifest="${build.dir}/MANIFEST.MF" compress="false">
            <fileset dir="${class.dir}">
                <patternset>
                    <include name="**/*.*"/>
                </patternset>
            </fileset>
        </jar>
        <copy todir="${build.dir}/lib">
            <fileset dir="${GlueSDK.dir}/lib/gluelib">
                <include name="*.jar"/>
            </fileset>
            <fileset dir="${GlueSDK.dir}/lib/gluestd">
                <include name="*.jar"/>
            </fileset>
        </copy>
    </target>
</project>