업데이트:


제가 진행중인 프로젝트에서는 특정 메서드가 실행되었을 때, N초를 delay 시켜야 하는 요구사항이 있습니다.

그리고 특정 Job에 스케쥴링을 건 이후에 해당 Job의 스케쥴링을 제거할 수도 있어야 합니다.

이를 Quartz로 구현해 보도록 하겠습니다.

이 포스트에 사용된 모든 코드는 여기에서 확인하실 수 있습니다.


구성 요소

  • Job 인터페이스 구현체 : 실제 실행할 로직을 구현합니다.
  • JobDetail : Job 인터페이스 구현체의 인스턴스를 생성합니다.
    • 같은 그룹에는 동일한 이름을 가진 Job을 생성할 수 없습니다.
  • JobDataMap : 스케쥴러에서 Job이 실행될 때 사용할 값을 전달하는데에 사용합니다.
    • key-value 형식으로 값을 전달하고, Job을 실행할 때 해당 값을 key를 통해 꺼내 쓸 수 있습니다.
  • Trigger : Job을 어떤 방식과 주기로 실행시킬지 결정합니다.
    • SimpleTrigger : 시작 시간, 종료 시간, 실행 간격, 반복 횟수 등 설정
    • CronTrigger : Cron 형식으로 주기를 지정
    • Job : Trigger = 1 : N
  • Scheduler : 생성한 Job과 Trigger를 가지고 스케쥴링을 실행합니다.
  • Listener : 작업의 시작, 중간, 끝, 에러를 처리합니다.
    • ScheduleListener
    • TriggerListener


Spring Boot 설정

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-quartz'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

저는 위와 같이 의존성들이 설정된 Spring Boot 애플리케이션을 생성했습니다.


기본 구성요소 생성

Quartz 테스트를 위한 요소들을 생성해 보겠습니다.

adapter 생성

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import taeheekim.quartzdelay.port.inbound.MyJobAccessor;

@Slf4j
@RestController
@RequiredArgsConstructor
public class MyJobController {

    private final MyJobAccessor myJobAccessor;

    @GetMapping("/start-myjob")
    public String startMyJob() {
        log.info("MyJobStartController - startMyJob");
        myJobAccessor.startMyJob();
        return "MyJobStarted!!";
    }

    @GetMapping("/stop-myjob")
    public String stopMyJob() {
        log.info("MyJobStartController - stopMyJob");
        myJobAccessor.stopMyJob();
        return "MyJobStopped!!";
    }
}

inbound port 생성

public interface MyJobAccessor {

    void startMyJob();

    void stopMyJob();
}


Quartz 실행 요소들 생성

Job 인터페이스 구현체 생성

import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import java.time.LocalDateTime;

/**
 * Quartz의 Job 인터페이스를 구현한 클래스입니다.
 */
@Slf4j
public class MyJob implements Job {

    /**
     * Job이 실행되면 execute 메서드가 호출됩니다. 스케쥴러의 쓰레드 중 하나에 의해 호출됩니다.
     *
     * @param context JobExecutionContext 객체입니다.
     *                이 Job을 실행하는 런타임 환경에 대한 정보를 담고 있습니다.
     *                Scheduler, Trigger, JobDetail 등을 포함하여 Job 인스턴스에 대한 정보를 제공하는 객체입니다.
     *                여기에서는 JobDataMap에서 key 값을 통해 value값을 가져와 로그로 출력했습니다.
     */
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        log.info("MyJob Start time={}", LocalDateTime.now());
        String testValue = context.getJobDetail().getJobDataMap().get("ABCDE").toString();
        log.info("testValue={}", testValue);
        log.info("MyJob End time={}", LocalDateTime.now());
    }
}

스케쥴러 설정

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.stereotype.Service;
import taeheekim.quartzdelay.port.inbound.MyJobAccessor;

import java.time.LocalDateTime;

import static org.quartz.TriggerBuilder.newTrigger;

@Slf4j
@Service
@RequiredArgsConstructor
public class MyJobProcessor implements MyJobAccessor {

    private final Scheduler scheduler; // Quartz의 스케쥴러를 주입받습니다.

    @Override
    public void startMyJob() {
        // JobDataMap을 통해 실행되는 Job에게 key-value 형식으로 데이터를 전달합니다.
        // JobDataMap은 JobExecutionContext를 통해 전달됩니다.
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put("ABCDE", "12345");

        // 실행시킬 Job을 만들고, 위에서 생성한 JobDataMap을 전달합니다.
        JobDetail jobDetail = JobBuilder.newJob(MyJob.class) // 앞서 생성한 MyJob 클래스를 지정합니다.
                .usingJobData(jobDataMap) // JobDataMap를 전달합니다.
                .build();

        // Trigger를 생성합니다.
        Trigger trigger = newTrigger()
                .withIdentity(TriggerKey.triggerKey("myTestJob", "myGroup")) // TriggerKey를 지정합니다.
                .startAt(DateBuilder.futureDate(5, DateBuilder.IntervalUnit.SECOND)) // 5초 후에 실행되도록 설정합니다.
                .build();

        try {
            scheduler.scheduleJob(jobDetail, trigger); // JobDetail과 Trigger를 스케쥴러에 등록합니다.
            scheduler.start(); // 스케쥴러를 시작합니다.
        } catch (SchedulerException e) {
            log.error("스케쥴러 실행 실패", e);
            throw new RuntimeException(e);
        }
    }

    @Override
    public void stopMyJob() {
        try {
            scheduler.unscheduleJob(TriggerKey.triggerKey("myTestJob", "myGroup")); // 스케쥴러에서 TriggerKey로 Job을 스케쥴에서 제거합니다.
            log.info("MyJob Stopped time={}", LocalDateTime.now());
        } catch (SchedulerException e) {
            log.error("스케쥴러 중지 실패", e);
            throw new RuntimeException(e);
        }
    }
}


테스트

Spring Boot 애플리케이션을 시작합니다.

MyJob 실행

Controller에 등록해놓은 엔드포인트 http://localhost:8080/start-myjob 를 호출해 봅니다.

image

위와 같이 5초 뒤에 MyJob이 실행된 것을 확인할 수 있습니다.

JobData에서 key를 통해 꺼낸 value도 잘 출력된 것을 확인할 수 있습니다.

MyJob 실행 후 중지

MyJob을 실행한 후 5초 내에 http://localhost:8080/stop-myjob를 호출시켜 중지가 잘 되는지 확인해보겠습니다.

image

위처럼 MyJob의 실행이 취소되어 해당 클래스의 로그는 찍히지 않는것을 확인할 수 있습니다.

TriggerKey의 name, group명이 일치해야 해당 Job을 찾아서 실행을 취소시킬 수 있습니다.

둘 중 하나라도 다르면 실행이 취소되지 않습니다.


마치며

프로젝트 요구사항에 맞추어 특정 메서드가 실행되었을 때 N초 뒤에 실행되도록 하고, 스케쥴링을 건 이후에 특정 Job의 스케쥴링을 제거하는 기능을 Quartz를 이용해 구현해 보았습니다.

감사합니다.


참고


댓글남기기