Spring Batch 테스트 하기

yeolyeol·2025년 4월 2일
0

til

목록 보기
28/30
post-thumbnail

Spring Batch 스케줄러, 언제까지 기다려? 앱 시작 시 바로 테스트하기! 🚀

안녕하세요!
오늘은 SSAFY 프로젝트 중 Spring Batch로 야심 차게 만든 스케줄러 잡(Job)을 개발 중에 어떻게 하면 바로바로 테스트할 수 있을까 고민했던 경험과 그 해결 방법을 공유하려고 합니다.

🤔 문제 상황: "월요일 9시까지 기다려야 하나요...?"

매주 월요일 오전 9시에 사용자들에게 카드 청구서를 받아 DB에 저장하는 배치 잡을 만들었습니다. @Scheduled 어노테이션 덕분에 스케줄링 자체는 간단했죠. UtilityBatchScheduler 클래스에 슥슥 코드를 추가했습니다.

@Slf4j
@Component
@RequiredArgsConstructor
public class UtilityBatchScheduler {
    // ... JobLauncher, Job 등 주입 ...

    // 매주 월요일 오전 9시 (한국 시간) 실행
    @Scheduled(cron = "0 0 9 * * MON", zone = "Asia/Seoul")
    public void runUtilityBillingJob() {
        try {
            JobParameters jobParameters = new JobParametersBuilder()
                    .addString("JobID", String.valueOf(System.currentTimeMillis()))
                    .toJobParameters();

            log.info(">>> 스케줄러 실행: Utility Billing Job 시작. Params: {}", jobParameters);
            jobLauncher.run(utilityBillingStatementJob, jobParameters);
            log.info("<<< 스케줄러 실행: Utility Billing Job 완료.");

        } catch (Exception e) {
            log.error("!!! 스케줄러 실행 중 오류 발생: Utility Billing Job 실패", e);
        }
    }

     @Scheduled(cron = "0 0 17 * * THU", zone = "Asia/Seoul") // KST 기준 목요일 17시 0분 0초
    public void runCollectToUtilityAccountJob() {
        try {
            JobParameters jobParameters = new JobParametersBuilder()
                    .addString("JobID", String.valueOf(System.currentTimeMillis())) // 현재 시간을 파라미터로 추가
                    .toJobParameters();

            log.info(">>> 스케줄러 실행: Collect To Utility Account Job 시작. Params: {}", jobParameters);
            jobLauncher.run(collectToUtilityAccountJob, jobParameters); // Job 실행
            log.info("<<< 스케줄러 실행: Collect To Utility Account Job 완료.");
        } catch (Exception e) {
            log.error("!!! 스케줄러 실행 중 오류 발생: Collect To Utility Account Job 실패", e);
        }
    }
}

코드는 완성했는데, 이걸 어떻게 테스트하죠? 당장 다음 주 월요일 오전 9시까지 기도라고 해야 할까요? 아니면 서버 시간을 월요일 8시 59분으로 바꾸는 위험한(?) 시도를 해야 할까요?
개발 중에는 코드를 수정하고 바로 결과를 확인하는 빠른 피드백이 생명인데, 이건 너무 답답했습니다. 😫

💡 "앱 시작할 때 딱! 실행시키면 되지 않을까?"

고민 끝에 떠오른 생각은 '스프링 부트 애플리케이션이 시작될 때, 내가 원하는 잡을 딱 한 번 실행시키면 어떨까?' 였습니다. 마치 앱이 로딩되자마자 "자, 지금부터 테스트 시작!" 하고 명령하는 거죠.

Spring Boot에는 애플리케이션 시작 시점에 특정 로직을 실행할 수 있는 몇 가지 방법이 있습니다. CommandLineRunnerApplicationListener<ApplicationReadyEvent>가 대표적이죠. 저는 후자, 즉 애플리케이션이 완전히 준비된 후에 실행되는 ApplicationListener<ApplicationReadyEvent>를 사용하기로 했습니다. 이게 왠지 더 안정적인 느낌이 들었거든요.

✨ ApplicationListener로 테스트 Runner 구현하기

그래서 다음과 같이 BatchTestRunner라는 컴포넌트를 만들었습니다.

import java.time.Instant;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.Environment; // 프로파일 확인에 필요해요!
import org.springframework.scheduling.TaskScheduler; // 살짝 지연시킬 때 유용해요!
import org.springframework.stereotype.Component;

@Component // 당연히 스프링 빈으로 등록!
@RequiredArgsConstructor
@Slf4j
public class BatchTestRunner implements ApplicationListener<ApplicationReadyEvent> { // 이 인터페이스 구현!

    private final JobLauncher jobLauncher; // 잡을 실행시켜줄 친구
    private final Job utilityBillingStatementJob; // 테스트할 첫 번째 잡
    private final Job collectToUtilityAccountJob; // 테스트할 두 번째 잡
    private final TaskScheduler taskScheduler; // 바로 실행하면 좀 그러니까 살짝 딜레이를 줄까?
    private final Environment environment; // 현재 활성 프로파일 확인 (아주 중요!)

    /**
     * 애플리케이션이 준비되면 이 메소드가 호출됩니다!
     */
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        // 잠깐! 혹시 'test' 프로파일인가? 자동 테스트 중엔 실행되면 안 되지!
        if (!List.of(environment.getActiveProfiles()).contains("test")) {
            log.info("Application Ready! 잠시 후 스케줄러 잡 테스트 실행합니다... 🚀");

            // 2초 뒤에 첫 번째 잡 실행 예약
            taskScheduler.schedule(
                    () -> runSpecificJob(utilityBillingStatementJob, "utilityBillingStatementJob"),
                    Instant.now().plusSeconds(2) // 2초 딜레이
            );

            // 5초 뒤에 두 번째 잡 실행 예약 (로그 섞이지 않게 시간차를 좀 둘까요?)
            taskScheduler.schedule(
                    () -> runSpecificJob(collectToUtilityAccountJob, "collectToUtilityAccountJob"),
                    Instant.now().plusSeconds(5) // 5초 딜레이
            );

        } else {
            // 'test' 프로파일일 때는 건너뛰기! (JUnit 테스트 돌릴 때 방해 안 되게)
            log.info("'test' 프로파일 활성화됨. ApplicationReady 이벤트 잡 실행 건너뜁니다.");
        }
    }

    /**
     * 실제 잡을 실행하는 로직 (재사용 가능하게 만들었어요)
     * @param jobToRun 실행시킬 Job 객체
     * @param jobName 로그나 파라미터에 쓸 Job 이름
     */
    private void runSpecificJob(Job jobToRun, String jobName) {
        try {
            // 잡 파라미터! 매번 다르게 줘야 재실행 가능해요. 현재 시간 + 잡 이름 조합!
            JobParameters jobParameters = new JobParametersBuilder()
                    .addString("runnerJobName", jobName) // 어떤 잡인지 구분
                    .addLong("runTimeMillis", System.currentTimeMillis()) // 매번 다른 값!
                    .toJobParameters();

            log.info("[{}] 잡 테스트 실행 시작! (Params: {})", jobName, jobParameters);
            jobLauncher.run(jobToRun, jobParameters); // 자, 이제 돌아라!
            log.info("[{}] 잡 테스트 실행 완료!", jobName);

        } catch (Exception e) {
            // 앗, 에러!
            log.error("!!! [{}] 잡 테스트 실행 중 에러 발생!", jobName, e);
        }
    }
}
              

코드 설명

  • ApplicationListener<ApplicationReadyEvent> 구현
    이게 핵심입니다.
    스프링 부트가 "나 준비 끝났어!"라고 외치면 onApplicationEvent 메소드가 실행됩니다.
  • Environment로 프로파일 확인
    test 프로파일(주로 자동화된 테스트 환경)에서는 이 Runner가 동작하지 않도록 막았습니다. 실수로 테스트 중에 배치가 돌면 곤란하니까요! (if (!List.of(environment.getActiveProfiles()).contains("test")))
  • TaskScheduler로 지연 실행
    필수는 아니지만, 앱 시작 직후 너무 바쁘게 돌아가는 걸 방지하고 로그를 차분히 보기 위해 taskScheduler.schedule()로 몇 초 정도 딜레이를 주었습니다.
  • 고유한 JobParameters
    System.currentTimeMillis() 등을 이용해 매번 다른 잡 파라미터를 생성해주는 것이 중요합니다. 그래야 JobInstanceAlreadyCompleteException 같은 에러 없이 앱을 재시작할 때마다 잡을 다시 실행해볼 수 있습니다.
  • runSpecificJob 메소드 분리: 잡 실행 로직을 별도 메소드로 분리해서 코드를 깔끔하게 유지하고 재사용성을 높였습니다.

🎉 결과

이 BatchTestRunner 덕분에, 더 이상 다음 주 월요일 오전 9시를 기다릴 필요가 없어졌습니다!
로컬 환경에서 애플리케이션을 실행(dev 또는 기본 프로파일)하면, 몇 초 뒤에 BatchTestRunner가 자동으로 두 개의 배치 잡을 실행시켜 줍니다. 로그를 보면서 잡이 의도대로 잘 도는지 바로바로 확인할 수 있게 되었죠.
개발 속도가 훨씬 빨라졌습니다! 💨

❗주의할 점

이 Runner는 개발 및 테스트 목적입니다.
실제 운영 환경에서는 여전히 @Scheduled 어노테이션이 제 역할을 해야 합니다! 그리고 @Scheduled가 제대로 동작하려면 애플리케이션 어딘가에 @EnableScheduling 어노테이션이 꼭 필요하다는 것도 잊지 마세요!

// UtilityBatchScheduler.java (최종 버전)
// ...
@EnableScheduling // 메인 클래스나 설정 클래스에 이게 필요해요!
// ...

@Slf4j
@Component
@RequiredArgsConstructor
public class UtilityBatchScheduler {
    // ... JobLauncher, Job 등 주입 ...

    // 실제 운영 환경에서는 이게 동작!
    @Scheduled(cron = "0 0 9 * * MON", zone = "Asia/Seoul")
    public void runUtilityBillingJob() { /* ... */ }

    @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
    public void runCollectToUtilityAccountJob() { /* ... */ }
}

마무리

Spring Batch 스케줄러를 처음 만들었을 때, 실제 스케줄 시간까지 기다려야만 테스트할 수 있다는 점이 솔직히 조금 답답했습니다. '더 효율적인 방법은 없을까?' 고민했었죠.

이번 기회를 통해 ApplicationListener<ApplicationReadyEvent> (또는 CommandLineRunner)를 활용하면 개발 중에도 스케줄 잡을 애플리케이션 시작 시점에 즉시 실행하고 테스트할 수 있다는 것을 배우게 되었습니다. 단순히 기다리는 시간을 줄이는 것을 넘어, 코드를 수정한 후 결과를 바로바로 피드백 받을 수 있게 되니 디버깅이 훨씬 수월해지고 전체적인 개발 사이클도 눈에 띄게 빨라지는 경험을 했습니다.

앞으로 Spring Batch 스케줄러 관련 작업을 할 때는 이 방법을 적극 활용해서, 답답함 없이 더 효율적으로 개발을 진행할 수 있을 것 같습니다. 역시 개발 중 피드백 루프를 단축하는 방법을 찾는 것이 중요하다는 것을 다시 한번 깨닫는 계기가 되었네요. 😊

profile
한 걸음씩 꾸준히

0개의 댓글