안녕하세요!
오늘은 SSAFY 프로젝트 중 Spring Batch로 야심 차게 만든 스케줄러 잡(Job)을 개발 중에 어떻게 하면 바로바로 테스트할 수 있을까 고민했던 경험과 그 해결 방법을 공유하려고 합니다.
매주 월요일 오전 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에는 애플리케이션 시작 시점에 특정 로직을 실행할 수 있는 몇 가지 방법이 있습니다. CommandLineRunner
와 ApplicationListener<ApplicationReadyEvent>
가 대표적이죠. 저는 후자, 즉 애플리케이션이 완전히 준비된 후에 실행되는 ApplicationListener<ApplicationReadyEvent>
를 사용하기로 했습니다. 이게 왠지 더 안정적인 느낌이 들었거든요.
그래서 다음과 같이 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>
구현이 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 스케줄러 관련 작업을 할 때는 이 방법을 적극 활용해서, 답답함 없이 더 효율적으로 개발을 진행할 수 있을 것 같습니다. 역시 개발 중 피드백 루프를 단축하는 방법을 찾는 것이 중요하다는 것을 다시 한번 깨닫는 계기가 되었네요. 😊