@SchedulingConfigurer를 이용한 배치 시스템의 운영화
개요
Spring에서는 @Scheduled 애노테이션 하나만으로 간단하게 배치 작업을 등록할 수 있다.
하지만 실제 운영 환경에서는 단순한 애노테이션 기반 스케줄링으로는 한계가 명확하다.
이 글에서는 @Scheduled의 구조적 한계에 대해 알아보고, 이를 극복하기 위해 @SchedulingConfigurer 기반의 DB 메타 중심 배치에 대해 정리하려고 한다.
@Scheduled의 한계
@Scheduled는 간단한 테스트나 단일 노드 환경에는 적합하지만, 복잡한 실무 환경에서는 운영 안정성을 확보하기 어렵다.
동적 제어 불가 | 실행 중인 배치를 중지하거나 갱신할 수 없다 |
상태 추적 어려움 | 언제 실행됐는지, 다음 실행은 언제인지 외부에서 알 수 없다 |
운영툴 연동 어려움 | 모니터링 시스템에서 실행 상태 조회/변경이 어렵다 |
스케줄 조건 유연성 부족 | 특정 조건(@Lock, @Cond 등)을 기반으로 실행 여부 결정이 어렵다 |
클러스터 환경 제어 어려움 | 다중 WAS 환경에서 배치 중복 실행을 제어하기 어렵다 |
@SchedulingConfigurer
SchedulingConfigurer는 Spring이 제공하는 스케줄링 설정 인터페이스로, 기본적인 @Scheduled 방식에서 벗어나 스케줄 실행 구조를 커스터마이징할 수 있게 해준다.
public interface SchedulingConfigurer {
void configureTasks(ScheduledTaskRegistrar taskRegistrar);
}
이 인터페이스를 구현하면 Spring이 @Scheduled로 등록한 작업들을 직접 다루거나 스레드풀/실행 조건 등을 설정할 수 있다.즉, 스케줄링의 실행 전략을 제어할 수 있게 된다.
실제 구현 예시
실행모드는 yml의 설정값 등으로 결정한다.
public class BatchConfig implements SchedulingConfigurer {
...
@Override
@Transactional
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
List<FixedDelayTask> fixedDelayTasks = taskRegistrar.getFixedDelayTaskList();
for (FixedDelayTask task : fixedDelayTasks) {
String className = extractClass(task);
String methodName = extractMethod(task);
Method method = Class.forName(className).getMethod(methodName);
BatchCond cond = method.getAnnotation(BatchCond.class);
SchedulerLock lock = method.getAnnotation(SchedulerLock.class);
long interval = task.getInterval();
LocalDateTime now = LocalDateTime.now();
for (int i = 0; i < 하루치반복; i++) {
LocalDateTime nextRun = now.plusSeconds(interval / 1000 * i);
saveSchedule(className, methodName, nextRun, cond, lock);
}
}
if (isDbMode()) {
taskRegistrar.setFixedDelayTasksList(null);
}
batchTaskScheduler.initialize();
}
...
}
@Service
public class BatchTaskScheduler {
private final CmmBatchJobScheDAO batchJobScheDAO;
private final ApplicationContext context;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
private final Map<Long, ScheduledFuture<?>> scheduledTaskMap = new ConcurrentHashMap<>();
public void initialize() {
// DB에서 오늘 기준 활성화된 배치 스케줄 불러오기
List<CmmBatchJobScheBean> jobs = batchJobScheDAO.findActiveJobsForToday();
for (CmmBatchJobScheBean job : jobs) {
schedule(job);
}
}
public void schedule(CmmBatchJobScheBean job) {
// Runnable 생성 (리플렉션 기반 메서드 호출)
Runnable task = () -> {
try {
Object bean = context.getBean(Class.forName(job.getClassName()));
Method method = bean.getClass().getMethod(job.getMethodName());
method.invoke(bean);
} catch (Exception e) {
log.error("배치 실행 오류: {}", e.getMessage(), e);
}
};
// 스케줄러에 등록
LocalDateTime now = LocalDateTime.now();
Duration delay = Duration.between(now, job.getNextRunTime());
if (delay.isNegative()) return;
ScheduledFuture<?> future = scheduler.schedule(task, delay.toMillis(), TimeUnit.MILLISECONDS);
scheduledTaskMap.put(job.getJobId(), future);
}
public void cancel(Long jobId) {
ScheduledFuture<?> future = scheduledTaskMap.get(jobId);
if (future != null) {
future.cancel(true);
scheduledTaskMap.remove(jobId);
}
}
}
핵심 흐름 요약
@SchedulingConfigurer를 이용하여 Spring Boot WAS가 기동되면 다음과 같은 순서로 배치 메타가 처리되게 할 수 있다.
WAS 기동 시
├── @Scheduled 어노테이션 파싱
│ ├── fixedDelayTaskList / cronTaskList 추출
│ ├── 클래스, 메서드 이름 추출
│ ├── 리플렉션으로 커스텀 어노테이션 추출 (@BatchCond, @SchedulerLock 등)
│ ├── 다음 실행 시간 계산 (fixedDelay → 간격, cron → CronExpression)
│ └── DB에 실행 계획 및 메타 정보 저장
│
├── 실행모드가 DB이면:
│ ├── taskRegistrar.setFixedDelayTasksList(null)로 @Scheduled 무력화
│ ├── 직접 실행 관리 (예: Quartz, Redis Lock 기반 커스텀 스케줄러)
│
└── 배치 스케줄러 initialize() 호출로 DB에 저장된 배치 실행
WAS 기동 시점에 모든 배치 실행 정보를 동기화하고, 필요하다면 @Scheduled 자체를 무력화할 수 있다
.
따라서 시스템 다음과 같은 방식으로 동작할 수 있게 구현가능하다.
- ScheduledTaskRegistrar로부터 모든 @Scheduled 작업(fixedDelay, cron)을 가져온다.
- 각 작업의 클래스명, 메서드명을 추출한다.
(ex: com.example.batch.MyJob.runJob) - 리플렉션으로 해당 메서드에서 커스텀 어노테이션(@BatchCond, @SchedulerLock)을 추출한다.
- 실행 간격(fixedDelay), 또는 실행 스케줄(cron)을 기준으로
하루치 실행 시각을 계산해 DB에 저장한다. - 설정값에 따라 @Scheduled를 비활성화하거나, 일부 작업만 유지한다.
이 모든 로직은 SchedulingConfigurer를 구현한 설정 클래스 안에서 이루어진다.
주요 기능 요약
정적 분석 | @Scheduled 메서드 분석 및 리플렉션 기반 어노테이션 추출 |
DB 메타 저장 | 실행 시간, 클래스/메서드, 조건 등을 DB 테이블에 저장 |
실행 조건 반영 | @BatchCond 조건과 @SchedulerLock의 락 시간 정보를 함께 저장 |
스케줄 무효화 제어 | 설정에 따라 Spring 내장 스케줄러를 제거하고 외부 제어로 전환 가능 |
예외 배치 분리 관리 | 시스템 필수 배치 작업은 설정에 따라 유지 가능 (DO_NOT_TOUCH_SYSTEM_BATCH) |
이 구조의 장점
중앙 집중형 메타 관리 | 배치 정보를 모두 DB에 저장하여 운영툴, 관리자 페이지와 연동이 쉽다 |
WAS 기동 동기화 | 실행 시점마다 배치 상태를 자동 초기화 및 재등록할 수 있다 |
조건 기반 실행 제어 | 커스텀 어노테이션(@BatchCond, @SchedulerLock)으로 실행 조건을 정밀하게 제어할 수 있다 |
클러스터 환경 대응 | Redis Lock 등과 연계해 다중 WAS 환경에서 배치 중복 실행을 방지할 수 있다 |
스케줄 무효화 가능 | 필요 시 @Scheduled를 완전히 비활성화하고 DB 기반으로만 실행할 수 있다 |
Quartz 등 외부 확장 가능성 | 구조적으로 분리되어 있어 외부 스케줄러(Quartz 등)로 이관이 용이하다 |
실시간 작업 추적 가능 | 등록된 ScheduledFuture를 Map으로 관리하므로, 실행 중인 작업을 중지하거나 재등록할 수 있다 |
배치 재스케줄 및 롤백 용이 | DB 정보만 수정하면 재스케줄이 가능하며, 스케줄 롤백도 SQL 한 줄이면 된다 |
실행 가능 스케줄만 등록 | DB에 저장된 실행 시점이 현재 기준 유효할 때만 등록하여 사용할 수 있따. |
가능한 예시 시나리오
- CmmDailyReportBatch.run()에 @Scheduled(cron = "0 0 1 * * *")이 붙어 있음
- 기동 시 configureTasks()가 해당 정보를 추출하여 DB에 저장
- 설정값이 EXEC_MODE=DB일 경우 → taskRegistrar.setXxxTaskList(null)로 Spring의 자동 실행은 차단됨
- 대신 init()에서 DB에 등록된 해당 작업을 직접 읽고 schedule() 호출
- 실행 시간 계산 후 ScheduledExecutorService.schedule(...)로 직접 실행 예약
기존 @Scheduled는 선언만 해두고 운영은 DB 중심으로 통합 관리하는 구조이며,
원한다면 @Scheduled를 전혀 쓰지 않고 DB만으로도 완전한 배치 운용이 가능하다.
배치 시스템의 운영화
단순한 @Scheduled만으로는 복잡한 운영환경을 커버하기 어렵다.
그래서 @SchedulingConfigurer를 통해 스케줄링을 메타 데이터화한 후 DB 기반으로 실행 계획을 관리하는 구조로 바꿀 수 있었다. 이를 통해서
- WAS 기동 시점마다 일관된 배치 등록
- 조건 기반 실행 통제
- 배치 실행 시간의 시각화/예측
- 필요 시 @Scheduled 무효화 및 외부 연동
등이 가능해졌다.
즉 기존 @Scheduled 배치를 분석해서 DB에 저장하고, 설정에 따라 실행하거나 제외하며, 별도로 등록된 DB 기반 스케줄도 실행할 수 있다.
스프링이 등록한 스케줄러 → 메타정보 해석 → DB 기반 작업 제어 → 일부 예외 스케줄 유지 → 동적 실행 컨트럴 제공