배치를 잘 만드는 것과 잘 운영하는 것은 다른 문제다. 언제 실행할지, 실패하면 어떻게 대응할지, 처리량을 어떻게 모니터링할지 — 운영 관점의 설계가 배치의 안정성을 결정한다.
스케줄링
Spring Batch 자체는 스케줄러가 아니다. 언제 실행할지는 외부 스케줄러가 담당한다.
단순 주기 실행"] B["Quartz
복잡한 스케줄"] C["Jenkins
CI/CD 파이프라인"] D["K8s CronJob
컨테이너 환경"] end Schedulers --> JL["JobLauncher.run()"] JL --> Job["Spring Batch Job"] style JL fill:#E8F4F8,stroke:#2196F3
@Scheduled — 가장 간단한 방법
Spring의 @Scheduled로 주기적으로 Job을 실행한다. 단일 서버에서 간단한 배치를 돌릴 때 적합하다.
@Component
@RequiredArgsConstructor
public class BatchScheduler {
private final JobLauncher jobLauncher;
private final Job settlementJob;
@Scheduled(cron = "0 0 2 * * *") // 매일 새벽 2시
public void runSettlement() {
String targetDate = LocalDate.now().minusDays(1).toString();
JobParameters params = new JobParametersBuilder()
.addString("targetDate", targetDate)
.addLong("timestamp", System.currentTimeMillis())
.toJobParameters();
try {
jobLauncher.run(settlementJob, params);
} catch (Exception e) {
log.error("정산 배치 실행 실패", e);
// Slack 알림 등 장애 대응
}
}
}
@EnableScheduling을 설정 클래스에 추가해야 동작한다.
- 서버가 여러 대면 모든 서버에서 동시에 실행된다. 분산 환경에서는 ShedLock 같은 분산 락이 필요하다.
- 서버가 재시작되면 스케줄이 초기화된다.
- 실행 이력 관리 기능이 없다.
단일 서버의 간단한 배치에만 사용하고, 운영 수준에서는 Jenkins나 Quartz를 고려하라.
cron 표현식
@Scheduled와 Jenkins 모두 cron 표현식을 사용한다. Spring의 cron은 6자리다.
초 분 시 일 월 요일
0 0 2 * * * → 매일 새벽 2시
0 0 */6 * * * → 6시간마다
0 30 1 1 * * → 매월 1일 01:30
0 0 3 ? * MON-FRI → 평일 새벽 3시
멱등성 설계
같은 배치를 여러 번 실행해도 결과가 동일해야 한다. 이것을 멱등성이라고 한다. 배치 운영에서 가장 중요한 설계 원칙이다.
멱등성이 없으면 어떤 일이 벌어지는지 보자.
재시작이나 수동 재실행 시 데이터가 중복 생성된다. 이걸 방지하는 패턴이 멱등성이다.
멱등성 확보 패턴
가장 간단한 방법은 처리 전에 기존 데이터를 삭제하는 것이다.
@Bean
public Step cleanupStep(JobRepository jobRepository,
PlatformTransactionManager transactionManager) {
return new StepBuilder("cleanupStep", jobRepository)
.tasklet((contribution, chunkContext) -> {
String targetDate = chunkContext.getStepContext()
.getJobParameters().get("targetDate").toString();
settlementRepository.deleteByTargetDate(targetDate);
return RepeatStatus.FINISHED;
}, transactionManager)
.build();
}
정산 Step 실행 전에 해당 날짜의 기존 정산 데이터를 삭제한다. 2번 실행해도 결과가 동일하다.
다른 방법은 UPSERT를 사용하는 것이다.
INSERT INTO settlement (seller_id, target_date, amount)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE amount = VALUES(amount)
키가 중복되면 UPDATE로 처리되므로, 여러 번 실행해도 최종 결과가 같다.
- INSERT 대신 DELETE + INSERT 또는 UPSERT를 사용하는가?
- 같은 파라미터로 다시 실행해도 결과가 동일한가?
- 외부 시스템 호출(알림, 결제 등)은 중복 방지 처리가 되어 있는가?
모니터링
배치가 정상적으로 동작하는지 지속적으로 확인해야 한다.
메타데이터 조회
가장 기본적인 모니터링은 메타데이터 테이블을 조회하는 것이다.
-- 최근 실행 이력
SELECT job_name, status, start_time, end_time,
TIMESTAMPDIFF(SECOND, start_time, end_time) AS duration_sec
FROM batch_job_execution bje
JOIN batch_job_instance bji ON bje.job_instance_id = bji.job_instance_id
ORDER BY start_time DESC
LIMIT 10;
-- Step별 처리 건수
SELECT step_name, read_count, write_count, skip_count, status
FROM batch_step_execution
WHERE job_execution_id = ?;
Spring Actuator 연동
Actuator의 /actuator/health에 배치 상태를 노출할 수 있다.
management:
endpoints:
web:
exposure:
include: health, metrics
health:
defaults:
enabled: true
알림 설정
배치 실패 시 즉시 알림을 보내야 한다. @AfterJob Listener에서 Slack이나 이메일 알림을 발송하는 것이 일반적이다.
@AfterJob
public void afterJob(JobExecution jobExecution) {
if (jobExecution.getStatus() == BatchStatus.FAILED) {
String message = String.format("[BATCH FAIL] %s — %s",
jobExecution.getJobInstance().getJobName(),
jobExecution.getExitStatus().getExitDescription());
slackNotifier.send(message);
}
}
성능 최적화 팁
chunk 크기 튜닝
기본값 1,000에서 시작하되, 실제 환경에서 측정하고 조정한다. Writer가 JDBC Batch를 사용하면 chunk 크기와 batch size를 맞춘다.
페이지 크기 = chunk 크기
Reader의 pageSize와 Step의 chunkSize를 동일하게 설정해서 chunk당 DB 쿼리를 1회로 유지한다.
인덱스 확인
Reader의 WHERE 절과 ORDER BY 절에 인덱스가 걸려있는지 확인한다. 배치는 대량 데이터를 조회하므로 인덱스 유무에 따라 수십 배 성능 차이가 날 수 있다.
JDBC Batch Insert 활용
JPA의 saveAll()보다 JdbcBatchItemWriter가 빠르다. 특히 INSERT 위주의 작업에서는 JDBC Batch가 압도적이다.
Reader는 JpaPagingItemReader로 엔티티를 편하게 읽고, Writer는 JdbcBatchItemWriter로 빠르게 저장하는 조합이 실용적이다. Reader에서는 JPA의 편의성을, Writer에서는 JDBC의 성능을 취하는 것이다.
자주 하는 실수
배치는 반드시 재실행될 수 있다. 장애 복구, 수동 재처리, 스케줄러 오류 등 재실행 상황은 흔하다. 멱등성이 없으면 데이터 중복, 이중 결제, 중복 알림 같은 심각한 문제가 발생한다. 배치 설계의 첫 번째 원칙으로 멱등성을 잡아라.
[!DANGER] 배치 실패 알림을 설정하지 않기
새벽 2시에 도는 배치가 실패해도 아무도 모르면, 다음 날 업무 시간에 "어제 정산이 안 됐어요"라는 문의를 받게 된다. 배치 실패 시 즉시 알림이 가는 체계를 반드시 갖춰야 한다.
[!DANGER] 운영 DB에서 직접 메타데이터 테이블을 수정하기
배치가 FAILED 상태인데 수동으로 COMPLETED로 바꾸면, Spring Batch의 실행 이력이 꼬여서 재시작이 정상 동작하지 않는다. 메타데이터 테이블은 Spring Batch가 관리하는 영역이다. 직접 수정하지 말고, 새로운 파라미터로 다시 실행하라.