본문 바로가기
Spring/Batch

Spring Batch Quartz(쿼츠) 로 배치 스케줄링 하기

by 딘딘은딘딘 2024. 10. 7.
반응형

- Spring Quartz(쿼츠)란?

스프링 배치 쿼츠는 스프링 배치와 쿼츠(Quartz)를 결합하여 스케줄링된 작업을 관리하고 실행하는 방식이다.

스프링 배치는 데이터 처리 및 대용량 작업을 처리하기 위한 프레임워크이고, 쿼츠는 자바 기반의 스케줄링 프레임워크이다.

이 두 가지를 결합하면 특정 시간에 스프링 배치 작업(Job)을 실행하도록 스케줄링할 수 있다.

 

스프링 배치 자체는 데이터 처리의 배치 작업을 담당하지만, 작업 실행 시점을 정하는 기능은 기본 제공하지 않는데

이를 보완하기 위해, 쿼츠를 사용하여 특정 시간에 배치 작업을 실행할 수 있다.

 

- 쿼츠와 스케줄링의 차이

쿼츠스케줄링은 유사한 개념으로 보이지만, 세부적으로는 다음과 같은 차이가 있다.

  • 쿼츠(Quartz):
    • 자바에서 사용할 수 있는 강력한 스케줄링 라이브러리
    • 정교한 트리거 설정이 가능하며, JobTrigger를 통해 작업의 실행 시간과 주기를 매우 유연하게 정의할 수 있다.
    • 데이터베이스를 이용하여 작업의 상태를 저장하고, 중단된 스케줄의 복구나 클러스터링 환경에서 작업 관리가 가능합니다.
  • 스케줄링(Scheduling):
    • 일반적으로 자바에서 사용되는 스케줄링 기능은 대표적으로 @Scheduled 어노테이션 같은 간단한 방식으로 사용된다.
      예를 들어, @Scheduled(fixedRate = 5000)를 사용하면 5초마다 작업을 실행할 수 있다.
    • 쿼츠에 비해 설정이 간단하고 빠르게 사용할 수 있지만, 복잡한 스케줄링이나 클러스터 환경에서의 작업 관리에는 적합하지 않다.
    • 주로 단순한 주기적 작업을 처리하는 데 사용된다.

 

- 쿼츠의 장단점

장점:

  • 정교한 스케줄 관리: Cron 표현식 등을 사용해 매우 정밀한 스케줄을 설정할 수 있는데, 예를 들어 특정 시간, 주기, 요일, 월 등 다양한 조건에 맞춰 작업을 예약할 수 있다.
  • 클러스터링 지원: 쿼츠는 클러스터링을 지원하여 여러 서버에서 작업을 분산 처리하거나 하나의 노드에서만 실행되도록 할 수 있다.
    이 기능은 고가용성을 필요로 하는 환경에서 유용하다.
  • 작업 상태 관리: 쿼츠는 데이터베이스를 사용하여 작업의 실행 상태(예: 성공, 실패, 재시도)를 관리할 수 있어, 중단된 작업의 재시도나 상태 모니터링이 가능합니다.
  • 복구 기능: 서버 재시작 후에도 이전의 스케줄 상태를 유지할 수 있으며, 예상치 못한 중단 상황에서도 스케줄의 지속성이 보장됩니다.

단점:

  • 설정의 복잡성: 쿼츠의 강력한 기능은 곧 복잡한 설정을 요구합니다. 여러 설정 파일과 객체를 다루어야 하고, 트리거 및 잡 설정을 정교하게 다루어야 하기 때문에 간단한 작업을 처리하기엔 오히려 복잡할 수 있습니다.
  • 리소스 사용량: 내부적으로 데이터베이스와 연동하여 상태를 관리하기 때문에, 대규모 트리거나 작업 스케줄링이 빈번한 경우 데이터베이스 및 시스템 자원을 많이 소모할 수 있습니다.
  • 관리의 어려움: 클러스터링과 같은 기능을 활용할 때는 설정 및 관리가 까다로울 수 있습니다. 여러 서버 간의 동기화 문제나 스케줄 조정 등에서 관리 비용이 증가할 수 있습니다.

쿼츠는 단순히 일정한 주기로 작업을 실행하는 것을 넘어서, 정밀하고 복잡한 스케줄 관리가 필요한 경우에 적합합니다. 반면, 간단한 스케줄링이 필요한 경우에는 스프링의 @Scheduled 같은 간단한 방법이 더 나을 수 있습니다.

이처럼 스프링 배치와 쿼츠를 결합하여 사용하면, 복잡한 스케줄링과 대용량 데이터 처리 작업을 효율적으로 관리할 수 있습니다.

 

쿼츠의 클러스터링 지원

쿼츠의 클러스터링은 여러 서버에서 하나의 스케줄링 작업을 공유하며 실행할 수 있는 기능이다.
이 기능은 특히 고가용성과 부하 분산이 필요한 환경에서 유용하다.

예를들어
두 개의 서버 A와 B가 있고, 이 서버들이 동일한 쿼츠 스케줄러와 동일한 데이터베이스를 사용하고 있고,
서버 A와 B 모두 QuartzScheduler를 설정할 때
yaml에 org.quartz.jobStore.isClustered=true 속성을 활성화하여 클러스터링 모드로 설정한다.

클러스터 모드에서는 Job 실행 시 두 서버가 동시에 동일한 작업을 실행하지 않고, 오직 하나의 서버에서만 Job이 실행된다.
예를 들어, 매일 자정에 실행되는 배치 작업이 있을 때, 서버 A와 B 중 하나에서만 실행되고. 만약 서버 A가 중단되면 서버 B가 그 작업을 이어서 실행하게 된다.

이런 클러스터링 설정은 주로 JobStore를 데이터베이스에 두고, 두 서버가 동일한 JobStore를 공유하는 구조에서 이루어지며
이를 통해 각 서버는 현재 실행 중인 작업 상태를 데이터베이스를 통해 공유하게 되어, 중복된 작업 실행을 방지할 수 있다.

 

JDBCJobStore를 사용하면, 모든 서버가 동일한 데이터베이스에 접근하여 현재 실행 중인 작업, 예약된 작업, 트리거 상태 등을 데이터베이스에 저장하고 관리할 수 있다.

 

이번에는 클러스터링 기능을 테스트를 진행하지 않을 것이기 때문에 쿼츠의 클러스터링에 대한 내용은 간단하게만 소개하고 넘어간다.

 

- 쿼츠의 핵심 클래스 및 인터페이스

 

  • Scheduler: 스케줄링 작업을 관리하는 가장 중요한 인터페이스
    Scheduler는 스케줄러를 시작, 중지, 일시 정지하거나 재개할 수 있으며, Job 및 Trigger를 등록하고 관리할 수 있다.
    쿼츠의 Scheduler 인스턴스는 SchedulerFactory를 통해 생성된다.

  • JobDetail: Job 클래스와 관련된 메타데이터를 포함하는 클래스
    JobDetail은 Job 인스턴스와 관련된 정보를 담고 있으며, Job이 실행될 때 어떤 구성을 사용할지를 정의합니다.
    JobDetail은 주로 JobBuilder를 사용해 생성한다.

  • Trigger: Job이 언제 실행될지를 정의하는 인터페이스
    Trigger는 SimpleTriggerCronTrigger가 대표적인 구현체.
    • SimpleTrigger: 특정 시간에 한 번 실행하거나, 일정한 간격으로 반복 실행할 때 사용.
    • CronTrigger: 크론 표현식을 사용하여 매우 정밀하게 시간 설정을 할 수 있다.
      예를 들어, "매주 월요일 오전 9시에 실행"과 같은 스케줄을 정의할 수 있습니다.
  • SchedulerFactory: Scheduler 인스턴스를 생성하는 역할
    StdSchedulerFactory가 가장 많이 사용되는 구현체로. 설정 파일이나 프로그래밍 방식으로 Scheduler의 설정을 관리할 수 있다.

  • JobStore: 스케줄러가 Job 및 Trigger의 상태를 저장하는 방식을 정의하는 인터페이스
    JobStore는 쿼츠는 두 가지 주요 JobStore를 제공한다.
    • RAMJobStore: 메모리에 작업 상태를 저장 -> 영속성이 없고, 클러스터링이 불가능하다.
    • JDBCJobStore: 데이터베이스를 사용하여 작업의 상태를 저장 -> 클러스터링 환경에서는 이 JDBCJobStore를 사용하여
      서버 간 작업 상태를 공유한다.

      JDBCJobStore를 사용하면, 모든 서버가 동일한 데이터베이스에 접근하여 현재 실행 중인 작업, 예약된 작업, 트리거 상태 등을 데이터베이스에 저장하고 관리할 수 있다.
  • TriggerListener : 트리거의 상태 변화나 실행 이벤트를 감지하고, 이에 따른 콜백 메서드를 제공한다
    트리거가 실행되기 전이나 후, 또는 트리거가 완료되었을 때의 이벤트를 처리할 수 있다.
    이를 통해 트리거 실행의 흐름을 제어하거나 추가적인 로깅이나 알림 기능을 구현할 수 있다.

 

- JDBCJobStore 를 사용할때 생성되는 Job과 Trigger 정보를 저장하는 쿼츠의 테이블들

쿼츠의 기본 테이블들은 QRTZ_로 시작하며, 각 테이블의 이름과 기능은 다음과 같다

  1. QRTZ_JOB_DETAILS
    • Job의 정의와 메타데이터를 저장하는 테이블로, 실제 Job의 실행 내용이 아닌 Job의 기본 정보가 담겨 있다.
    • 주요 컬럼:
      • JOB_NAME, JOB_GROUP: Job의 이름과 그룹. 동일한 그룹 내에서는 Job 이름이 유일해야 한다.
      • JOB_CLASS_NAME: 실행할 Job의 클래스 이름을 저장한다.
      • IS_DURABLE: 잡이 트리거 없이도 유지될 수 있는지 여부를 나타낸다.
  2. QRTZ_TRIGGERS
    • 스케줄러가 트리거를 관리하기 위해 필요한 메타 정보를 저장하며, 트리거와 관련된 실행 상태를 추적한다.
    • 주요 컬럼:
      • TRIGGER_NAME, TRIGGER_GROUP: 트리거의 이름과 그룹. 동일한 그룹 내에서는 트리거 이름이 유일해야 한다.
      • TRIGGER_STATE: 트리거의 현재 상태 (예: WAITING, ACQUIRED, PAUSED 등)
      • NEXT_FIRE_TIME, PREV_FIRE_TIME: 트리거의 다음 실행 시간과 마지막 실행 시간을 저장한다.
  3. QRTZ_SIMPLE_TRIGGERS
    • QRTZ_TRIGGERS 테이블과 조인하여 사용되며, 반복 실행 횟수나 주기를 설정하는 
      SimpleTrigger에 특화된 정보를 저장한다.
    • 주요 컬럼:
      • REPEAT_COUNT: 반복 실행 횟수.
      • REPEAT_INTERVAL: 반복 주기 (밀리초).
  4. QRTZ_CRON_TRIGGERS
    • CronTrigger 유형의 트리거에 대한 추가 정보를 저장한다. 
      QRTZ_TRIGGERS 테이블과 조인하여 사용되며, 크론 표현식 기반의 트리거 스케줄을 정의할 때 사용됨.
    • 주요 컬럼:
      • CRON_EXPRESSION: 크론 표현식. 주기적인 스케줄링을 위해 사용된다.
  5. QRTZ_BLOB_TRIGGERS
    • 복잡한 트리거의 데이터를 Blob 형태로 저장한다. 
      일반적인 텍스트 정보 외의 복잡한 객체 정보를 저장하는 용도로 사용됨
    • 주요 컬럼:
      • BLOB_DATA: 트리거와 관련된 직렬화된 데이터
  6. QRTZ_CALENDARS
    • 작업 스케줄을 조정하는 캘린더 정보를 저장합니다.
      특정 시간대를 제외하고 작업을 실행하거나, 특정 날짜에만 작업을 수행하는 등 스케줄링 시간 조정을 위해 사용됨
    • 주요 컬럼:
      • CALENDAR_NAME: 캘린더의 이름.
      • CALENDAR: 캘린더 객체를 직렬화한 Blob 데이터.
  7. QRTZ_FIRED_TRIGGERS
    • 최근에 실행된 트리거의 상태를 저장한다.
      트리거의 실행 히스토리와 관련된 정보를 저장하여 트리거의 실행 로그를 관리하는 데 사용됨
    • 주요 컬럼:
      • ENTRY_ID: 실행된 트리거의 고유 ID.
      • INSTANCE_NAME: 실행한 스케줄러 인스턴스의 이름.
      • FIRED_TIME: 트리거가 실행된 시간.
  8. QRTZ_PAUSED_TRIGGER_GRPS
    • 현재 일시 중지된 트리거 그룹을 저장한다.
      특정 트리거 그룹이 일시 중지 상태인지 여부를 확인할 수 있다.
  9. QRTZ_SCHEDULER_STATE
    • 현재 스케줄러의 상태 정보를 저장하며, 클러스터링 환경에서 각 스케줄러 인스턴스의 상태를 추적하는 데 사용된다.
      이를 통해 
      서버의 가동 상태를 모니터링하고, 장애가 발생했을 때 다른 인스턴스가 작업을 이어받을 수 있다.
    • 주요 컬럼:
      • INSTANCE_NAME: 스케줄러 인스턴스의 이름.
      • LAST_CHECKIN_TIME: 마지막 체크인 시간.
      • CHECKIN_INTERVAL: 체크인 주기.
  10. QRTZ_LOCKS
    • 트리거나 작업의 락 정보를 저장하여, 여러 인스턴스 간에 동일한 작업이 중복 실행되지 않도록 한다.
    • 설명: 클러스터링 환경에서 작업 실행 중에 락을 걸어 다른 인스턴스가 동일한 트리거를 실행하지 못하게 함.
      이는 데이터 정합성을 보장하는 데 중요한 역할을 한다.

 


의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-quartz'

 

 

application.yml

spring:
  datasource:
    driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
    url: jdbc:log4jdbc:mysql://127.0.0.1:43306/TESTDB?allowMultiQueries=true
    username: root
    password: 1234
    hikari:
      connection-timeout: 3000
      max-lifetime: 1740000
      maximum-pool-size: 20
  jpa:
    hibernate:
      ddl-auto: update
    open-in-view: false
    show-sql: false
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQLDialect
        storage-engine: innodb
        hbm2ddl-auto: none  # must be none
        show-sql: true
        format_sql: true          # 포매터 적용
        use_sql_comments: true    # jpql 출력

  batch:
    job:
      enabled: false
    jdbc:
      initialize-schema: never # always: 배치 메타테이블 항상 생성 / never: 생성하지 않음 / embedded: H2 DB 인 경우 생성
  # quartz Configuration
  quartz:
    scheduler-name: test-quartz
    job-store-type: jdbc # 스케줄 정보를 jdbc로 관리하도록 설정
    jdbc:
      initialize-schema: always # Quartz 관련 테이블을 자동으로 생성할지 여부를 결정.
                                # - always: 애플리케이션이 실행될 때마다 Quartz의 테이블을 자동으로 생성. (임베디드 DB 또는 빈 DB에서만 유효)
                                # - never: 테이블을 생성하지 않음 (사용자가 직접 DDL 스크립트를 실행해야 함).
                                # - embedded: 임베디드 DB를 사용하는 경우에만 자동으로 테이블 생성.
    properties:
      org:
        quartz:
          scheduler:
            instanceName: TestQuartzScheduler # 스케줄러의 이름을 지정. 클러스터링 시 각 인스턴스가 고유한 이름을 가질 수 있도록 설정.
            instanceId: AUTO # 스케줄러 인스턴스의 고유 ID. 'AUTO'로 설정하면 Quartz가 자동으로 각 인스턴스에 고유 ID를 할당함.
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX # Quartz에서 사용하는 JobStore 클래스를 지정.
                                                           # JobStoreTX는 트랜잭션을 지원하는 데이터베이스 기반 JobStore로, 스케줄 정보가 데이터베이스에 저장됨.
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # 사용하는 DB의 JDBC 드라이버에 맞는 Delegate 클래스를 지정.
                          # 기본적으로 표준 JDBC를 사용하며, 데이터베이스에 맞는 Delegate를 지정해야 할 수도 있음 (예: MySQL, PostgreSQL 등).
            dataSource: batchDataSource # Quartz에서 사용할 데이터소스 지정.
            tablePrefix: QRTZ_ # 데이터베이스 테이블의 이름에 사용할 접두어.
            isClustered: false # Quartz가 클러스터 모드에서 실행될지 여부.
                              # true로 설정하면 여러 노드에서 스케줄러를 실행할 수 있고, 하나의 잡을 여러 인스턴스에서 동시에 실행하지 않도록 조율.
                              # false로 설정하면 단일 노드에서만 실행됨.
          threadPool: # Quartz가 사용할 스레드 수를 지정. 잡을 처리할 때 동시에 실행할 수 있는 스레드의 수.
            threadCount: 2
          dataSource:
            url: jdbc:log4jdbc:mysql://127.0.0.1:43306/TESTDB?allowMultiQueries=true
            username: root
            password: 1234
    auto-startup: true # 서버가 시작될 때 스케줄러가 자동으로 시작됨

 

BatchScheduler.Java

@RequiredArgsConstructor
@Component
@Slf4j
public class BatchScheduler extends QuartzJobBean {
  @Autowired
  private JobLauncher jobLauncher;

  @Autowired
  private JobRegistry jobRegistry;

  @Override
  protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
    String jobName = context.getJobDetail().getKey().getName(); // JobDetail에서 잡 이름을 가져옴
    try {
      Job job = jobRegistry.getJob(jobName);  // JobRegistry에서 Job을 동적으로 조회
      jobLauncher.run(job, new JobParametersBuilder().addLong("time", System.currentTimeMillis()).toJobParameters());
    } catch (Exception e) {
      log.error("fail to execute job : {}, {}", jobName, this.getClass().getSimpleName(), e);
      throw new JobExecutionException(e);
    }
  }
}

 

QuartzJobBean를 상속받은 이유는 스프링과 쿼츠의 통합을 간소화하고, 트랜잭션 관리 및 스프링 컨텍스트와의 호환성을 높이기 위해

설정했다. 이를 통해 스프링 애플리케이션에서 쿼츠 잡을 정의할 때 간편하게 사용할 수 있다.

 

스프링에서 쿼츠를 사용할 때 QuartzJobBean을 상속받으면, ApplicationContext의 빈들을 자동으로 DI 할 수 있어,

애플리케이션 내에서 설정된 의존성 주입을 사용할 수 있다. (예: JobLauncher, JobRegistry)

또한, 배치 잡 실행 시 트랜잭션의 시작과 종료를 자동으로 관리할 수 있다.

 

QuartzConfig.Java

@Configuration
@Slf4j
public class QuartzConfig {

  @Bean("TEST_QUARTZ_BEAN")
  public SchedulerFactoryBean schedulerFactoryBean(@Qualifier(value = "batchDataSource") DataSource dataSource,
      @Qualifier(value = "batchTransactionManager") PlatformTransactionManager transactionManager) throws Exception {

    SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();

    // Spring의 DI(의존성 주입)를 사용하는 JobFactory 설정
    schedulerFactoryBean.setJobFactory(new SpringBeanJobFactory());

    // DataSource 및 트랜잭션 설정
    schedulerFactoryBean.setDataSource(dataSource);
    schedulerFactoryBean.setTransactionManager(transactionManager);

    // 기존 잡을 덮어쓸지 여부
    schedulerFactoryBean.setOverwriteExistingJobs(true);

    return schedulerFactoryBean;

  }
}

 

쿼츠 스케줄러가 스프링 컨텍스트 내의 빈과 데이터베이스를 사용하여 작업을 관리할 수 있도록 설정한다.

 

DataSourceConfigration.Java

@Configuration
public class DataSourceConfiguration {
  @Value("${spring.datasource.driver-class-name}")
  private String driverClassName;

  @Value("${spring.datasource.url}")
  private String url;

  @Value("${spring.datasource.username}")
  private String username;

  @Value("${spring.datasource.password}")
  private String password;

  @Value("${spring.datasource.hikari.connection-timeout}")
  private String connectionTimeout;

  @Value("${spring.datasource.hikari.max-lifetime}")
  private String maxLifetime;

  @Value("${spring.datasource.hikari.maximum-pool-size}")
  private String maximumPoolSize;

  @Bean(name = "batchDataSource")
  @Primary
  @BatchDataSource
  public DataSource batchDataSource() {
    HikariConfig config = new HikariConfig();
    config.setDriverClassName(driverClassName);
    config.setJdbcUrl(url);
    config.setUsername(username);
    config.setPassword(password);
    config.setConnectionTimeout(Long.parseLong(connectionTimeout));
    config.setMaxLifetime(Long.parseLong(maxLifetime));
    config.setMaximumPoolSize(Integer.parseInt(maximumPoolSize));
    config.setConnectionTestQuery("SELECT 1");
    HikariDataSource paymentHikariDataSource = new HikariDataSource(config);
    return new LazyConnectionDataSourceProxy(paymentHikariDataSource);
  }
}

 

TransactionManagerConfig.Java

@Configuration
@EnableTransactionManagement // 트랜잭션 관리 활성화. @Transactional 을 사용해 선언적 트랜잭션으로 관리가 가능
@EnableJpaRepositories(
    entityManagerFactoryRef = "batchEntityManagerFactory",
    transactionManagerRef = "batchTransactionManager",
    basePackages = "com.ani.study.domain"
)
public class TransactionManagerConfig {

    @Value("${spring.jpa.properties.hibernate.dialect}")
    private String dialect;

    @Value("${spring.jpa.properties.hibernate.storage-engine}")
    private String storageEngine;

    @Value("${spring.jpa.properties.hibernate.show-sql}")
    private String showSql;

    @Value("${spring.jpa.properties.hibernate.hbm2ddl-auto}")
    private String hbm2ddlAuto;

    /**
     * JPA 구현체에 대한 설정을 반환
     * */
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
        jpaVendorAdapter.setShowSql(true); // 로그 출력
        return jpaVendorAdapter;
    }

    /**
     * 엔티티 매니저 팩토리 빈 생성
     * */
    @Bean(name = "batchEntityManagerFactory")
    @Primary // 기본 EntityManagerFactory 설정
    public LocalContainerEntityManagerFactoryBean batchEntityManagerFactory(@Qualifier("batchDataSource") DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setJpaVendorAdapter(jpaVendorAdapter());
        em.setJpaPropertyMap(hibernateJpaProperties());
        em.setPackagesToScan("com.ani.study.domain.entity");  // JPA 엔티티 위치 설정
        return em;
    }

    /**
     * 하이버네이트 관련 설정 반환
     * */
    private Map<String, ?> hibernateJpaProperties() {
        HashMap<String, String> properties = new HashMap<>();

        properties.put("hibernate.dialect", dialect);
        properties.put("hibernate.dialect.storage_engine", storageEngine);
        properties.put("hibernate.show_sql", showSql);
        properties.put("hibernate.format_sql", "true");
        properties.put("hibernate.hbm2ddl.auto", hbm2ddlAuto);
        return properties;
    }

    /**
     * 트랜잭션 매니저 설정
     * */
    @Bean(name = "batchTransactionManager")
    @Primary
    public PlatformTransactionManager batchTransactionManager(@Qualifier("batchEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}

 

DataSource 및 TransactionManager 설정을 추가해준다.

JDBC 기반 쿼츠를 사용할 예정이기 때문에 DB 설정은 필수이다.

 

 

JOB은 2개를 생성할 예정이다

하나는 5초에 한번씩 실행되는 Job과 또 하나는 10초에 한번씩 실행되는 Job를 생성하고 로그가 어떻게 찍히는지 확인 해본다.

 

QuartzTestJobConfiguration.Java

@Configuration
@RequiredArgsConstructor
@Slf4j
public class QuartzTestJobConfiguration {
  private final JobRepository jobRepository;

  private final PlatformTransactionManager batchTransactionManager;

  private int itemReaderCnt = 0;

  @Bean
  public Job quartzTestJob() {
    return new JobBuilder("quartzTestJob", jobRepository)
        .start(quartzTestStep())
        .build();
  }

  @Bean
  public Step quartzTestStep() {
    return new StepBuilder("quartzTestStep", jobRepository)
        .<Integer, Integer>chunk(10, batchTransactionManager)
        .reader(quartzTestItemReader())
        .writer(quartzTestItemWriter())
        .build();
  }

  @Bean
  public ItemReader<Integer> quartzTestItemReader() {
    return () -> {
      itemReaderCnt++;
      if(itemReaderCnt == 11) {
        itemReaderCnt = 0;
        return null;
      };

      int rtnCnt = itemReaderCnt * 2;

      log.info("[quartzTestItemReader] rtnCnt : {}", rtnCnt);
      return rtnCnt;
    };
  }

  @Bean
  public ItemWriter<Integer> quartzTestItemWriter() {
    return chunk -> {
      log.info("[quartzTestItemWriter] items : {}", chunk);
      log.info("Hello Quartz!!");
    };
  }

  // 스케줄러 설정
  @Bean
  public JobDetailFactoryBean QuartzTestJobDetail() {
    JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
    factoryBean.setJobClass(BatchScheduler.class);
    factoryBean.setDescription("Invoke A Job");
    factoryBean.setDurability(true);
    factoryBean.setName("quartzTestJob");
    return factoryBean;
  }

  @Bean
  public CronTriggerFactoryBean QuartzTestTrigger(JobDetail QuartzTestJobDetail) {
    CronTriggerFactoryBean trigger = new CronTriggerFactoryBean();
    trigger.setJobDetail(QuartzTestJobDetail);
    trigger.setCronExpression("*/10 * * * * ?"); // 10초마다 실행하는 Cron 표현식
    return trigger;
  }

  @Bean
  public Scheduler quartzTestScheduler(SchedulerFactoryBean factoryBean, JobDetail QuartzTestJobDetail, Trigger QuartzTestTrigger) throws Exception {
    Scheduler scheduler = factoryBean.getScheduler();
    scheduler.scheduleJob(QuartzTestJobDetail, QuartzTestTrigger);  // 잡과 트리거를 스케줄러에 등록
    scheduler.start();  // 스케줄러 시작
    return scheduler;
  }
}

 

QuartzSecondTestJobConfiguration.Java

@Configuration
@RequiredArgsConstructor
@Slf4j
public class QuartzSecondTestJobConfiguration {
  private final JobRepository jobRepository;

  private final PlatformTransactionManager batchTransactionManager;

  private int itemReaderCnt = 0;

  @Bean
  public Job quartzSecondTestJob() {
    return new JobBuilder("quartzSecondTestJob", jobRepository)
        .start(quartzSecondTestStep())
        .build();
  }

  @Bean
  public Step quartzSecondTestStep() {
    return new StepBuilder("quartzSecondTestStep", jobRepository)
        .<Integer, Integer>chunk(10, batchTransactionManager)
        .reader(quartzSecondTestItemReader())
        .writer(quartzSecondTestItemWriter())
        .build();
  }

  @Bean
  public ItemReader<Integer> quartzSecondTestItemReader() {
    return () -> {
      itemReaderCnt++;
      if(itemReaderCnt == 11) {
        itemReaderCnt = 0;
        return null;
      };

      int rtnCnt = itemReaderCnt;

      log.info("[quartzSecondTestItemReader] rtnCnt : {}", rtnCnt);
      return rtnCnt;
    };
  }

  @Bean
  public ItemWriter<Integer> quartzSecondTestItemWriter() {
    return chunk -> {
      log.info("[quartzSecondTestItemWriter] items : {}", chunk);
      log.info("Hello Quartz22222!!");
    };
  }

  // 스케줄러 설정
  @Bean
  public JobDetailFactoryBean QuartzSecondTestJobDetail() {
    JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
    factoryBean.setJobClass(BatchScheduler.class);
    factoryBean.setDescription("Second Job");
    factoryBean.setDurability(true);
    factoryBean.setName("quartzSecondTestJob");
    return factoryBean;
  }

  @Bean
  public CronTriggerFactoryBean QuartzSecondTestTrigger(JobDetail QuartzSecondTestJobDetail) {
    CronTriggerFactoryBean trigger = new CronTriggerFactoryBean();
    trigger.setJobDetail(QuartzSecondTestJobDetail);
    trigger.setCronExpression("*/5 * * * * ?"); // 5초마다 실행하는 Cron 표현식
    return trigger;
  }

  @Bean
  public Scheduler quartzSecondTestScheduler(SchedulerFactoryBean factoryBean, JobDetail QuartzSecondTestJobDetail, Trigger QuartzSecondTestTrigger) throws Exception {
    Scheduler scheduler = factoryBean.getScheduler();
    scheduler.scheduleJob(QuartzSecondTestJobDetail, QuartzSecondTestTrigger);  // 잡과 트리거를 스케줄러에 등록 QuartzSecondTestTrigger : Triggers의 Name이 들어간다.
    scheduler.start();  // 스케줄러 시작
    return scheduler;
  }
}

 

이제 서버를 실행 해주고 배치가 돌아가는 것을 확인해본다.

 

5초 주기

 

10초 주기

 

두 Job 모두 정해진 주기로 정상 실행되는 것을 확인 할 수 있다.

 

QRTZ_TRIGGERS

QRTZ_TRIGGERS 테이블에도 정상적으로 트리거와 Job이 등록 된 것을 확인할 수 있다.

 

 

 

참고 : https://docs.spring.io/spring-boot/reference/io/quartz.html

https://tall-developer.tistory.com/47

https://www.quartz-scheduler.org/documentation/

 

반응형