첫번째, 스프링 배치보다 간편한 스프링 Quartz


2편에 걸쳐서 저의 경험을 공유하고자 합니다.

간략하게 스프링 Quartz를 설명하겠다.
스프링 프레임워크은 스케줄링을 지원하는 통합 클래스를 지원한다.

JDK의 Timer와 Quartz 스케줄러를 지원한다.
각각 두 개의 스케줄러들은 각각의 FactoryBean을 제공하고 있다.

현업에서 많이 사용하고 있는 리눅스 Crontab처럼 Quartz 스케줄러를 많이
사용하고 있다.

지상 과제!!!!
WAS 중지 없이 쿼츠(Quartz)의 표현식(Expression) 변경과
돌아가고 있는 잡(Job)의 시작, 중지를 해야 하는 미션이 생겼다.

구글링을 해보자!
스프링 배치가 나온다.

스프링 배치는 너무 테이블이 많이 생성되고, 클래스들도 너무 많다. (1)
JobStore도 있는거 같다. (2)

:: 변명 (1) ::
맞다! 사실 내가 사용법을 잘 몰라서 이렇게 변명하는거다.
나는 쿼츠(Quartz) 스케줄러의 일부분만 사용하고 싶을 뿐이다 ㅋㅋㅋㅋ

:: 변명 (2) ::
잘 모르니까~ ㅋㅋㅋㅋㅋ
뭔가 복잡해 보인다. ㅠㅜ

::: 혼자 현실과 타협했다. :::
내가 원하는 기능은 이미 스프링 배치에 존재하고 있다.
그러나, 내가 쓰기엔 너무 과하다. ㅠㅜ
이미 위에 혼자만의 변명으로 결론을 대신하겠다 ㅋㅋㅋ

유레카!!!
구글링하니까 쿼츠(Quartz)를 잘 확장하면 내가 원하는 기능을 만들수 있겠다.
이제 본론으로 들어가자.

스프링 쿼츠는 보통 아래와 같이 사용한다.

<bean id="oneService" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="oneJobService" />   
    <property name="targetMethod" value="execute" />
    <property name="concurrent" value="false" />
</bean>
<bean id="twoService" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="twoJobService" />   
    <property name="targetMethod" value="execute" />
    <property name="concurrent" value="false" />
</bean>
<bean id="threeService" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="threeJobService" />   
    <property name="targetMethod" value="execute" />
    <property name="concurrent" value="false" />
</bean>

<bean id="oneTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
    <property name="jobDetail" ref="oneService" />
    <property name="cronExpression" value="0 0 1 * * ?" />
</bean>
<bean id="twoTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
    <property name="jobDetail" ref="twoService" />
    <property name="cronExpression" value="0 0 2 * * ?" />
</bean>
<bean id="threeTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
    <property name="jobDetail" ref="threeService" />
    <property name="cronExpression" value="0 0 3 * * ?" />
</bean>

<bean id="schedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="triggers">
            <list>
                <ref bean="oneTrigger" />
                <ref bean="twoTrigger" />
                <ref bean="threeTrigger" />
            </list>
        </property>
    </bean>
</bean>

한번 만들어진 배치(Batch)는 대개 표현식이나 잡(Job)을 시작/중지 할 일이 별로 없었는데
이번에는 가끔 변경이 일어난다고 하니까~

위의 방식대로 잘 안될 것 같아서 스프링 쿼츠(Quartz)를 확장해 보자.

나는 복잡하게 하는거 싫어한다.
아주 간단한게 좋은거니까~ 좋은게 좋잖아요!

스프링 구동시 ContextLoaderListener가 스프링 환경설정 파일을 로딩할 것이다.
아님 말고;;;;
분명 위의 배치 관련 환경설정 파일도 로딩할테니~
쪼물딱~ 쪼물딱~ 하다 보면 뭔가 되겠지~ ??

org.springframework.scheduling.quartz.SchedulerFactoryBean
일단 이 녀석을 살펴보자.

이름(SchedulerFactoryBean)부터 멋진 녀석이다.
이 녀석이 하는 일은 JobDetail들과 Trigger들을 관리하며, 해당 Trigger에 연관된 Job을 수행시키는
엄청난 역활을 하는 놈이다. (짜식~ 맘에 들어!)

위 XML에 보면 Trigger 년놈들(ㅠㅜ)을 등록시켜주고 있다.

우리는 동적으로 트리거(Trigger)들을 등록/수정/삭제를 해야 한다.
XML에 기술되어 있는 트리거 리스트는 변하지 않는다.

DB로 저 트리거를 관리해 보자.

별거 없다.
뭔가 있어 보일려면 설명을 해야 하니까~

SchedulerFactoryBean 클래스를 상속해서 새로운 나만의 SchedulerFactoryBean 클래스를 만들어보자.
이름하여 “MySchedulerFactoryBean”이다. -_-;;

내꺼 아닌 니꺼 같은 내꺼 같은 SchedulerFactoryBean (미안하다~~~)

@Override
public void setTriggers(Trigger[] triggers) {
    List triggerList = getDbTriggers(triggers);    // DB에서 트리거 리스트를 조회한다.
    Trigger[] newTriggers = triggerList.toArray(new Trigger[triggerList.size()]);
    
    super.setTriggers(newTriggers);
}

private List getDbTriggers(Trigger[] triggers) {
    List newTriggers = new ArrayList();
    
    // 여기서 DB 테이블 조회해서 트리거 가져오면 될 것이다. (아마도? ㅋㅋ)
    // 트리거 사용 유뮤(Y/N) 뭐 이런거 있겠죠?
    // 이 부분은 알아서 구현하세요 ㅋㅋ
    List triggerList = batchDAO.selectTriggerList();

    for (Trigger trigger : triggers) {
        for (TriggerVO trigger : triggerList) {
            // 트리거에 사용 유무가 난 필요해서 서로 트리거명을 비교해서 Y/N 비교해서
            // 넣을거 넣고, 안 넣을거 안 넣었다.
            
            // 직접 트리거를 만들어야 한다면, 아마 CronTrigger라는 클래스로 잘 만지면(?) 될 것이다.
            // 난 안해봤지롱~
            newTriggers.add(trigger);
        }
    }
}

이렇게 하면 MySchedulerFactoryBean 완성되었다.
짜잔~

<bean id="schedulerFactory" class="kr.co.jaeger.MySchedulerFactoryBean">
        <property name="triggers">
            <list>
                <ref bean="oneTrigger" />
                <ref bean="twoTrigger" />
                <ref bean="threeTrigger" />
            </list>
        </property>
    </bean>
</bean>

 
위 MySchedulerFactoryBean에서 DB의 트리거 사용 유무에 XML에 정의된 트리거가
스프링 구동시에 선택할 수 있을 것이다.

org.springframework.scheduling.quartz.CronTriggerBean은 뭐하는 녀석일까? 곰곰~
어떤 Job을 어떤 조건(Expression)을 결정하고 JobDetail를 이용하여 스케줄링을 수행하는 녀석인거 같다.

뭐~ 이녀석도 확장해 보자.

CronTriggerBean 상속 받아서 새로운 이름을 지어주자~
역시나 “MyCronTriggerBean”으로 이름을 지었다.
 

@Override
public void setCronExpression(String cronExpression) throws ParseException {
    // 해당 잡 이름을 가져온다.
    String jobDetailName = super.getJobDetail().getName();

    // DB에서 해당 배치 조회
    CronExpression cronExpr = getDbCronExpression(jobDetailName);

    Log.info("[Cron Expression] jobDetailName : " + jobDetailName);
    if (cronExpr == null) {
        Log.info("[Cron Expression] XML : " + cronExpression);

        // 최초 배치 추가시 DB 정보가 없기 때문에 XML에 저장된 최초 크론 표현식으로 가동
        super.setCronExpression(cronExpression);
    } else {
        Log.info("[Cron Expression] DB : " + cronExpr.getCronExpression());

        // 배치 정보가 DB에 있다면 DB에 저장된 크론 표현식으로 가동
        super.setCronExpression(cronExpr.getCronExpression());
    }
}

private CronExpression getDbCronExpression(String jobDetailName) {
    // 파라미터 세팅
    JobDetailVO paramVO = new JobDetailVO();
    paramVO.setBatchName(jobDetailName);

    JobDetailVO resultVO = null;
    CronExpression cronExpr = null;

    // DB 조회
    resultVO = batchDAO.selectJobDetailList(paramVO);
    if (resultVO != null) {
        // 크론 표현식 세팅
        cronExpr = new CronExpression(resultVO.getExpression());
    }

    return cronExpr;
}

 
트리거가 여러 개니까 크론 표현식(Cron Expression)도 여러 번 수행될 것이다.

위와 같이 상속하여 확장했다면 아래와 같을 것이다.

<bean id="oneService" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="oneJobService" />   
    <property name="targetMethod" value="execute" />
    <property name="concurrent" value="false" />
</bean>
<bean id="twoService" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="twoJobService" />   
    <property name="targetMethod" value="execute" />
    <property name="concurrent" value="false" />
</bean>
<bean id="threeService" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="threeJobService" />   
    <property name="targetMethod" value="execute" />
    <property name="concurrent" value="false" />
</bean>

<!--
    DB에 저장된 잡(Job) 이름으로 Cron 표현식을 읽어와 변경해 준다.
-->
<bean id="oneTrigger" class="kr.co.jaeger.MyCronTriggerBean">
    <property name="jobDetail" ref="oneService" />
    <property name="cronExpression" value="0 0 1 * * ?" />
</bean>
<bean id="twoTrigger" class="kr.co.jaeger.MyCronTriggerBean">
    <property name="jobDetail" ref="twoService" />
    <property name="cronExpression" value="0 0 2 * * ?" />
</bean>
<bean id="threeTrigger" class="kr.co.jaeger.MyCronTriggerBean">
    <property name="jobDetail" ref="threeService" />
    <property name="cronExpression" value="0 0 3 * * ?" />
</bean>

<!--
    DB에 저장된 트리거 목록을 가져와 사용 유무에 따라 트리거를 등록해준다.
-->
<bean id="schedulerFactory" class="kr.co.jaeger.MySchedulerFactoryBean">
        <property name="triggers">
            <list>
                <ref bean="oneTrigger" />
                <ref bean="twoTrigger" />
                <ref bean="threeTrigger" />
            </list>
        </property>
    </bean>
</bean>

 
이번 편은 여기서 마치기로 한다.

다음에는 WAS 구동 중에 Job의 시작/중지와 크론 표현식(Cron Expression)을 수정하는 것을 다루어 보겠다.
언제 올릴지 알수 없다. (이거 쓰고 나니까 진이 빠진다 ㅋㅋㅋㅋㅋ)

미안하다 ㅠㅜ