강의

멘토링

로드맵

인프런 커뮤니티 질문&답변

식빵님의 프로필 이미지
식빵

작성한 질문수

스프링 배치

JobExecutionListener / StepExecutionListener

StepExecutionListener 의 afterStep 에서 return ExitStatus.FAILED 에 의한 동작에 의문이 갑니다.

작성

·

237

·

수정됨

0

안녕하세요 선생님, 개인적으로 이 강의를 다 보고,

혼자서 이것저것 해보고 있는데, 뭔가 이해가 안되는 현상이 있어서 질문드립니다.

 

일단 제가 작성한 아주 간단한 Batch 코드를 먼저 공유하겠습니다. (Spring boot 3 + Spring Batch core 5 버전입니다.)

```java

package coding.toast.batch.job.reader.config;

import coding.toast.batch.job.reader.listener.EmptyReadFailureListener;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.support.ListItemReader;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class CustomItemStreamConfig {
    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    
    @Bean
    public Job customItemStreamConfigJob() {
       return new JobBuilder("customItemStreamConfigJob", jobRepository)
          .start(customItemStreamConfigStep())
          .next(simpleStep2())
          .build();
    }
    
    private Step simpleStep2() {
       return new StepBuilder("simpleStep", jobRepository)
          .tasklet((contribution, chunkContext) -> {
             System.out.println("이게 나오면 안됩니다!");
             return RepeatStatus.FINISHED;
          }, transactionManager).build();
    }
    
    @Bean
    public Step customItemStreamConfigStep() {
       return new StepBuilder("customItemStreamConfigStep", jobRepository)
          .<Integer, Integer>chunk(5, transactionManager)
          .reader(new ListItemReader<>(List.of())) // 고의적으로 비워놨습니다!
          .writer(chunk -> {
             chunk.getItems().forEach(System.out::println);
          })
          .listener(emptyReadFailureListener())
          .build();
    }
    
    @Bean
    public EmptyReadFailureListener emptyReadFailureListener() {
       return new EmptyReadFailureListener();
    }
}

 

여기서 EmptyReadFailureListener 타입의 인스턴스를 빈으로 등록하고, 이를 Step 의 listener 로 등록했습니다.

 

이 리스너의 역할은 ItemReader 로 부터 읽은 것이 아무것도 없을 때 Step 이 FAILED 되도록 하기 위한 것입니다.

 

 

자세한 내용은 아래와 같습니다.

package coding.toast.batch.job.reader.listener;

import org.springframework.batch.core.*;

public class EmptyReadFailureListener implements StepExecutionListener {
	@Override
	public ExitStatus afterStep(StepExecution stepExecution) {
		if (stepExecution.getReadCount() > 0) {
			return stepExecution.getExitStatus();
		} else {
			return ExitStatus.FAILED.addExitDescription("exit Because Of No Item Read");
		}
	}
}

 

위처럼 작성했습니다.

저는 위의 Job 을 실행하면...

  1. Job 의 BatchStatus 와 ExitStatus 모두 FAILED 가 된다.

  2. simpleStep 은 이전 Step 에서 ExitStatus Failed 가 됐으므로 실행되지 않는다.

     

... 하지만 정확히 정반대의 결과가 나왔습니다 😅😅

 

batch_job_execution 테이블의 결과:

idea64_pKoyVMUdrp.png

 

 

batch_step_execution 테이블의 결과:

idea64_rseBpSfwTu.png

 

 

보시는 바와 같이 Job 자체는 Completed 로 끝이 났습니다.

또한 실행되지 않았으면 했던 두번째 step(=simpleStep) 도 실행이 되버리더라구요...

 

 

질문:
1. 왜 이렇게 동작하는 걸까요? 혹시 ExitStatus 에 대해서 뭔가 잘못 알고 있는 걸까요?

2. 그리고 이를 해결하기 위해서는 EmptyReadFailureListener 를 어떻게 고치면 좋을까요?
아니면 혹시 더 좋은 방법이 있을까요??

답변 2

1

안녕하세요, 인프런 AI 인턴입니다.

식빵님께서 의도하신 대로 Step의 ExitStatus가 FAILED로 반환되었음에도 Job의 BatchStatus가 COMPLETED로 나타나는 현상은 Spring Batch의 기본적인 동작 방식에서 기인합니다. Step의 ExitStatus가 FAILED로 설정되었다고 해서 Job이 자동으로 FAILED로 설정되는 것은 아니기 때문입니다.

  1. Spring Batch에서 기본적으로 Step의 ExitStatus는 다음 Step의 실행 여부를 결정하는 데 사용되지 않습니다. 따라서 simpleStep2가 계속 실행된 것입니다. Job 전체의 BatchStatus는 모든 Step의 수행 결과에 따라 결정되며, 기본적으로 마지막 Step이 성공적으로 완료되면 COMPLETED로 설정됩니다.

  2. 원하는 동작을 구현하시려면 jobExecutionListener를 통해 Step의 ExitStatus를 확인하고 Job의 상태를 설정하거나, JobBuilder에서 next() 메서드 대신 on() 메서드를 사용하여 Step의 ExitStatus에 따라 다음 Step을 실행할지 여부를 조건부로 결정하는 것이 좋습니다.

@Bean
public Job customItemStreamConfigJob() {
   return new JobBuilder("customItemStreamConfigJob", jobRepository)
      .start(customItemStreamConfigStep())
      .on("FAILED").end() // 실패 시 Job을 종료합니다.
      .from(customItemStreamConfigStep()).on("*").to(simpleStep2()) // 다른 경우에 simpleStep2를 실행합니다.
      .end()
      .build();
}

이와 같이 변경함으로써 customItemStreamConfigStep의 ExitStatus가 FAILED인 경우, simpleStep이 실행되지 않도록 할 수 있습니다.

이 방법을 통해 문제를 해결할 수 있기를 바랍니다! 추가 질문이 있으면 언제든지 문의 부탁드립니다.

Spring Batch Reference

0

식빵님의 프로필 이미지
식빵
질문자

자문자답입니다.

 

package coding.toast.batch.job.reader.listener;

import org.springframework.batch.core.*;

public class EmptyReadFailureListener implements StepExecutionListener {
	@Override
	public ExitStatus afterStep(StepExecution stepExecution) {
		// 기존에 이미 예외가 터져서 failed 된 상태인지 먼저 확인한다.
		if (!stepExecution.getStatus().equals(BatchStatus.FAILED)) {
			
			if (stepExecution.getReadCount() > 0) {
				return stepExecution.getExitStatus();
			} else {
				
                 // 추후 Job 과 Step 의 재기동을 위해서 BatchStatus.FAILED 를 꼭 설정합니다.
                 // SimpleJob 에서는 이 설정을 하면 남아 있는 Step 들의 실행을 방지한다.
				stepExecution.setStatus(BatchStatus.FAILED);
                
				return ExitStatus.FAILED
                	.addExitDescription("exit Because Of No Item Read");
			}
		}
		
		return stepExecution.getExitStatus();
	}
}

 

이렇게 하면 결과적으로 다음과 같은 결과가 나옵니다.

  • JobExecution : Batch Status = FAILED / EXIT STATUS = FAILED

  • 첫번째 StepExecution : Batch Status = FAILED / EXIT STATUS = FAILED

  • 두번째 StepExecution : 실행 자체가 안됩니다.

키 포인트는 stepExecution.setStatus(BatchStatus.FAILED); 를 afterStep 메소드 내에서 한번 호출한다는 점입니다. 이렇게 됨으로써 Job, Step 모두 추후에 재실행이 됩니다.

 

그리고 만약에 SimpleJob 이 아니라 FlowJob 을 구성한다면 아래처럼 할 수도 있습니다.

@Configuration
@RequiredArgsConstructor
public class MyJobConfig {
	private final JobRepository jobRepository;
	private final PlatformTransactionManager transactionManager;
	
	@Bean
	public Job customItemStreamConfigJob() {
		//@formatter:off
		return new JobBuilder("customItemStreamConfigJob", jobRepository)
			.start(customItemStreamConfigStep())
				.on("FAILED")
				.fail()
			.from(customItemStreamConfigStep())
				.on("*").to(simpleStep())
				.end()
			.build();

		// 조근 더 간단하게는 아래처럼 작성할 수 있습니다.
		// return new JobBuilder("customItemStreamConfigJob", jobRepository)
		//	.start(customItemStreamConfigStep())
		//	.on("EMPTY_READ").fail()
		//	.next(simpleStep()).end()
		//	.build();
		//@formatter:on
	}
	
    // 나머지 코드는 이전 질문에 작성한 예시코드와 동일합니다.
    // ...생략...
}

 

참고한 글: java - Spring batch job exit status for failed step - Stack Overflow

식빵님의 프로필 이미지
식빵

작성한 질문수

질문하기