스프링배치에서 중단하고 싶을 때
스프링 배치(Spring Batch)를 이용한 대용량 데이터 처리 환경에서, 특정 작업이 시스템 리소스를 과도하게 점유하거나 운영상 긴급히 중단해야 하는 상황은 빈번히 발생합니다.
이때 프로세스를 강제 종료(kill -9)하면 비즈니스 데이터의 오염이나 메타데이터 불일치가 발생합니다. 이를 방지하기 위해 프레임워크가 제공하는 '안전한 중단(Graceful Stop)'의 내부 메커니즘과, 실무에서 외부에 노출하기 어려운 executionId 대신 고정된 jobName을 기반으로 추상화하여 제어하는 구조를 종합적으로 정리합니다.
1. JobName 기반 추상화의 필요성
스프링 배치 내부적으로 특정 실행 단위를 제어하려면 동적으로 생성되는 기본키인 executionId가 필요합니다. 하지만 외부 스케줄러(Jenkins, Airflow 등)나 어드민 UI 시스템이 매번 실시간으로 생성되는 이 ID를 추적하고 관리하는 것은 비효율적입니다.
따라서 외부에 노출되는 엔드포인트는 고정된 문자열인 jobName을 파라미터로 받고, 내부적으로 배치 레지스트리(JobExplorer)를 조회하여 현재 활성화된(STARTED) executionId를 역추적해 중단 명령을 내리는 구조가 실무 아키텍처에서 권장됩니다.
2. 내부 동작 메커니즘 및 상태 전이
JobOperator.stop(executionId) 신호가 트리거되면 스프링 배치는 다음과 같은 단계적인 상태 전이 과정을 거칩니다.
- 중단 신호 접수: 외부 요청을 받은 관리자 컴포넌트가
JobOperator.stop(executionId)을 호출합니다. - STOPPING 상태 마킹: 스프링 배치는 메타데이터 테이블(
BATCH_JOB_EXECUTION)의STATUS필드를 즉시STOPPING으로 변경합니다. - 현재 청크 보존: 청크(Chunk) 지향 프로세스 내부에서 연산 중이던 데이터는 멈추지 않고, 현재 청크 분량의
ItemProcessor와ItemWriter.write()를 거쳐 비즈니스 DB에 정상적으로 커밋(Commit)됩니다. - 청크 경계선 감지: 현재 청크의 트랜잭션이 완료되고 다음 청크가 시작되기 직전(청크 경계선), 프레임워크는 내부 스레드에서
JobExecution의 상태를 검사합니다. - 최종 중단:
STOPPING신호가 확인되면 즉시 루프를 탈출하고, 메타 테이블의STATUS와EXIT_CODE를 최종적으로STOPPED로 기록한 뒤 프로세스를 안전하게 종료합니다.
3. 즉각적인 중단 조치가 불가능한 이유 (지연 현상의 원인)
중단 명령을 내렸음에도 배치가 그 자리에서 즉시 멈추지 않고 수 초에서 수 분의 지연 시간(Lag)이 발생하는 것은 데이터 정합성을 최우선으로 하는 스프링 배치의 아키텍처적 제약 때문입니다.
실행 중인 스레드를 강제로 인터럽트하거나 물리적으로 종료하면, DB 적재 작업이 중간에 끊겨 비즈니스 데이터가 누락되거나 중복되는 대참사가 일어납니다. 프레임워크는 이를 막기 위해 반드시 청크 경계선(Chunk Boundary)에서만 중단 신호를 체크하도록 설계되어 있습니다.
따라서 다음과 같은 상황에서는 즉각적인 중단이 안 되고 대기 시간이 길어질 수 있습니다.
- 과도하게 설정된 Chunk Size: 청크 사이즈가 5,000 또는 10,000 건 단위로 과도하게 클 경우, 해당 데이터 묶음이 모두 처리되고 커밋될 때까지 배치는 멈추지 않습니다.
- 무거운 내부 로직 및 외부 I/O:
ItemProcessor내에서 건당 외부 API를 호출하거나 복잡한 연산을 수행하여 청크 하나를 소화하는 턴(Turn) 자체가 길다면, 다음 청크 경계선에 도달할 때까지 상당한 지연이 유도됩니다.
4. 실무 구현 코드 (TriggerManager & Controller)
① JobTriggerManager (추상화 매핑 컴포넌트)
jobName을 기반으로 JobExplorer를 조회하여 현재 런타임에서 실행 중인 JobExecution들을 찾아내고 중단 신호를 보냅니다. 별도의 인메모리 Map을 관리할 필요 없이 DB 메타데이터 레지스트리를 신뢰하므로 분산 환경에서도 안전합니다.
JobTriggerManager.java
@Component
@RequiredArgsConstructor
@Slf4j
public class JobTriggerManager {
private final JobOperator jobOperator;
private final JobExplorer jobExplorer;
/**
* 고정된 jobName을 기반으로 활성화된 배치를 역추적하여 안전하게 중단시킵니다.
* @param jobName 배치의 고유 명칭 (예: "unpaidUserReminderJob")
*/
public boolean stopJobByJobName(String jobName) {
// 1. 메타데이터 레지스트리에서 현재 '실행 중(STARTED/STOPPING)'인 JobExecution 세트를 조회
Set<JobExecution> runningExecutions = jobExplorer.findRunningJobExecutions(jobName);
if (runningExecutions.isEmpty()) {
throw new IllegalStateException("현재 실행 중인 배치 작업이 존재하지 않습니다: " + jobName);
}
boolean isStopSignaled = false;
// 2. 실행 중인 모든 인스턴스의 ID를 추출하여 중단 신호 전달
for (JobExecution execution : runningExecutions) {
try {
// 내부적으로 STATUS를 STOPPING으로 변경 시도
boolean stopped = jobOperator.stop(execution.getId());
if (stopped) {
isStopSignaled = true;
log.info("JobName: {}, ExecutionID: {} 에 중단 신호가 입력되었습니다.", jobName, execution.getId());
}
} catch (NoSuchJobExecutionException | JobExecutionNotRunningException e) {
log.warn("이미 종료되었거나 제어할 수 없는 Execution ID입니다: {}", execution.getId());
}
}
return isStopSignaled;
}
}② BatchAdminController (엔드포인트)
외부 시스템이나 관리자는 복잡한 일회성 ID 대신 직관적인 jobName 문자열 파라미터만으로 중단 명령을 하달합니다.
BatchAdminController.java
@RestController
@RequestMapping("/api/v1/batch")
@RequiredArgsConstructor
public class BatchAdminController {
private final JobTriggerManager jobTriggerManager;
/**
* Job 고유 이름을 기반으로 실행 중인 배치를 안전하게 중단(Graceful Stop)시킵니다.
*/
@PostMapping("/stop")
public ResponseEntity<String> stopJob(@RequestParam String jobName) {
try {
boolean success = jobTriggerManager.stopJobByJobName(jobName);
if (success) {
return ResponseEntity.ok(jobName + " 배치에 중단 신호가 전달되었습니다. 현재 청크 완료 후 종료됩니다.");
}
return ResponseEntity.badRequest().body("중단 신호를 전달하지 못했습니다.");
} catch (IllegalStateException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("오류가 발생했습니다: " + e.getMessage());
}
}
}5. 아키텍처적 결론 및 재시작(Restart) 연계
이 방식은 청크 경계선까지 기다려야 하는 지연(Lag) 비용이 발생하지만, 비즈니스 트랜잭션의 완결성을 완벽하게 보장합니다. 최종적으로 데이터 오염 없이 딱 성공한 청크 지점까지만 메타 테이블에 저장된 상태로 STOPPED 처리됩니다.
이렇게 안전하게 중단된 배치는 추후 리소스 경합 문제가 해결되거나 결함이 보정된 후, 동일한 Job Parameter로 재시작(Restart) 명령을 내렸을 때 프레임워크가 메타데이터를 역참조하여 처음부터 다시 도는 것이 아니라 정확히 중단되었던 다음 청크 위치부터 이어붙이기(Resume)를 수행할 수 있는 결정적인 기반이 됩니다.
댓글
GitHub 계정으로 의견이나 질문을 남길 수 있습니다.