Step은 실제 Batch 작업을 수행하는 역할이다.
이전에 작성한 코드를 살펴보면 Job은 코드가 거의 없다.
실제로 Batch 비지니스 로직을 처리하는 (ex: log.info()) 기능은 Step에 구현되어 있다.
이처럼 Step에서는 Batch로 실제 처리하고자 하는 기능과 설정을 모두 포한하는 장소라고 생각하면 된다.
Batch 처리 내용을 담다보니, Job 내부의 Step들간에 순서 혹은 처리 흐름을 제어할 필요가 있는데 이번엔 여러 Step들을 어떻게 관리할지에 대해서 알아본다.
StepNextJobConfig.java
@Slf4j
@Configuration
@RequiredArgsConstructor
public class StepNextJobConfiguration {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean
public Job stepNextJob() {
return jobBuilderFactory.get("stepNextJob")
.start(step1())
.next(step2())
.next(step3())
.build();
}
@Bean
public Step step1() {
return stepBuilderFactory.get("step1")
.tasklet((contribution, chunkContext) -> {
log.info(">>>>> This is Step1");
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public Step step2() {
return stepBuilderFactory.get("step2")
.tasklet((contribution, chunkContext) -> {
log.info(">>>>> This is Step2");
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public Step step3() {
return stepBuilderFactory.get("step3")
.tasklet((contribution, chunkContext) -> {
log.info(">>>>> This is Step3");
return RepeatStatus.FINISHED;
})
.build();
}
}
next()는 순차적으로 Step들을 연결시킬 때 사용된다.
step1 -> step2 -> step3 순으로 하나씩 실행될 떄 next()는 좋은 방법이다.
이번에는 Job Parameter를 version=1
로 변경한뒤 실행해보면
stepNextJob배치
가 실행된 것을 확인할 수 있다. 하지만 기존에 있던 simpleJob
도 함께 실행되었다.
만약 방금 만든 stepNextJob
만 실행하고 싶다면 아래와 같이 변경해야 한다.
application.yml에 아래의 코드를 추가한다.
spring.batch.job.names: ${job.name:NONE}
위의 코드의 역할은 Spring Batch가 실행될 때, Program arguments로 job.name 값이 넘어오면 해당값과 일치하는 Job만 실행하겠다는 것이다.
여기서 ${job.name:NONE}
을 보면 :를 사이에 두고 좌측에 job.name이, 우측에 NONE이 있는데요.
이 코드의 의미는 job.name이 있으면 job.name값을 할당하고, 없으면 NONE을 할당하겠다는 의미이다.
spring.batch.job.names
에 NONE이 할당되면 어떤 배치도 실행하지 않겠다는 의미입니다.
즉, 혹시라도 값이 없을때 모든 배치가 실행되지 않도록 막는 역할이다.
job.name을 배치 실행시에 Program arguments로 넘겨보자.
Job Parameter를 수정했던 Program arguments 항목에 아래와 같이 코드를 입력한다.
--job.name=stepNextJob
위 사진처럼 Program arguments 항목에 위 코드를 추가하고 version=1
은 이미 실행했으니 version=2
로 변경해서 실행한다.
(version=1을 변경하지 않으면 Job Instance 중복 문제가 발생한다.)
실행해보면 지정한 stepNextJob
만 실행된 것을 확인할 수 있다.
Next가 순차적으로 Step의 순서를 제어한다는 것을 알게 되었다.
( 앞의 step에서 오류가 나면 나머지 뒤에 있는 step들은 실행되지 못한다.)
하지만 상황에 따라 정상일땐 step B로 오류가 났을 때는 step C를 수행해야할때가 잇다.
이럴경우를 대비해 Spring Batch Job에서는 조건별로 step을 사용할 수 있다.
StepNextConditionalJobConfig.java
@Slf4j
@Configuration
@RequiredArgsConstructor
public class StepNextConditionalJobConfiguration {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean
public Job stepNextConditionalJob() {
return jobBuilderFactory.get("stepNextConditionalJob")
.start(conditionalJobStep1())
.on("FAILED") // FAILED 일 경우
.to(conditionalJobStep3()) // step3으로 이동한다.
.on("*") // step3의 결과 관계 없이
.end() // step3으로 이동하면 Flow가 종료한다.
.from(conditionalJobStep1()) // step1로부터
.on("*") // FAILED 외에 모든 경우
.to(conditionalJobStep2()) // step2로 이동한다.
.next(conditionalJobStep3()) // step2가 정상 종료되면 step3으로 이동한다.
.on("*") // step3의 결과 관계 없이
.end() // step3으로 이동하면 Flow가 종료한다.
.end() // Job 종료
.build();
}
@Bean
public Step conditionalJobStep1() {
return stepBuilderFactory.get("step1")
.tasklet((contribution, chunkContext) -> {
log.info(">>>>> This is stepNextConditionalJob Step1");
/**
ExitStatus를 FAILED로 지정한다.
해당 status를 보고 flow가 진행된다.
**/
contribution.setExitStatus(ExitStatus.FAILED);
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public Step conditionalJobStep2() {
return stepBuilderFactory.get("conditionalJobStep2")
.tasklet((contribution, chunkContext) -> {
log.info(">>>>> This is stepNextConditionalJob Step2");
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public Step conditionalJobStep3() {
return stepBuilderFactory.get("conditionalJobStep3")
.tasklet((contribution, chunkContext) -> {
log.info(">>>>> This is stepNextConditionalJob Step3");
return RepeatStatus.FINISHED;
})
.build();
}
}
위의 코드는 step1이 실패냐 성공이냐에 따라 수행결과가 달라진다.
.on()
*
일 경우 모든 ExitStatus가 지정된다.to()
from()
to()
에 포함된 step
을 호출한다.from
을 써야만 함 end()
on("*")
뒤에 있는 end는 FlowBuilder를 반환하는 endbuild()
앞에 있는 end는 FlowBuilder를 종료하는 end - FlowBuilder를 반환하는 end 사용시 계속해서 from
을 이어갈 수 있음여기서 중요한 점은 on이 캐치하는 상태값이 BatchStatus가 아닌 ExitStatus라는 것이다.
그래서 분기처리를 위해 상태값 조정이 필요하다면 ExitStatus를 조정해야한다.
조정하는 코드는 아래와 같다.
원하는 상황에 따라 분기로직을 작성하여 contribution.setExitStatus
의 값을 변경하면 된다.
여기서 FAILED를 발생시켜 step1 -> step3를 테스트 해보자.
step1과 step3만 실행된 것을 확인할 수 있다.
(ExitStatus.FAILED로 인해 step2가 무시되고 실행되었다.)
코드를 수정해서 step1 -> step2 -> step3가 실행되는지 확인해보자.
주석을 걸고 다시 실행해보면
step1 -> step2 -> step3가 차례대로 수행된 것을 확인할 수 있다.
Batch Status와 Exit Status의 차이를 아는 것이 중요하다.
BatchStatus는 Job 또는 Step 의 실행 결과를 Spring에서 기록할 때 사용하는 Enum이다.
BatchStatus로 사용 되는 값은
COMPLETED
, STARTING
, STARTED
, STOPPING
, STOPPED
, FAILED
, ABANDONED
, UNKNOWN
있다.
대부분의 값들은 단어와 같은 뜻으로 해석해서 이해하면 된다.
ExitStatus는 Step의 실행 후 상태를 얘기한다.
예를 들어,
.on("FAILED").to(stepB())
위 코드에서 on 메소드가 참조하는 것은 BatchStatus
으로 생각할 수 있지만 실제 참조되는 값은 Step의 ExitStatus
이다.
(ExitStatus는 Enum이 아니다.)
위 예제를 좀더 쉽게 풀이 하자면 exitCode
가 FAILED로 끝나게 되면 StepB로 가라는 뜻이다.
Spring Batch는 기본적으로 ExitStatus
의 exitCode
는 Step의 BatchStatus
와 같도록 설정이 되어 있다.
하지만 만약에 본인만의 커스텀한 exitCode
가 필요하다면,
(즉, BatchStatus와 달라야하는 상황)
.start(step1())
.on("FAILED")
.end()
.from(step1())
.on("COMPLETED WITH SKIPS")
.to(errorPrint1())
.end()
.from(step1())
.on("*")
.to(step2())
.end()
위 step1의 실행 결과는 다음과 같이 3가지가 될 수 있다.
step1이 실패하며, Job 또한 실패하게 된다.
step1이 성공적으로 수행되어 step2가 수행된다.
step1이 성공적으로 완료되며, COMPLETED WITH SKIPS의 exit 코드로 종료 된다.
위 코드에 나오는 COMPLETED WITH SKIPS는 ExitStatus에는 없는 코드입니다.
원하는대로 처리되기 위해서는 COMPLETED WITH SKIPS exitCode를 반환하는 별도의 로직이 필요합니다.
public class SkipCheckingListener extends StepExecutionListenerSupport {
public ExitStatus afterStep(StepExecution stepExecution) {
String exitCode = stepExecution.getExitStatus().getExitCode();
if (!exitCode.equals(ExitStatus.FAILED.getExitCode()) &&
stepExecution.getSkipCount() > 0) {
return new ExitStatus("COMPLETED WITH SKIPS");
}
else {
return null;
}
}
}
위 코드를 설명하면 StepExecutionListener 에서는 먼저 Step이 성공적으로 수행되었는지 확인하고, StepExecution의 skip 횟수가 0보다 클 경우 COMPLETED WITH SKIPS
의 exitCode
를 갖는 ExitStatus
를 반환한다.
2번에서 Step의 결과에 따라 서로 다른 Step으로 이동하는 방법을 알아봤다. 이번에는 다른 방식의 분기처리를 알아보려 한다.
2번에서 진행했던 방식은 2가지 문제가 있다.
ExitStatus
조작이 필요하다.ExitStatus
를 커스텀하게 고치기 위해선 Listener를 생성하고 Job Flow에 등록하는 등 번거로움이 존재한다.이러한 불편함을 해소하기 위해 Spring Batch에는
명확하게 Step들간의 Flow 분기만 담당하면서 다양한 분기처리가 가능한 JobExecutionDecider
이라는 타입이 따로 존재한다.
DeciderJobConfig.java
@Slf4j
@Configuration
@RequiredArgsConstructor
public class DeciderJobConfig {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean
public Job deciderJob() {
return jobBuilderFactory.get("deciderJob")
.start(startStep())
.next(decider()) // 홀수 | 짝수 구분
.from(decider()) // decider의 상태가
.on("ODD") // ODD라면
.to(oddStep()) // oddStep로 간다.
.from(decider()) // decider의 상태가
.on("EVEN") // ODD라면
.to(evenStep()) // evenStep로 간다.
.end() // builder 종료
.build();
}
@Bean
public Step startStep() {
return stepBuilderFactory.get("startStep")
.tasklet((contribution, chunkContext) -> {
log.info(">>>>> Start!");
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public Step evenStep() {
return stepBuilderFactory.get("evenStep")
.tasklet((contribution, chunkContext) -> {
log.info(">>>>> 짝수입니다.");
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public Step oddStep() {
return stepBuilderFactory.get("oddStep")
.tasklet((contribution, chunkContext) -> {
log.info(">>>>> 홀수입니다.");
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public JobExecutionDecider decider() {
return new OddDecider();
}
public static class OddDecider implements JobExecutionDecider {
@Override
public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
Random rand = new Random();
int randomNumber = rand.nextInt(50) + 1;
log.info("랜덤숫자: {}", randomNumber);
if(randomNumber % 2 == 0) {
return new FlowExecutionStatus("EVEN");
} else {
return new FlowExecutionStatus("ODD");
}
}
}
}
이 Batch의 Flow는 아래와 같다
decider를 Flow 사이에 넣는 로직은 위의 사진과 같다.
start()
next()
from()
분기 로직에 대한 모든 일은 0ddDecider
가 전담하고 있다.
때문에 복잡한 분기로직이 있더라도 Step과는 명확히 역할과 책임이 분리된채 진행할 수 있게 된다.
(JobExecutionDecider 인터페이스를 구현한 OddDecider)
여기서는 랜덤하게 숫자를 생성하여 홀수/짝수인지에 따라 서로 다른 상태를 반환한다.
주의할 점은 Step으로 처리하는게 아니기 때문에 ExitStatus
가 아닌 FlowExecutionStatus
로 상태를 관리한다는 것이다.
아주 쉽게 EVEN, ODD라는 상태를 생성하여 반환하였고, 이를 from().on() 에서 사용하는 것을 알 수 있다.
여러번 실행해보면 짝수/홀수가 나오면서 서로 다른 step(oddStep,evenStep)이 실행되는 것을 확인할 수 있다.