개발/spring boot

[Spring] Quartz 도입기 1

냥덕_ 2024. 10. 31. 14:49

처음 공부한 내용이라 틀린 부분이 있을 수도 있습니다........


Github Code
 : batch quartz demo


1 도입기

기업 연계 프로젝트를 진행하면서 아래와 같은 기능이 필요하게 됨

  • 5분 간격으로 상품 증분데이터를 받아와야 하는 기능
  • 상품 데이터의 이상치 검사를 위해서 받은 상품 데이터들을 하나의 배치 ID로 매핑해서 이상치 검사 서버로 보냄
  • 일정 주기로 동작(대충 스케줄링?)+ 데이터를 묶어서 처리(음...배치?)를 생각하게 됨
  • 대용량 수준까지는 아니지만 반복적인 작업을 처리하고 새로운 상품 데이터의 처리라는 점에서 작업 관리가 중요하다고 생각함

Scheduling?

  • 일정한 시간 간격으로 반복적인 작업을 수행하는 도구
  • 특정 작업을 주기적 혹은 일정 시간이 지난 후에 작업을 수행할 수 있으며 효율적인 작업 관리 가능
  • Java나 Spring에서는 다양한 스케줄링 클래스들이 존재
    • Timer
    • ScheduledExecitorService

Spring에서도 @Scheduled를 활용해서 스케줄링을 실행시킬 수 있다.

 

하지만 내가 최종적으로 원한 것은 작업의 상태를 DB나 특정 저장소에서 작업 스케줄을 관리하고 싶었다. 물론 @Scheduled를 사용해서도 시작전에 작업을 저장하고 할 수 있지만 짧은 개발 기간을 고려하면 시간을 너무 잡아먹을 거 같았다.

 

2 Quartz

 

Quartz의 주요 기능

기능 설명
작업 스케줄링 지정된 시간 간격 또는 특정 시간에 작업 예약
작업 실행 및 관리 작업 실행 횟수, 마지막 실행 시간, 다음 실행 시간 등 작업에 대한 메타데이터를 추적
작업 중단 및 재개 특정 이벤트나 조건에 따라 작업을 멈추고 필요에 따라 다시 시작
병렬 처리 멀티스레드 환경을 지원하여 동시에 여러 작업을 병렬로 실행
결과 처리 작업의 실행 이력을 관리하고, 이후 로직이나 오류 처리를 효율적 관리

 

쿼츠에서 주로 사용하는 클래스와 객체들은 아래와 같다.

 

클래스 및 인터페이스

용어 설명
Job 실행할 작업을 정의하는 인터페이스 특정 작업 클래스를 정의하고 해당 인터페이스를 상속 받아 실행 내용을 정의.
ex) class Customjob implements Job
JobDetail Job 인스턴스에 대한 메타데이터를 저장하는 클래스
Trigger 작업의 실행 시점을 결정하는 스케줄링 조건을 정의하는 인터페이스
SimpleTrigger 고정된 간격이나 지연 시간에 따라 작업을 실행시키는 Trigger
CronTigger Cron 표현식을 사용한 스케줄링 트리거
JobDataMap Job 인스턴스가 실행할 때 사용할 수 있는 정보를 담을 수 있는 객체 
JobDetail 생성시에 같이 생성하여 설정할 수 있다.
Scheduler JobDetil과 Trigger를 시스템에 등록하고 스케줄에 맞춰 Job을 실생시키는 객체
SchedulerFactory Scheduler 인스턴스를 생성하는 팩토리 객체
Lisetenr 스케줄러의 이벤트를 받을 수 있는 인터페이스이며 JobListener와 TriggerListener를 제공한다.

 

대강적으로 큰그림을 보면 아래와 같은 구조로 구성된다고 생각하면 될거 같다.

Spring Boot Quartz 

간단한 스케줄러를 하나 만들어보자 일단 의존성을 추가해준다.

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-quartz'
}

 

 

1. Job 정의

  • PersistJobDataAfterExecution - 해당 Job이 실행되고 나서 JobDataMap 안의 값을 유지하게 해줌
    • Job 인스턴스는 작업이 끝나면 인스턴스는 사라진다.
    • 즉 해당 작업 안에서 JobDataMap의 변경 사항을 유지하도록 하는 역할
    • Job을 수행하다가 실패했을 때 실패 전에 값을 변경했다면 반영됨
  • DisallowConcurrentExecution - 동일한 Job이 동시에 여러 스레드에서 중복 실행되는 것을 방지
@Slf4j
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class NewProductJob implements Job {
    public static String FIRST_KEY = "job1";
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        JobDataMap map = jobExecutionContext.getJobDetail().getJobDataMap();
        int tmpIdx = map.getInt(FIRST_KEY);
        log.info("One : {} 번째 스케줄러 실행", tmpIdx);
        map.put(FIRST_KEY, tmpIdx + 1);
        try {
            Thread.sleep(15000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        log.info("One : {} 번째 스케줄러 완료", tmpIdx);
    }
}

 

로직을 간단히 설명하면

  • JobDataMap에서 "job1"의 값을 읽어옴
  • log 기록
  • job1 값 증가
  • sleep 15초
    • sleep을 수행하는 이유는 밑에서 자세히 설명하겠슴다
더보기

참고 Job vs QuartzJobBean

spring quartz 라이브러리에는 QuartzJobBean이라는 추상 클래스가 존재한다. QuartzJobBean 내부적으로 Job을 상속받아서 구현해놓았는데 차이점은 아래와 같다.

 

Job을 사용해야 할 때:

  • Spring 없이 Quartz를 사용하는 경우.
  • Spring 환경이 필요 없는 간단한 Quartz Job을 작성할 때.

QuartzJobBean을 사용해야 할 때:

  • Spring의 DI를 통해 빈을 주입받아야 하는 경우.
  • Spring 관리 하에서 Quartz를 사용하는 경우.

2. JobDetail 및 Trigger 설정

@Slf4j
@Configuration
public class QuartzConfig {

    @Bean
    public JobDetail findNewProduct() {
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put(NewProductJob.FIRST_KEY, 1);
        return JobBuilder.newJob(NewProductJob.class)
                .withIdentity("newProductJob")
                .setJobData(jobDataMap)
                .storeDurably()
                .build();
    }

    @Bean
    public Trigger findNewProductTrigger(JobDetail findNewProduct) {
        return TriggerBuilder.newTrigger()
                .withIdentity("newProductTrigger")
                .forJob(findNewProduct)
                .startNow()
                .withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?"))
                .build();
    }
}
  • Bean으로 등록해서 하는 방식은 위 코드처럼 등록하면 된다
  • 이때 storeDualy()은 트리거 없이도 스케줄러에 등록할 수 있도록 보장해주는 옵션이다
  • Bean으로 JobDetail을 등록 할 때 해당 설정 없이 등록한다면 스케줄러에 등록될 때 Trigger 설정 없이 등록되어  아래와 같은 에러가 남
Jobs added with no trigger must be durable.

 

만약 해당 설정 없이 등록하고 싶다면 아래처럼 스케줄러를 DI 받아 직접 등록해주면 된다

@Slf4j
@Configuration
public class QuartzConfig {
    private final Scheduler scheduler;

    public QuartzConfig(Scheduler scheduler) {
        this.scheduler = scheduler;
    }
    
   	@PostConstruct
    public void scheduleNewProductJob() throws SchedulerException {
        // JobDetail 생성
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put(NewProductJob.FIRST_KEY, 1);

        JobDetail jobDetail = JobBuilder.newJob(NewProductJob.class)
                .withIdentity("newProductJob")
                .setJobData(jobDataMap)
                .build();

        // Trigger 생성
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("newProductTrigger")
                .startNow()
                .withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?"))
                .forJob(jobDetail)
                .build();

        // Scheduler에 JobDetail과 Trigger 추가
        scheduler.scheduleJob(jobDetail, trigger);
    }

}

 

  • Cron 스케줄러로 매 5초 단위로 Job 실행

3. sleep 15초를 수행한 이유

시작은 만약 1분 간격으로 상품 데이터를 읽어왔는데 1만건의 데이터를 읽어올 경우 로직을 수행하다 보면 작업이 밀리는 상황이 발생했을 때 어떻게 되는지 궁금했다. 

  • 우선 비슷한 상황을 가정해보기 위해 15초 sleep()을 걸고 스케줄러는 5초마다 실행
  • 스케줄러의 스레드풀의 기본 값이 10이므로 작업이 밀려도 스레드 풀에 여유가 있다면 5초마다 작업 진행
    • 즉 5초에 시작한 작업 밀림(스레드 1개 사용)
    • 10초 정상 실행
    • 15초 정상 실행
    • 이런식으로 쭉 실행 됨 
  • 그렇다면 2초 간격으로 스케줄을 설정하고 10개의 스레드를 다 쓰고 있다면?
    • JobStore에 저장된 작업 리스트에서 작업이 끝나면 바로 밀린 작업 시작

결과를 보면 중간에 "2번째 실행"이 처음으로 나오는 부분을 보면 작업이 끝나면 바로 밀린 작업을 실행하는 것을 볼 수 있다. 

2024-10-31T14:40:38.005+09:00  INFO 2212 --- [eduler_Worker-1] c.s.s.g.schedule.job.NewProductJob       : One : 1 번째 실행
2024-10-31T14:40:40.000+09:00  INFO 2212 --- [eduler_Worker-2] c.s.s.g.schedule.job.NewProductJob       : One : 1 번째 실행
2024-10-31T14:40:42.010+09:00  INFO 2212 --- [eduler_Worker-3] c.s.s.g.schedule.job.NewProductJob       : One : 1 번째 실행
2024-10-31T14:40:44.004+09:00  INFO 2212 --- [eduler_Worker-4] c.s.s.g.schedule.job.NewProductJob       : One : 1 번째 실행
2024-10-31T14:40:46.000+09:00  INFO 2212 --- [eduler_Worker-5] c.s.s.g.schedule.job.NewProductJob       : One : 1 번째 실행
2024-10-31T14:40:48.007+09:00  INFO 2212 --- [eduler_Worker-6] c.s.s.g.schedule.job.NewProductJob       : One : 1 번째 실행
2024-10-31T14:40:50.001+09:00  INFO 2212 --- [eduler_Worker-7] c.s.s.g.schedule.job.NewProductJob       : One : 1 번째 실행
2024-10-31T14:40:52.001+09:00  INFO 2212 --- [eduler_Worker-8] c.s.s.g.schedule.job.NewProductJob       : One : 1 번째 실행
2024-10-31T14:40:53.005+09:00  INFO 2212 --- [eduler_Worker-1] c.s.s.g.schedule.job.NewProductJob       : One : 1 번째 완료
2024-10-31T14:40:54.002+09:00  INFO 2212 --- [eduler_Worker-9] c.s.s.g.schedule.job.NewProductJob       : One : 2 번째 실행
2024-10-31T14:40:55.014+09:00  INFO 2212 --- [eduler_Worker-2] c.s.s.g.schedule.job.NewProductJob       : One : 1 번째 완료
2024-10-31T14:40:56.001+09:00  INFO 2212 --- [duler_Worker-10] c.s.s.g.schedule.job.NewProductJob       : One : 2 번째 실행
2024-10-31T14:40:57.024+09:00  INFO 2212 --- [eduler_Worker-3] c.s.s.g.schedule.job.NewProductJob       : One : 1 번째 완료
2024-10-31T14:40:58.001+09:00  INFO 2212 --- [eduler_Worker-1] c.s.s.g.schedule.job.NewProductJob       : One : 2 번째 실행
2024-10-31T14:40:59.006+09:00  INFO 2212 --- [eduler_Worker-4] c.s.s.g.schedule.job.NewProductJob       : One : 1 번째 완료
2024-10-31T14:41:00.000+09:00  INFO 2212 --- [eduler_Worker-2] c.s.s.g.schedule.job.NewProductJob       : One : 2 번째 실행
2024-10-31T14:41:01.007+09:00  INFO 2212 --- [eduler_Worker-5] c.s.s.g.schedule.job.NewProductJob       : One : 1 번째 완료
2024-10-31T14:41:02.012+09:00  INFO 2212 --- [eduler_Worker-3] c.s.s.g.schedule.job.NewProductJob       : One : 2 번째 실행
2024-10-31T14:41:03.009+09:00  INFO 2212 --- [eduler_Worker-6] c.s.s.g.schedule.job.NewProductJob       : One : 1 번째 완료
2024-10-31T14:41:04.001+09:00  INFO 2212 --- [eduler_Worker-4] c.s.s.g.schedule.job.NewProductJob       : One : 2 번째 실행

 

 

  • 어...근데 만약 특정 작업은 순차적으로 진행되어야 한다면?
    • 이럴 경우 DisallowConcurrentExecution 해당 설정을 통해 해당 작업이 진행되고 있다면 다른 스레드가 수행하지 못하도록 할 수 있다.

 

 

2. Quartz 도입기 2


참고

https://advenoh.tistory.com/52

https://kouzie.github.io/springboot/Spring-Boot-Quartz/#spring-boot-quartz-%EC%84%A4%EC%A0%95

https://adjh54.tistory.com/170

'개발 > spring boot' 카테고리의 다른 글

전략 패턴과 팩토리 메소드 패턴 리팩토링  (2) 2025.01.03
[Spring] Quartz 도입기 2  (0) 2024.11.21
[Spring] IoC, DI  (0) 2024.04.24
[Spring]BeanFactory와 ApplicationContext  (0) 2023.08.09
[Spring]관심사의 분리  (0) 2023.08.07