'2008/11'에 해당되는 글 2건

  1. 2008.11.06 자바 애플리케이션 스케쥴러! Quartz

어떤 프로젝트에서 정확히 정해진 시간이나 일정한 시간 간격으로 실행되는 작업이 필요할 수 있다. 이 글에서 우리는 자바 개발자가 표준 Java Timer API를 사용하여 어떻게 이러한 요구사항을 구현할 수 있는지 살펴볼 것이다. 그리고, Java Timer API가 제공하는 기본적인 스케쥴링 시스템 외에 추가적인 기능을 필요로 하는 사람을 위해 오픈 소스 라이브러리인 Quartz에 대해 살펴볼 것이다.

먼저 스케쥴링 작업을 필요로 할 때, 이러한 상황을 인식하는데 도움을 줄 수 있는 일반적인 유스케이스에 대해서 몇 가지 살펴보자. 그리고 나서 우리는 여러분의 기능적 요구에 가장 알맞는 최선의 해결책을 찾아 볼 것이다.

대부분의 비지니스 애플리케이션은 유저들이 필요로 하는 보고서와 통계를 가지고 있다. 시스템에 투자하는 사람들의 일반적인 목적은 대량의 데이터를 수집하고 그것을 미래의 비지니스 계획을 세우는데 도움이 되는 방식으로 보는 것이기 때문에, 이러한 일을 가능하게 해주는 보고서가 없는 시스템은 상상하기 힘들다. 이러한 보고서를 생성하는데 있어서의 문제점은 처리해야할 데이터의 양이 대량이기 때문에, 일반적으로 데이터베이스 시스템에 큰 부하가 걸린다는 것이다. 이러한 부하는 시스템이 보고서를 생성하는 동안, 전체적인 애플리케이션의 성능을 떨어뜨리고 단지 데이터 수집을 위하여 시스템을 사용하는 유저에게도 영향을 끼친다. 또한 유저입장에서 생각한다면, 생성하는데 10분이 걸리는 보고서는 좋은 응답시간의 예가 아니다.

우리는 먼저 실시간으로 실행될 필요가 없는, 캐쉬될 수 있는 종류의 보고서에 대해 살펴볼 것이다. 기쁘게도 대부분의 보고서가 이러한 부류에 들어간다. -- 작년 6월의 어떤 제품 판매에 관한 통계, 또는 1월의 회사 소득 등. 이러한 캐쉬가 가능한 보고서는 간단한 방법으로 해결할 수 있다 : 시스템이 한가할 때 또는 데이터베이스 시스템의 부하가 최소일 때, 보고서를 생성하도록 스케쥴링하면 된다. 여러분이 많은 사무실(모두 같은 시간대에 있는)을 가지고 있는 지역 책 판매자를 위한 애플리케이션을 만든다고 해보자. 여러분은 주당 소득에 대한 보고서(아마도 대량의)를 생성할 필요가 있다. 매주 일요일 밤과 같이 시스템이 사용되지 않는 시간에 데이터베이스에 캐쉬해서 이러한 작업을 스케쥴링 할 수 있다. 이러한 방식으로 구현하면, 판매 담당자는 여러분의 애플리케이션에서 성능상의 문제점을 찾지 못할 것이다. 그리고 회사 관리부는 필요한 모든 데이터를 빠른 시간에 구할 수 있을 것이다.

다음으로 다룰 두번째 예제는 계정 사용기한 만료와 같은 일련의 공지(notification)를 애플리케이션 유저에게 보내는 작업에 관한 것이다. 이것은 유저 데이터에서 날짜 필드를 사용하여 유저의 조건을 검사하는 쓰레드를 생성함으로써 행해질 수 있다. 그러나 이러한 경우 스케쥴러를 사용하는 것이 명백하게 더욱 우아한 해결책이고, 전체적인 시스템 아키텍쳐(아키텍쳐는 중요하다. 그렇지 않은가?)의 측면에서도 더 좋다. 복잡한 시스템에서 여러분은 이러한 종류의 공지를 많이 가지고 있을 것이고, 이외의 다른 많은 경우에도 또한 스케쥴러 시스템이 필요할 것이다. 따라서, 스케쥴링 작업을 필요로 하는 각각의 경우에 대해 해결책을 구현하는 것은 시스템을 변경하고 유지보수하는 것을 더욱 어렵게 할 것이다. 일일이 스케쥴링 작업을 구현하는 대신 여러분은 애플리케이션의 모든 스케쥴링을 담당하는 API를 사용해야만 한다. 이것이 이 글의 나머지 부분에서 다루는 주제이다.

간단한 해결책

자바 애플리케이션에 기본적인 스케쥴러를 구현하기 위해 여러분은 어떤 외부 라이브러리도 필요 없다. J2SE 1.3 이후로 자바는 이러한 목적으로 사용될 수 있는 java.util.Timer, java.util.TimerTask 두 개의 클래스를 포함하고 있다. 먼저 이 API로 가능한 모든 것을 설명해주는 간단한 예제를 만들어보자.

package net.nighttale.scheduling;

import java.util.Calendar;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class ReportGenerator extends TimerTask {

  public void run() {
    System.out.println("Generating report");
    //TODO generate report
  }

}

class MainApplication {

  public static void main(String[] args) {
    Timer timer  new Timer();
    Calendar date = Calendar.getInstance();
    date.set(
      Calendar.DAY_OF_WEEK,
      Calendar.SUNDAY
    );
    date.set(Calendar.HOUR, 0);
    date.set(Calendar.MINUTE, 0);
    date.set(Calendar.SECOND, 0);
    date.set(Calendar.MILLISECOND, 0);
    // Schedule to run every Sunday in midnight
    timer.schedule(
      new ReportGenerator(),
      date.getTime(),
      1000 * 60 * 60 * 24 * 7
    );
  }
}

이 글의 모든 예제 코드는 글 말미의 링크에서 다운로드 받을 수 있다.

위의 코드는 서두에서 언급한, 시스템이 한가한 시간(예제의 경우 일요일 밤)에 보고서를 생성하도록 스케쥴링하는 예제를 구현하고 있다.

먼저 우리는 실제로 스케쥴링된 작업을 수행하는 "worker" 클래스를 구현해야 한다. 우리의 예제에서 이것은 ReportGenerator 이다. 이 클래스는 java.lang.Runnable을 구현하는 java.util.TimerTask 클래스를 상속받아야 한다. 그리고 나머지 할 일은 보고서를 생성하는 코드를 run() 메소드 안에 오버라이드하는 것 밖에 없다.

우리는 이 객체의 실행을 Timer 클래스의 스케쥴링 메소드 중의 하나를 이용해서 스케쥴링한다. 예제의 경우 최초 실행 날짜와 밀리세컨드 단위의 실행 주기를 인자로 받아들이는 schedule() 메소드를 사용한다.(왜냐하면 우리는 이 보고서를 매주 생성할 것이기 때문이다.)

스케쥴링 기능을 사용할 때, 우리는 스케쥴링 API가 제공하는 리얼타임에 대한 보증을 알아야만 한다. 불행하게도 자바의 특성과 다양한 플랫폼에서의 구현때문에, 각각의 JVM에서의 쓰레드 스케쥴링의 구현은 일치하지 않는다. 그러므로, Timer는 우리의 스케쥴링 작업이 정확한 규정된 시간에 실행될 것이라고 보증할 수 없다. 우리의 스케쥴링 작업은 Runnable 객체로 구현되어 있고 때때로 일정 시간동안 sleep 상태가 된다. 그러면 Timer는 규정된 순간에 이들을 깨운다. 그러나 정확한 실행 시간은 JVM의 스케쥴링 정책과 현재 얼마나 많은 쓰레드가 프로세서를 기다리고 있느냐에 따라 달라진다. 우리의 스케쥴링 작업 실행을 지연시킬 수 있는 두 가지 일반적인 경우가 있다. 첫째, 많은 수의 쓰레드가 실행되기를 기다리고 있는 경우이다; 둘째, 가비지 콜렉션의 활동에 의해 지연되는 경우가 있다. 이러한 영향들은 다른 JVM에서 스케쥴러를 실행 또는 가비지 콜렉터의 옵션 튜닝과 같은 여러가지 기법을 사용함으로써 최소화될 수 있다. 그러나 그것은 이 글의 주제를 벗어나는 것이다.

다시 본론으로 돌아오자. Timer 클래스에는 두 개의 다른 스케쥴링 메소드 그룹이 있다: 고정된 딜레이로 스케쥴링하는 schedule() 메소드와 고정된 비율로 스케쥴링하는 scheduleFixedRate() 메소드가 그것이다. 첫번째 그룹의 메소드들을 사용할 때, 각 작업 실행의 딜레이는 다음 작업 실행으로 전달될 것이다. 후자 그룹의 경우 딜레이를 최소화하면서 모든 연속된 작업 실행은 최초 작업 실행의 시간에 맞춰 스케쥴링 될 것이다. 어떤 메소드를 사용하느냐는 여러분의 시스템에 어떤 파라미터가 더 중요하느냐에 달려 있다.

매우 중요한 것이 한 가지 더 있다: 각 Timer 객체는 쓰레드를 백그라운드로 시작한다. 이러한 방식은 J2EE 애플리케이션 서버와 같은 환경에서는 바람직하지 않을 것이다. 왜냐하면 이러한 쓰레드들이 컨테이너 영역 내에 있지 않기 때문이다.

평범한 것을 넘어서

지금까지 애플리케이션에서 어떻게 스케쥴링을 하는지 살펴 보았고, 이것은 간단한 요구사항에서는 충분하다. 그러나 고급 유저와 복잡한 요구사항을 위해서는 유용한 스케쥴링을 지원하기 위해 더 많은 기능들을 필요로 한다. 이러한 경우 두 가지 일반적인 해결책이 있다. 첫번째는 자신이 필요로 하는 기능을 가지고 있는 스케쥴러를 만드는 것이다; 두번째는 필요로 하는 요구사항을 충족하는 프로젝트를 찾아내는 것이다. 시간과 자원을 절약할 수 있고 다른 누군가의 노력을 중복해야 할 필요가 없기 때문에, 두번째 해결책이 대부분의 경우에 있어서 더욱 적합할 것이다.

이러한 요구사항은 우리를 단순한 Timer API 이상의 훨씬 뛰어난 장점들을 가진 오픈 소스 작업 스케쥴링 시스템인 Quartz 로 유도한다.

Quartz의 첫번째 장점은 영속성이다. 만약 여러분의 작업이 앞서의 예제와 같이 "정적"이라면 영속성 지원은 필요 없을 것이다. 그러나 종종 어떤 조건이 충족됐을 때 "동적"으로 수행되는 작업을 필요로 할 때도 있다. 그리고 이러한 작업들이 시스템 재시작(또는 시스템 다운) 사이에도 실행되야 할 때가 있다. Quartz는 비 영속성과 영속성 작업 모두를 제공한다. 영속성 작업의 상태는 데이터베이스에 저장될 것이다. 따라서 이러한 작업들이 실행되지 않는 경우가 없을 거라고 확신할 수 있다. 영속성 작업은 시스템에 추가적인 성능 감소를 유발하기 때문에 주의깊게 사용해야만 한다.

Timer API는 원하는 실행시간을 단순하게 설정할 수 있는 메소드가 부족하다. 위 예제에서 본 대로, 실행시간을 설정하기 위해서 할 수 있는 것은 시작일자와 반복주기를 설정하는 것 밖에 없다. 분명히, 유닉스의 cron을 사용해 본 사람들은 스케쥴러를 그와 유사하게 설정할 수 있기를 바랄 것이다. Quartz는 원하는 시간, 날짜를 유연한 방법으로 설정할 수 있게 해주는 org.quartz.CronTrigger를 정의하고 있다.

개발자는 자주 한 가지 이상의 기능을 필요로 한다: 작업의 이름에 의한 작업의 관리와 조직화. Quartz에서 당신은 이름이나 그룹에 의해 원하는 작업을 찾아낼 수 있다. 이것은 스케쥴된 작업이 많은 환경에서 큰 도움이 될 것이다.

이제 보고서 생성 예제를 Quartz를 사용하여 구현하고 라이브러리의 기본적인 기능들에 대해 설명할 것이다.

package net.nighttale.scheduling;

import org.quartz.*;

public class QuartzReport implements Job {

  public void execute(JobExecutionContext cntxt)
    throws JobExecutionException {
      System.out.println(
        "Generating report - " +
cntxt.getJobDetail().getJobDataMap().get("type")
      );
      //TODO Generate report
  }

  public static void main(String[] args) {
    try {
      SchedulerFactory schedFact 
       new org.quartz.impl.StdSchedulerFactory();
      Scheduler sched  schedFact.getScheduler();
      sched.start();
      JobDetail jobDetail 
        new JobDetail(
          "Income Report",
          "Report Generation",
          QuartzReport.class
        );
      jobDetail.getJobDataMap().put(
                                "type",
                                "FULL"
                               );
      CronTrigger trigger  new CronTrigger(
        "Income Report",
        "Report Generation"
      );
      trigger.setCronExpression(
        "0 0 12 ? * SUN"
      );
      sched.scheduleJob(jobDetail, trigger);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

Quartz는 Job, Trigger라는 두 개의 기본 추상 계층을 정의한다. Job은 실제 실행되는 작업의 추상 계층이고, Trigger는 언제 작업이 실행되어야 하는지를 나타내는 추상 계층이다.

Job은 인터페이스이다. 그래서 우리가 해야할 일은 클래스가 org.quartz.Job(또는 나중에 살펴보게 될 org.quartz.StatefulJob) 인터페이스를 구현하도록 하고 execute() 메소드를 오버라이드하는 것 뿐이다. 예제에서 java.util.Map의 변형된 구현인 jobDataMap 어트리뷰트를 통해 어떻게 Job에게 파라미터를 전달할 수 있는지 살펴 봤다. 상태가 있는 작업, 또는 비 상태 작업 중 어떤 것을 구현하느냐 결정하는 것은 스케쥴링 작업의 실행동안 이러한 파라미터들을 변경하기를 원하느냐 아니냐를 결정하는 문제이다. Job 인터페이스를 구현한다면 모든 파라미터들은 작업이 최초로 스케쥴링 되는 순간에 저장된다. 그리고 이후의 변경은 모두 버려진다. execute() 메소드 내에서 StatefulJob의 파라미터를 변경한다면, 작업이 다음에 새로 스케쥴링 될 때 이 새로운 값이 전달될 것이다. 고려해야할 한 가지 중요한 사항은 StatefulJob 인터페이스를 구현한 작업들은 실행되는 동안 인자들이 변할 수 있기 때문에 동시에 실행될 수 없다는 것이다.

Trigger에는 두 가지의 기본적인 Trigger가 있다: SimpleTrigger 와 CronTrigger. SimpleTrigger는 기본적으로 Timer API가 제공하는 것과 같은 기능을 제공한다. 작업이 시작된 이후에 정해진 간격으로 반복해서 실행되는 경우, SimpleTrigger를 써야 한다. SimpleTrigger는 시작일, 종료일, 반복횟수, 실행 주기를 정의할 수 있다.

위의 예제에서는 더욱 사실적인 바탕에서 작업을 스케쥴링할 수 있는 유연성때문에 CronTrigger를 사용했다. CrinTrigger 사용함으로써 "매주 평일 오후 7시" 또는 "토요일과 일요일 매 5분마다"와 같은 스케쥴링 작업을 할 수 있다. 더 이상 cron에 관해 설명하지는 않을 것이다. cron에 관한 세부사항은 CronTrigger의 Javadoc을 참조하기 바란다.

위의 예제를 실행하기 위해 클래스패쓰에 기본적인 Quartz의 설정을 하는 quartz.properties 파일을 필요로 한다. 만약 파일이름을 다르게 사용하기를 원한다면, 파일이름을 StdScheduleFactory 생성자에 인자로 전달해야만 한다. 아래에 최소한의 프로퍼티들만 설정한 파일의 예제가 있다:

#
# Configure Main Scheduler Properties 
#

org.quartz.scheduler.instanceName = TestScheduler
org.quartz.scheduler.instanceId = one

#
# Configure ThreadPool 
#

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount =  5
org.quartz.threadPool.threadPriority = 4

#
# Configure JobStore 
#

org.quartz.jobStore.misfireThreshold = 5000

org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

표준 Timer API에 비해 Quartz가 가진 또 다른 장점은 쓰레드 풀의 사용이다. Quartz는 작업 실행을 위한 쓰레드를 얻기 위해 쓰레드 풀을 사용한다. 쓰레드 풀의 크기는 동시에 실행될 수 있는 작업의 수에 영향을 미친다. 실행해야할 작업이 있지만 쓰레드 풀에 남아 있는 쓰레드가 없다면, 작업은 여분의 쓰레드가 생길 때까지 sleep 상태가 될 것이다. 시스템에서 얼마나 많은 쓰레드를 사용할 지는 매우 결정하기 어려운 문제이고, 실험적으로 결정하는 것이 가장 좋다. 쓰레드 풀 크기의 기본 값은 5이고 수천개의 작업을 다루지 않는다면 이것은 충분할 것이다. Quartz 자체에서 구현한 쓰레드 풀이 있지만, 다른 쓰레드 풀의 구현이 있다면 그것을 사용하는데 제약을 받지는 않는다.

이제 JobStore에 관해 살펴 보자.JobStore는 Job과 Trigger에 관한 모든 데이터를 보존한다. 따라서 작업에 영속성을 부여할 것인지 아닌지 결정하는 것은 어떤 JobStore를 사용하느냐에 달려 있다. 예제에서 우리는 org.quartz.simpl.RAMJobStore를 사용했다. 이것은 모든 데이터는 메모리에 저장될 것이고 그러므로 비 영속성이라는 것을 의미한다. 따라서 애플리케이션이 다운되면 스케쥴링 작업에 관한 모든 데이터는 사라질 것이다. 어떤 상황에서 이것은 바람직한 방식이다. 그러나 데이터를 보존하고 싶다면 애플리케이션이 org.quartz.simpl.JDBCJobStoreTX(또는 org.quartz.simpl.JDBCJobStoreCMP)를 사용하도록 설정해야 한다.JDBCJobStoreTX는 좀 더 많은 설정 파라미터를 필요로 하고 그것은 아래 예제에서 설명할 것이다.

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.tablePrefix = QRTZ_

#
# Configure Datasources 
#

org.quartz.dataSource.myDS.driver = org.postgresql.Driver
org.quartz.dataSource.myDS.URL = jdbc:postgresql:dev
org.quartz.dataSource.myDS.user = dejanb
org.quartz.dataSource.myDS.password =
org.quartz.dataSource.myDS.maxConnections  5

Quartz와 관계형 데이터베이스를 성공적으로 사용하기 위하여 먼저 Quartz가 필요로 하는 테이블을 생성할 필요가 있다. 적절한 JDBC 드라이버를 가지고 있다면 어떤 데이터베이스 서버도 사용 가능하다. docs/dbTables 폴더에서 필요한 테이블을 생성하는 초기화 스크립트를 찾을 수 있다.

테이블을 생성한 후에 표준 SQL 쿼리를 특정 RDBMS의 SQL 문법에 맞게 변경해주는 위임(delegate) 클래스를 선언해야 한다. 예제에서 우리는 PostgreSQL을 데이터베이스 서버로 선택했고 따라서 org.quartz.impl.jdbcjobstore.PostgreSQLDelegate 클래스를 위임 클래스로 설정했다. 당신이 사용하는 특정 데이터베이스 서버를 위해 어떤 위임 클래스를 사용해야 하는지에 관한 정보는 Quartz 문서를 참조하기 바란다.

tablePrefix 파라미터는 데이터베이스에서 Quartz 테이블이 사용할 접두어를 정의한다. 디폴트는 QRTZ_ 이다. 이런 방식으로 데이터베이스의 나머지 테이블과 구분할 수 있다.

사용하는 매 JDBC store마다 어떤 datasource를 사용할 것인지 정의할 필요가 있다. 이것은 일반적인 JDBC의 설정이기 때문에 여기서 더 이상 설명하지는 않을 것이다.

Quartz의 뛰어난 점은 이러한 설정을 변경한 후에 보고서 생성 예제의 코드를 단 한줄도 변경하지 않고, 데이터를 데이터베이스에 저장할 것이라는 것이다.

Advanced Quartz

지금까지 프로젝트에 Quartz를 사용하기 위한 좋은 기초가 될 수 있는 기본적인 것에 대해 살펴 보았다. 이외에도 Quartz 라이브러리는 당신의 수고를 크게 덜어줄 수 있는 뛰어난 아키텍쳐를 가지고 있다.

Quartz는 기본적으로 제공하는 것 외에 애플리케이션의 문제를 해결하는데 도움이 되는 뛰어난 아키텍쳐를 가지고 있다. 그 중 중요한 기능 중의 하나는 listener 이다: 이것은 시스템에 어떤 이벤트가 발생할 때 호출된다. 세 가지 종류의 리스너가 있다: JobListener, TriggerListener, SchedulerListener 이다.

리스너는 시스템에 무언가 이상이 생겨서 이에 대한 공지나 알림 기능을 원할 때 특히 유용하다. 예를 들어 보고서 생성 중에 에러가 발생하면 개발 팀에게 이를 알리는 우아한 방법은 E-mail이나 SMS를 보내는 JobListener 를 만드는 것이다.

JobListener 는 더 흥미로운 기능을 제공한다. 시스템 자원의 가용성에 크게 의존하는 일(그다지 안정적이지 못한 네트워크와 같은)을 다루는 작업을 상상해보라. 이러한 경우 작업이 실행될 때 자원이 사용 불가능하다면 이를 재실행 시키는 리스너를 만들 수 있다.

Quartz는 또한 Trigger 가 잘못 실행되거나 스케쥴러가 다운되서 실행되지 않았을 때의 상황을 다룰 수 있다. Trigger 클래스의 setMisfireInstruction() 메소드를 사용함으로써 오실행에 관한 처리를 설정할 수 있다. 이 메소드는 오실행 명령 타입을 인자로 받아들인고, 그 값은 다음중의 하나가 될 수 있다:

  • Trigger.INSTRUCTION_NOOP: 아무 일도 하지 않는다.
  • Trigger.INSTRUCTION_RE_EXECUTE_JOB: 즉시 작업을 실행한다.
  • Trigger.INSTRUCTION_DELETE_TRIGGER: 오실행한 작업을 삭제한다.
  • Trigger.INSTRUCTION_SET_TRIGGER_COMPLETE: 작업이 완료를 선언한다.
  • Trigger.INSTRUCTION_SET_ALL_JOB_TRIGGERS_COMPLETE: 작업을 위한 모든 trigger의 완료를 선언한다.
  • Trigger.MISFIRE_INSTRUCTION_SMART_POLICY: 특정 Trigger의 구현에 가장 알맞은 오실행 처리 명령을 선택한다.

CronTrigger와 같은 Trigger 구현은 유용한 오실행 처리 명령을 새로 정의할 수 있다. 이것에 관한 더 많은 정보는 이 클래스들의 Javadoc을 참고하기 바란다. TriggerListener를 사용함으로써 오실행이 발생했을 경우 취할 액션에 관해 더 많은 제어권을 가질 수 있다. 또한 트리거의 실행이나 종료와 같은 트리거 이벤트에 반응하기 위해 이것을 사용할 수 있다.

SchedulerListener 는 스케쥴러 종료나 작업과 트리거의 추가나 제거와 같은 전체적인 시스템 이벤트를 다룬다.

여기서 우리는 보고서 생성 예제를 위한 간단한 JobListener 를 보여줄 것이다. 먼저 JobListener 인터페이스를 구현하는 클래스를 작성해야 한다.

package net.nighttale.scheduling;

import org.quartz.*;


public class MyJobFailedListener implements JobListener {

  public String getName() {
    return "FAILED JOB";
  }

  public void jobToBeExecuted
    (JobExecutionContext arg0) {
  }


  public void jobWasExecuted(
    JobExecutionContext context,
    JobExecutionException exception) {

    if (exception != null) {
      System.out.println(
        "Report generation error"
      );
      // TODO notify development team
    } 
  }
}

그리고 예제의 main 메소드에 다음을 추가한다:

sched.addGlobalJobListener(new MyJobFailedListener());

이 리스너를 스케쥴러 작업 리스너의 전체 목록에 추가함으로써 리스너는 모든 작업의 이벤트에 대해 호출 될 것이다. 물론 리스너를 특정 작업에 대해서만 설정할 수도 있다. 그렇게 하기 위해 ScheduleraddJobListeners() 메소드를 사용해야 한다. 그리고 JobDetailaddJobListener() 메소드를 사용해서 등록된 리스너를 리스너의 작업 목록에 추가해라. 이 때 리스너 이름을 파라미터로 사용한다. 리스너 이름은 리스너의 getName() 메소드의 리턴값을 말한다.

sched.addJobListener(new MyJobFailedListener());
jobDetail.addJobListener("FAILED JOB");

리스너가 정말로 동작하는지 테스트하기 위해, 다음을 보고서 생성 작업의 execute() 메소드 안에 추가한다.

throw new JobExecutionException();

작업이 실행된 후 리스너의 jobWasExecuted() 메소드가 실행되고, 예외가 발생한다. 예외는 null 이 아니기 때문에 "Report generation error" 메세지를 화면에서 볼 수 있을 것이다.

리스너에 관한 마지막 사항은 애플리케이션의 성능을 떨어뜨릴 수 있기 때문에 시스템에서 사용되는 리스너의 갯수에 유의해야한다는 것이다.

Quartz의 기능을 확장할 수 있는 방법이 한 가지 더 있다. 그것은 플러그 인을 이용하는 것이다. 플러그 인은 실질적으로 여러분이 필요한 어떤 일도 할 수 있다; 단지 해야할 일은 org.quartz.spi.SchedulerPlugin 인터페이스를 구현하는 것 뿐이다. 이 인터페이스는 구현해야할 두 개의 메소드를 정의한다 -- 하나는 초기화(스케쥴러 객체를 파라미터로 받는)를 위한 것이고, 또 하나는 종료를 위한 것이다. 나머지는 모두 여러분에게 달려 있다. SchedulerFactory 가 플러그 인을 사용하도록 하기 위해 quartz.properties 파일에 플러그 인 클래스와 몇 가지 옵션 설정 파라미터(플러그인를에서 필요로 하는)를 추가한다. In order to make SchedulerFactory use a certain plug-in, all you have to do is to add a line in the properties file (quartz.properties) with the plug-in class and a few optional configuration parameters (which depend on the particular plug-in). There are a few plug-ins already in Quartz itself. One is the shutdownHook, which can be used to cleanly shut down the scheduler in case the JVM terminates. To use this plug-in, just add the following lines in the configuration file:

org.quartz.plugin.shutdownHook.class = 
   org.quartz.plugins.management.ShutdownHookPlugin
org.quartz.plugin.shutdownHook.cleanShutdown = true

어떤 환경에서도 융통성 있는

지금까지 Standalone 애플리케이션에서 Quartz를 사용하는 것을 살펴 봤다. 이제 Quartz 인터페이스를 자바 개발자의 가장 보편적인 환경 하에서 어떻게 사용하는지 살펴 보자.

RMI

RMI를 사용하는 분산 애플리케이션에서 Quartz는 지금까지 본 것과 마찬가지로 간단하다. 차이점은 설정 파일 밖에 없다.

두 가지 필수 단계가 있다: 먼저 Quratz를 우리의 request를 다루는 RMI 서버로 설정하는 것이다. 그러고 나면 단지 일반적인 방식으로 사용하기만 하면 된다.

이 예제의 소스 코드는 실질적으로 첫번째 예제와 동일하지만, 우리는 이것을 두 부분으로 나눌 것이다: 스케쥴러 초기화와 스케쥴러 사용.

package net.nighttale.scheduling.rmi;

import org.quartz.*;

public class QuartzServer {

  public static void main(String[] args) {
 
    if(System.getSecurityManager() != null) {
      System.setSecurityManager(
        new java.rmi.RMISecurityManager()
      );
    }
    
    try {
      SchedulerFactory schedFact =
       new org.quartz.impl.StdSchedulerFactory();
      Scheduler sched = schedFact.getScheduler();
      sched.start(); 
    } catch (SchedulerException se) {
      se.printStackTrace();
    }
  }
}

위에서 볼 수 있듯이, 스케쥴러 초기화 코드는 security manager를 설정하는 부분을 포함하고 있다는 것 외엔 다른 것과 다를 바 없다. 중요한 것은 설정 파일(quartzServer.properties)안에 있다. 그것은 다음과 같다:

#
# Configure Main Scheduler Properties 
#
org.quartz.scheduler.instanceName = Sched1
org.quartz.scheduler.rmi.export = true
org.quartz.scheduler.rmi.registryHost = localhost
org.quartz.scheduler.rmi.registryPort = 1099
org.quartz.scheduler.rmi.createRegistry = true

#
# Configure ThreadPool 
#

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 4

#
# Configure JobStore 
#

org.quartz.jobStore.misfireThreshold = 5000

org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

강조된 부분을 주목하라. 이것은 이 스케쥴러가 RMI를 통하여 인터페이스를 반출하고 RMI registry를 실행하기 위해 파라미터들을 제공해야 한다는 것을 보여준다.

서버를 성공적으로 배치하기 위해 몇 가지 할 일이 더 있다. 그것들은 모두 RMI를 통하여 객체를 반출하기 위한 전형적인 작업이다. 먼저 rmiregistry 를 시작할 필요가 있다. 이것은 다음과 같이 호출함으로써 이루어진다: 유닉스 시스템에서는

   rmiregistry &

또는 윈도우 플랫폼에서는

   start rmiregistry

 

다음에 QuartzServer 클래스를 다음과 같은 옵션으로 시작한다:

java  -Djava.rmi.server.codebase
   file:/home/dejanb/quartz/lib/quartz.jar
   -Djava.security.policyrmi.policy
   -Dorg.quartz.propertiesquartzServer.properties
   net.nighttale.scheduling.rmi.QuartzServer

이제 이러한 파라미터들을 좀 더 자세하게 살펴보자. Quartz의 Ant 빌드 태스크는 필요한 RMI 클래스를 클라이언트가 codebase로 가리키도록 이 클래스들을 생성하는 rmic 호출을 포함하고 있다. 그러기 위해 빌드 파일에 -Djava.rmi.server.codebase 파라미터로 시작하도록 설정해야 한다:추가적으로 quartz.jar 의 완전한 경로도 포함되어야 한다.(물론 이것은 라이브러리의 URL이 될 수도 있다.)

분산 시스템에서 중요한 이슈는 보안이다; 그러므로 RMI는 보안 정책을 사용하도록 강제할 것이다. 예제에서는 모든 권한을 허용하는 기본 정책(rmi.policy)을 사용했다.

grant {
  permission java.security.AllPermission;
};

실제 적용할 때는 시스템의 보안 요구사항에 따라 정책을 변경해야 한다.

이제 스케쥴러는 RMI를 통해 Job을 받아들일 준비가 되었다. 이제 클라이언트 코드를 살펴보자.

package net.nighttale.scheduling.rmi;

import org.quartz.*;
import net.nighttale.scheduling.*;

public class QuartzClient {

  public static void main(String[] args) {
    try {
      SchedulerFactory schedFact =
       new org.quartz.impl.StdSchedulerFactory();
      Scheduler sched = schedFact.getScheduler();
      JobDetail jobDetail = new JobDetail(
        "Income Report",
        "Report Generation",
        QuartzReport.class
      );
  
      CronTrigger trigger = new CronTrigger(
        "Income Report",
        "Report Generation"
      );
      trigger.setCronExpression(
        "0 0 12 ? * SUN"
      );
      sched.scheduleJob(jobDetail, trigger);
    } catch (Exception e) {
      e.printStackTrace();
    }
   }
}

위에 나온 바와 같이 클라이언트 소스는 이전 예제와 동일하다. 물론 여기에도 quartzClient.properties 의 설정은 다르다. 추가해야 할 것은 이 스케쥴러가 RMI 클라이언트(proxy)이고, 서버를 찾을 registry의 위치를 설정하는 것 뿐이다.

# Configure Main Scheduler Properties  

org.quartz.scheduler.instanceName = Sched1
org.quartz.scheduler.rmi.proxy = true
org.quartz.scheduler.rmi.registryHost = localhost
org.quartz.scheduler.rmi.registryPort = 1099

나머지는 모두 서버 측에서 이루어지기 때문에 클라이언트 측에 이외의 다른 어떤 설정도 필요하지 않다. 사실 다른 설정이 있다해도 이것은 무시될 것이다. 중요한 것은 스케쥴러의 이름은 클라이언트와 서버의 설정이 일치해야 한다는 것이다.(예제의 경우 Shed1) 그러면 시작하기 위한 준비는 끝났다. 단지 리다이렉트된 프로퍼티 파일로 클라이언트를 시작하기만 하면 된다:

java -Dorg.quartz.properties
       quartzClient.properties
       net.nighttale.scheduling.rmi.QuartzClient

이제 서버 콘솔에서 첫번째 예제와 같은 결과를 볼 수 있을 것이다.

웹과 엔터프라이즈 환경

웹이나 엔터프라이즈 환경의 솔류션을 개발하고 있다면, 스케쥴러를 시작하기 위한 적절한 곳은 어디인지 의문이 생길 것이다. 이것을 위해 Quartz는 org.quartz.ee.servlet.QuartzInitializerServlet를 제공한다. 할 일은 단지 web.xml 파일에 다음 설정을 추가하는 것 뿐이다:


  
   QuartzInitializer
  
  
   Quartz Initializer Servlet
  
  
   org.quartz.ee.servlet.QuartzInitializerServlet
  
  
   1
  

Job을 EJB 메소드에서 호출하고 싶다면, org.quartz.ee.ejb.EJBInvokerJob 클래스를 JobDetail 에 전달해야 한다. 이러한 기법을 보여주기 위해, ReportGenerator를 세션빈으로 구현하고 서블릿으로부터 generateReport() 메소드를 호출할 것이다.

package net.nighttale.scheduling.ee;

import java.io.IOException;

import javax.servlet.*;
import net.nighttale.scheduling.Listener;
import org.quartz.*;
import org.quartz.ee.ejb.EJBInvokerJob;
import org.quartz.impl.StdSchedulerFactory;

public class ReportServlet implements Servlet {

  public void init(ServletConfig conf)
    throws ServletException {
    JobDetail jobDetail = new JobDetail(
      "Income Report",
      "Report Generation",
      EJBInvokerJob.class
    );
    jobDetail.getJobDataMap().put(
      "ejb",
      "java:comp/env/ejb/Report"
    );
    jobDetail.getJobDataMap().put(
      "method",
      "generateReport"
    );
    Object[] args = new Object[0];
    jobDetail.getJobDataMap().put("args", args);
    CronTrigger trigger = new CronTrigger(
      "Income Report",
      "Report Generation"
    );
    try {
      trigger.setCronExpression(
        "0 0 12 ? * SUN"
      );
      Scheduler sched =
       StdSchedulerFactory.getDefaultScheduler();
      sched.addGlobalJobListener(new Listener());
      sched.scheduleJob(jobDetail, trigger);
      System.out.println(
        trigger.getNextFireTime()
      );
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public ServletConfig getServletConfig() {
    return null;
  }

  public void service(ServletRequest request,
                      ServletResponse response)
    throws ServletException, IOException {
  }

  public String getServletInfo() {
    return null;
  }

  public void destroy() {
  }
}

위에 나온 바와 같이, job에 전달할 필요가 있는 파라미터가 3개가 있다.

  • ejb: 엔터프라이즈 빈의 JNDI 이름.
  • method: 실제 호출되야 할 메소드의 이름.
  • args: 메소드의 인자로 전달되야 할 객체의 배열.

나머지는 Quartz의 일반 사용법과 다른 것이 없다. 간단하게 하기 위해 이 예제를 서블릿의 초기화 메소드에 위치시켰지만, 물론 애플리케이션의 어떠한 위치에 놓아도 상관 없다. Quartz를 성공적으로 실행하기 위해 웹 애플리케이션에 이 EJB를 등록할 필요가 있다. 일반적으로 web.xml 파일에 다음을 추가하면 된다.


  ejb/Report
  Session
  net.nighttale.scheduling.ee.ReportHome
  net.nighttale.scheduling.ee.Report
  ReportEJB

어떤 애플리케이션 서버(Orion과 같은)는 유저 쓰레드를 생성하기 위한 권한을 설정해야 할 필요가 있다. 그러므로 -userThread와 같은 옵션으로 실행해야 할 것이다.

지금까지 살펴 본 것이 Quartz의 엔터프라이즈 기능의 전부는 아니다. 그러나 처음 시작할 때 참고할 만할 것이다. 지세한 정보를 원한다면 Quartz's Javadocs 를 참고하기 바란다.

Web Services

Quartz는 현재 웹 서비스를 위해 내장된 지원은 없지만, XML-RPC 을 통하여 스케쥴러 인터페이스를 반출하는 플러그 인을 찾을 수 있다. 설치 절차는 간단하다. 시작하기 위해 플러그 인 소스를 Quartz 소스 폴더에 압축을 풀고 다시 빌드해야만 한다. 플러그 인은 Jakarta XML-RPC 라이브러리에 의존하므로, 그것이 클래스패쓰에 잡혀 있는지 확인해야 한다. 다음에 프로퍼티 파일에 아래의 내용을 추가한다.

org.quartz.plugin.xmlrpc.class = org.quartz.plugins.xmlrpc.XmlRpcPlugin
org.quartz.plugin.xmlrpc.port = 8080

이제 스케쥴러는 XML-RPC를 통해 사용할 수 있는 준비가 되었다. 이러한 방식으로 PHP나 Perl같은 다른 언어, 또는 RMI가 적합한 해결책이 아닌 분산 환경에서, Quartz의 기능 중의 일부를 사용할 수 있다.

요약

지금까지 자바에서 스케쥴링을 구현하는 두 가지 방법에 대해 살펴 봤다. Quartz는 매우 강력한 라이브러리지만, 간단한 요구사항에 대해서는 Timer API가 시간을 절약하고 시스템에 불필요한 복잡성을 피할 수 있게 해 준다. 스케쥴링이 필요한 작업이 그다지 많지 않고, 프로세스에서 실행시간이 잘 알려져 있을 경우(그리고 불변일 경우) Timer API를 사용할 것을 고려해라. 이러한 상황에서 시스템 중지나 다운 때문에 스케쥴링 작업들을 잃어버릴 것에 대해 걱정할 필요는 없다. 더욱 복잡한 요구사항을 위해서는 Quartz가 우아한 해결책이다. 물론 언제든지 Timer API를 기반으로 당신 자신의 프레임워크를 만들 수 있다. 그러나 이미 당신이 필요로 하는 모든 기능(그리고 그 이상의 기능)을 제공하는 훌륭한 해결책이 존재하는데 왜 그런 귀찮을 일을 하려는가?

한 가지 주목할 만한 이벤트는 IBM과 BEA이 제출한 "Timer for Application Servers" JSR 236 이다. 이 스펙은 스케쥴링에 관련된 표준 API가 부족한 J2EE 환경에서 타이머를 위한 API를 만드는 데 촛점을 두고 있다. IBM's developerWorks site에서 스펙에 관련된 보다 자세한 사항을 찾을 수 있다. 이 스펙은 봄에 일반 사용자에게 공개될 것이다.

샘플 코드

ReportGenerator.zip

Dejan Bosanac은 DNS 유럽의 소프트웨어 개발자 이다.


신고
Posted by 타임워커™


티스토리 툴바