Java

@SchedulingConfigurer를 이용한 배치 시스템의 운영화

devgenie 2025. 5. 7. 15:09

 

개요

 

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 자체를 무력화할 수 있다

.

따라서 시스템 다음과 같은 방식으로 동작할 수 있게 구현가능하다.

  1. ScheduledTaskRegistrar로부터 모든 @Scheduled 작업(fixedDelay, cron)을 가져온다.
  2. 각 작업의 클래스명, 메서드명을 추출한다.
    (ex: com.example.batch.MyJob.runJob)
  3. 리플렉션으로 해당 메서드에서 커스텀 어노테이션(@BatchCond, @SchedulerLock)을 추출한다.
  4. 실행 간격(fixedDelay), 또는 실행 스케줄(cron)을 기준으로
    하루치 실행 시각을 계산해 DB에 저장한다.
  5. 설정값에 따라 @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 기반 작업 제어 → 일부 예외 스케줄 유지 → 동적 실행 컨트럴 제공