From 0da81a7471eb8ed6e72808d02644acc600585c4a Mon Sep 17 00:00:00 2001 From: hyojin kim Date: Fri, 27 Feb 2026 10:17:57 +0900 Subject: [PATCH] fix(batch): orphan trigger remove --- .../snp/batch/global/config/QuartzConfig.java | 4 +- .../batch/scheduler/SchedulerInitializer.java | 62 +++++++++++++++++-- .../snp/batch/service/ScheduleService.java | 33 +++++----- 3 files changed, 76 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/snp/batch/global/config/QuartzConfig.java b/src/main/java/com/snp/batch/global/config/QuartzConfig.java index aced683..15948e2 100644 --- a/src/main/java/com/snp/batch/global/config/QuartzConfig.java +++ b/src/main/java/com/snp/batch/global/config/QuartzConfig.java @@ -29,7 +29,9 @@ public class QuartzConfig { SchedulerFactoryBean factory = new SchedulerFactoryBean(); factory.setJobFactory(springBeanJobFactory(applicationContext)); factory.setOverwriteExistingJobs(true); - factory.setAutoStartup(true); + // SchedulerInitializer에서 직접 start() 호출하므로 자동 시작 비활성화 + // 자동 시작 시 JDBC Store의 기존 trigger가 로드되어 중복 실행 발생 가능 + factory.setAutoStartup(false); // DataSource는 Spring Boot가 자동 주입 (application.yml의 spring.datasource 사용) return factory; diff --git a/src/main/java/com/snp/batch/scheduler/SchedulerInitializer.java b/src/main/java/com/snp/batch/scheduler/SchedulerInitializer.java index 66c011f..6b21cf7 100644 --- a/src/main/java/com/snp/batch/scheduler/SchedulerInitializer.java +++ b/src/main/java/com/snp/batch/scheduler/SchedulerInitializer.java @@ -10,6 +10,7 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import java.util.List; +import java.util.Set; /** * 애플리케이션 시작 시 DB에 저장된 스케줄을 Quartz에 자동 로드 @@ -34,11 +35,15 @@ public class SchedulerInitializer { log.info("========================================"); try { + // 기존 orphan trigger 전체 정리 (이전 실행에서 남은 잔여 trigger 제거) + cleanupOrphanTriggers(); + // DB에서 활성화된 스케줄 조회 List activeSchedules = scheduleRepository.findAllActive(); if (activeSchedules.isEmpty()) { log.info("활성화된 스케줄이 없습니다."); + startSchedulerIfNeeded(); return; } @@ -67,10 +72,7 @@ public class SchedulerInitializer { log.info("========================================"); // Quartz 스케줄러 시작 - if (!scheduler.isStarted()) { - scheduler.start(); - log.info("Quartz 스케줄러 시작됨"); - } + startSchedulerIfNeeded(); } catch (Exception e) { log.error("스케줄러 초기화 중 에러 발생", e); @@ -79,6 +81,7 @@ public class SchedulerInitializer { /** * 개별 스케줄을 Quartz에 등록 + * Trigger를 먼저 명시적으로 제거한 후 Job을 삭제하여 orphan trigger 방지 * * @param schedule JobScheduleEntity * @throws SchedulerException Quartz 스케줄러 예외 @@ -88,7 +91,13 @@ public class SchedulerInitializer { JobKey jobKey = new JobKey(jobName, "batch-jobs"); TriggerKey triggerKey = new TriggerKey(jobName + "-trigger", "batch-triggers"); - // 기존 스케줄 확인 및 삭제 + // 1. 기존 Trigger 명시적 제거 (orphan trigger 방지) + if (scheduler.checkExists(triggerKey)) { + scheduler.unscheduleJob(triggerKey); + log.debug("기존 Quartz Trigger 제거: {}", triggerKey); + } + + // 2. 기존 Job 삭제 (연결된 trigger도 함께 삭제됨) if (scheduler.checkExists(jobKey)) { scheduler.deleteJob(jobKey); log.debug("기존 Quartz Job 삭제: {}", jobName); @@ -118,4 +127,47 @@ public class SchedulerInitializer { log.debug(" → 다음 실행 예정: {}", trigger.getNextFireTime()); } } + + /** + * Quartz 스케줄러가 아직 시작되지 않았으면 시작 + */ + private void startSchedulerIfNeeded() throws SchedulerException { + if (!scheduler.isStarted()) { + scheduler.start(); + log.info("Quartz 스케줄러 시작됨"); + } + } + + /** + * 앱 시작 시 batch-triggers 그룹의 모든 기존 trigger와 batch-jobs 그룹의 모든 기존 job을 제거 + * JDBC Store에 잔존하는 orphan trigger로 인한 중복 실행 방지 + */ + private void cleanupOrphanTriggers() throws SchedulerException { + // batch-triggers 그룹의 모든 trigger 제거 + Set triggerKeys = scheduler.getTriggerKeys( + org.quartz.impl.matchers.GroupMatcher.triggerGroupEquals("batch-triggers")); + if (!triggerKeys.isEmpty()) { + log.info("기존 trigger {} 개 정리 시작 (batch-triggers 그룹)", triggerKeys.size()); + for (TriggerKey tk : triggerKeys) { + scheduler.unscheduleJob(tk); + log.debug(" Trigger 제거: {}", tk); + } + } + + // batch-jobs 그룹의 모든 job 제거 + Set jobKeys = scheduler.getJobKeys( + org.quartz.impl.matchers.GroupMatcher.jobGroupEquals("batch-jobs")); + if (!jobKeys.isEmpty()) { + log.info("기존 job {} 개 정리 시작 (batch-jobs 그룹)", jobKeys.size()); + for (JobKey jk : jobKeys) { + scheduler.deleteJob(jk); + log.debug(" Job 제거: {}", jk); + } + } + + if (!triggerKeys.isEmpty() || !jobKeys.isEmpty()) { + log.info("기존 스케줄 정리 완료: trigger {} 개, job {} 개 제거", + triggerKeys.size(), jobKeys.size()); + } + } } diff --git a/src/main/java/com/snp/batch/service/ScheduleService.java b/src/main/java/com/snp/batch/service/ScheduleService.java index 47115c7..c19fa38 100644 --- a/src/main/java/com/snp/batch/service/ScheduleService.java +++ b/src/main/java/com/snp/batch/service/ScheduleService.java @@ -253,6 +253,7 @@ public class ScheduleService { /** * Quartz에 Job 등록 + * Trigger → Job 순서로 명시적 제거 후 새로 등록하여 orphan trigger 방지 * * @param entity JobScheduleEntity * @throws SchedulerException Quartz 스케줄러 예외 @@ -262,6 +263,18 @@ public class ScheduleService { JobKey jobKey = new JobKey(jobName, "batch-jobs"); TriggerKey triggerKey = new TriggerKey(jobName + "-trigger", "batch-triggers"); + // 1. 기존 Trigger 명시적 제거 (orphan trigger 방지) + if (scheduler.checkExists(triggerKey)) { + scheduler.unscheduleJob(triggerKey); + log.debug("기존 Quartz Trigger 제거: {}", triggerKey); + } + + // 2. 기존 Job 삭제 (연결된 trigger도 함께 삭제됨) + if (scheduler.checkExists(jobKey)) { + scheduler.deleteJob(jobKey); + log.debug("기존 Quartz Job 삭제: {}", jobName); + } + // JobDetail 생성 JobDetail jobDetail = JobBuilder.newJob(QuartzBatchJob.class) .withIdentity(jobKey) @@ -277,23 +290,9 @@ public class ScheduleService { .forJob(jobKey) .build(); - // 기존 Job 삭제 후 등록 - try { - scheduler.deleteJob(jobKey); - } catch (Exception e) { - log.debug("기존 Job 삭제 시도: {}", jobName); - } - - // Job 등록 - try { - scheduler.scheduleJob(jobDetail, trigger); - log.info("Quartz에 스케줄 등록 완료: {} (Cron: {})", jobName, entity.getCronExpression()); - } catch (ObjectAlreadyExistsException e) { - log.warn("Job이 이미 존재함, 재시도: {}", jobName); - scheduler.deleteJob(jobKey); - scheduler.scheduleJob(jobDetail, trigger); - log.info("Quartz에 스케줄 재등록 완료: {} (Cron: {})", jobName, entity.getCronExpression()); - } + // Quartz에 스케줄 등록 + scheduler.scheduleJob(jobDetail, trigger); + log.info("Quartz에 스케줄 등록 완료: {} (Cron: {})", jobName, entity.getCronExpression()); } /** -- 2.45.2