Quartz로 스케쥴 작업

DDEO._.NU·2025년 7월 24일

Spring

목록 보기
5/5

프로젝트를 진행하면서 메시지를 예약발송 하는 기능을 구현하기 위해 Quartz를 사용하여 기능을 구현했습니다.

Quartz는 스케줄링 라이브러리로, 일정한 시간에 특정 작업(Job)을 반복해서 실행하거나 지정된 시점에 단발성 작업을 처리할 수 있게 해 줍니다.

pom.xml

라이브러리이기 때문에 의존성을 받아주어야 합니다.

<dependency>
  <groupId>org.quartz-scheduler</groupId>
  <artifactId>quartz</artifactId>
  <version>2.3.2</version>
</dependency>

QuartzJobSetup.java

해당 클래스 내부에서 서버 최초 실행 시 예약 발송 데이터 리스트를 조회한 후 Job에 등록하는 작업을 진행합니다.

@Component
public class QuartzJobSetup {
    /**
     * 스프링에서 관리하는 Scheduler를 사용해야
     * Job에서도 스프링에서 관리하는 의존성을 주입받을 수 있음
     */
    @Autowired
    private Scheduler scheduler;

    /**
     * 서버 재시작 시 Job 실행
     * @throws SchedulerException
     */
    @PostConstruct //스프링 빈 초기화 후 실행하기 위해 선언
    public void settingJobInit() throws SchedulerException, ParseException {

        // 1. DB에서 발송 안된 예약 문자 리스트 조회
        /* 필요한 로직 작성 */

        for(리스트 순회) {
            // 2. 발송이 안된 문자의 예약 발송 시간
            // Trigger는 Date타입을 받기 때문에 일치하지 않으면 타입 변환 필요
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date runTime = 발송 예약 시간;

            // 발송 시간이 지난 경우 발송 처리
            if (runTime.before(new Date())) {
                /* 즉시 발송헤 필요한 로직 작성 */
                // 해당 건은 job에 등록 x
                continue;
            }

			// 고유값을 사용하여 Job의 구분을 정한다.
            String jobId = 고유값;

            // 3. 해당 발송건에 대해서 Job이 이미 등록되어 있다면 패스
            JobKey jobKey = new JobKey("sendSmsJob"+jobId, "group1");
            if(!scheduler.checkExists(jobKey)){
                // 4. JobDetail 정의: 실행할 Job 클래스와 Job의 고유한 ID를 설정
                JobDetail job = JobBuilder.newJob(AdminEduSmsJob.class)
                        .withIdentity("sendSmsJob"+jobId, "group1") // Job의 이름과 그룹 지정
                        .usingJobData(new JobDataMap(Collections.singletonMap("smsObject", 저장할객체)))
                        .build();

                // 5. Trigger 정의: Job이 언제 실행될지 설정 (SimpleTrigger 사용)
                SimpleTrigger trigger = TriggerBuilder.newTrigger()
                        .withIdentity("sendSmsTrigger"+jobId, "group1") // Trigger의 이름과 그룹 지정
                        .startAt(runTime) // 시간 설정
                        .withSchedule(SimpleScheduleBuilder.simpleSchedule().withMisfireHandlingInstructionFireNow()) // 지연 발생 시 즉시 실행
                        .build();

                // 6. Scheduler에 Job과 Trigger 등록
                scheduler.scheduleJob(job, trigger);
            }

        }
    }

설명

  1. 발송이 안된 문자 목록을 조회
  2. 발송시간이 지난 문자의 경우 즉시 발송 처리
  3. 이미 Job에 등록된 문자라면 패스
  4. 문자의 고유 값으로 Job에 등록
  5. 발송 시간을 Trigger로 설정
  6. 설정한 Job과 Trigger로 스케쥴에 등록

QuartzJob.java

@Slf4j
@Component
public class QuartzJob implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        log.info("--- Quartz Job 실행 시작 ---");

        // 1. 발송할 메시지 오브젝트
        // usingJobData를 통해 저장한 객체
        @SuppressWarnings("unchecked")
        EgovMap smsObject = (EgovMap) context.getJobDetail()
                .getJobDataMap()
                .get("smsObject");

        // 2. 오브젝트를 토대로 메시지 발송
        /* 발송 로직 작성 */
        
        
        log.info("--- Quartz Job 실행 종료 ---");
    }
}

설명

  1. Trigger에서 설정한 시간이 되면 execute메소드가 실행됨
  2. QuartzJobSetup 에서 Job에 등록한 메시지 객체를 꺼내기
  3. 해당 객체를 토대로 메시지 발송 코드 작성

Quartz 스케줄러는 Job을 실행할 때, JobFactory를 사용하여 Job 클래스의 새로운 인스턴스를 생성합니다.
해당 작업은 스프링 컨테이너 밖에서 일어나기 때문에 스프링 의존성을 주입받지 않습니다.

스프링에서 관리되는 클래스를 전역변수로 선언하기 위해서는 해당 클래스 또한 스프링 빈으로 관리되도록 설정해야 합니다.


QuartzSchedulerConfig.java

@Configuration
public class QuartzSchedulerConfig {

    // 스프링 컨텍스트를 주입받음
    @Autowired
    private ApplicationContext applicationContext;

    /**
     * SpringBeanJobFactory를 빈으로 등록.
     * Quartz Job이 스프링 컨테이너의 빈을 주입받을 수 있도록 합니다.
     * @return JobFactory
     */
    @Bean
    public JobFactory springBeanJobFactory() {
        // 커스텀 SpringBeanJobFactory를 생성하여 ApplicationContext를 설정합니다.
        // 이를 통해 Job 인스턴스가 생성될 때 스프링 빈을 주입받을 수 있게 됩니다.
        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        jobFactory.setApplicationContext(applicationContext);
        return jobFactory;
    }

    /**
     * SchedulerFactoryBean을 빈으로 등록.
     * Quartz 스케줄러를 스프링에서 관리하고 초기화합니다.
     * @return SchedulerFactoryBean
     * @throws Exception
     */
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() throws Exception {
        SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();

        // 1. JobFactory 설정: 위에서 정의한 springBeanJobFactory를 주입합니다.
        schedulerFactory.setJobFactory(springBeanJobFactory());

        // 2. Quartz 설정 파일 지정 (선택 사항, 기본값 사용 가능)
        // schedulerFactory.setConfigLocation(new ClassPathResource("quartz.properties"));

        // 3. 데이터베이스 기반 스케줄링 시 DataSource 설정 (선택 사항)
        // schedulerFactory.setDataSource(dataSource);

        // 4. Job 자동 실행 방지 (애플리케이션 시작 시 Job이 바로 실행되지 않도록 설정)
        schedulerFactory.setOverwriteExistingJobs(true); // 기존 Job 덮어쓰기 여부
        schedulerFactory.setAutoStartup(true); // 스케줄러 자동 시작 여부 -- 스케줄러는 다른곳에서 또 start할 경우 두번 실행될 수 있음

        return schedulerFactory;
    }

}

설명

해당 클래스는 Job 구현 클래스가 스프링에서 관리되도록 하기 위해 작성된 Job 설정 클래스 파일입니다.


AutowiringSpringBeanJobFactory.java

public final class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {

    private transient ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(final ApplicationContext context) throws BeansException {
        this.applicationContext = context;
    }

    @Override
    protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
        final Object job = super.createJobInstance(bundle);
        // Job 인스턴스가 생성된 후 스프링 컨텍스트를 사용하여 의존성 주입을 수행합니다.
        applicationContext.getAutowireCapableBeanFactory().autowireBean(job);
        return job;
    }
}

설명

스프링 컨텍스틑 통해 의존성을 주입하기 위해 작성된 커스텀 SpringBeanJobFactory 클래스입니다.


문제 발생

⚠️주의사항
applicationContext를 초기화하는 부분이 여러곳 존재할 경우 스케쥴을 한번 등록했어도 여러번 호출되어 뜻하지 않게 작업에 여러번 진행될 수 있습니다.

ContextLoaderListenerdispatcherServlet에서 각각 스프링 빈을 등록하는 과정에서 QuartzSchedulerConfig가 빈으로 두번 등록되어 메시지 발송 작업이 두번 진행되는 문제가 생겼습니다.

dispatcherServlet에서 설정한 기본 패키지를 Quartz설정 파일이 있는 패키지를 포함하게 작성 했다 보니 ContextLoaderListener에서 루트 웹 애플리케이션 컨텍스트를 초기화 한 후 dispatcherServlet에서 루트 및 자식 애플리케이션 컨텍스트를 초기화 할 때 한번더 초기화를 하게 되었습니다.

문제 해결

<context:component-scan base-package="기본패키지경로">
  <!-- 클래스 파일의 경로를 설정 -->
  <context:exclude-filter type="assignable" expression="QuartzSchedulerConfig 클래스 파일의 경로"/>
  
  <!-- 기본패키지경로 하위에 존재하는 @Configuration설정 파일 제외 -->
  <context:exclude-filter type="annotation" expression="org.springframework.context.annotation.Configuration"/>
</context:component-scan>

dispatcherServlet의 컴포넌트를 스캔하는 범위에서 두 가지 선택지를 통해 스프링 빈 초기화 과정을 생략할 수 있습니다.

0개의 댓글