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

최준성님의 프로필 이미지
최준성

작성한 질문수

스프링 배치

사용자 정의 ExitStatus

JOB 상태값 관련 질문

작성

·

1.1K

0

안녕하세요

상태값 관련해서 질문이 있습니다.

18:07초에서 step2()가 COMPLETED 되어도 on에는 PASS만 정의되어 있기 때문에 step2의 ExitStatus가 PASS가 아니라면 Job의 BatchStatus와 ExitStatus는 FAILED로 된다고 설명해주셨는데요. 

이에 관한 내용은 SimpleJob 부분에서 다뤄주신다고 하셨는데 뒤쪽에 언급이 없으셔서 질문 남기게 되었습니다. 

강의 내용처럼 하게 되면 on에 PASS를 넣으면 말씀하신대로 JobExecution이 FAILED가 나옵니다. 

설명해주신 맥락대로라면  step2의 on을 FAILED로 두고, step2를 COMPLETED 통과시킨다면 step2에 COMPLETED에 대한 내용은 flow는 정의하지 않았으니 JOB의 BatchStatus와 ExitStatus는 결과적으로는 FAILED가 되어야 하는데 DB를 보면 COMPLETED 상태로 표기되고 있습니다. 

즉, 결과는 아래와 같습니다.

on에 custom한 ExitStatus(PASS)를 적용하고 step의 ExitStatus를 COMPLETE로 보내면 JOB의 상태는 FAILED로 찍힌다.

on에 custom하지 않은 ExitStatus(FAILED)를 적용하고 step의 ExitStatus를 COMPLETED로 보내면 JOB의 상태는 COMPLETED로 찍힌다.

그렇다면 결론적으로

Custom한 ExitStatus값만으로 on에 조건을 걸고 실행시 Step의 ExitStatus가 on조건에 매칭되지 않는다면 Job의 상태는 FAILED이고,

일반적인 ExitStatus값을 on에 조건을 걸고 실행 시 Step의 ExitStatus가 on조건에 매칭되지 않는다면 위의 custom처럼 FAILED가 아니라 해당 Step의 ExitStatus 값으로 Job의 상태가 업데이트 된다. 

이렇게 나는데 잘 이해하고 있는게 맞을까요? 

 

@Configuration
@RequiredArgsConstructor
public class TestJobConfiguration {

private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;

@Bean
public Job helloJob() {
return jobBuilderFactory.get("job")
.start(step1())
.on("COMPLETED")
.stop()
.from(step1())
.on("*")
.to(step2())
.on("FAILED")
.stop()
.end()
.build();
}

@Bean
public Step step1() {
return stepBuilderFactory.get("step1")
.tasklet((contribution, chunkContext) -> {
System.out.println("step1 completed");
contribution.setExitStatus(ExitStatus.FAILED);
return RepeatStatus.FINISHED;
})
.build();
}

@Bean
public Step step2() {
return stepBuilderFactory.get("step2")
.tasklet((contribution, chunkContext) -> {
System.out.println("step2 completed");
return RepeatStatus.FINISHED;
})
.build();
}


답변 4

1

정수원님의 프로필 이미지
정수원
지식공유자

이 부분은 사실 스프링 배치의 조건적 전환 로직에 따른 예외 메시지를 보면서 설정을 해야 하는 부분입니다.

앞서 위에서 답변해 드린 상황과 차이가 조금 있습니다.

.start(step1())
.on("FAILED")
.stop()

위 구문에서 스텝1이 실패하게 되면 중지하라고 되어 있습니다. 

즉 스텝 1의 다음 전환이 stop() 입니다.

스프링 배치에서는 to 에 해당하는 스텝이 존재해야 그 스텝에 대하여  fail() 혹은 complete() 에 한하여 기본적인 전환 조건을 추가하고 있습니다.

그래서 

.from(step1())
.on("*")
.to(step2())
.on("FAILED")

위의 구문은 스텝1의 조건에 따른 to 의 전환이 스텝2로 설정되어 있고 스텝 2는 fail() 이 존재하기 때문에 complete() 이 기본적으로 추가 되었습니다.

근데

 .start(step1())

                    .on("FAILED")

                    .stop()

은 스텝 1의 to 에 해당하는 상태조건이 stop 이라서 step1이 to 로서  존재하지 않습니다

그렇기 때문에 다음 조건적 전환을 무엇으로 할지 모르기 때문에 

Caused by: org.springframework.batch.core.job.flow.FlowExecutionException: Next state not found in flow=job for state=job.step0 with exit status=COMPLETED

위의 예외가 발생해서 Job 이 최종 실패한 것으로 나오게 됩니다.

만약에

@Bean
public Job helloJob() {
return jobBuilderFactory.get("job")
.start(step1())
.on("FAILED")
.to(step1())
.end()
.build();
}

위 구문으로 하시면 step 의 종료상태가 FAILED 가 아니더라도 job 의 상태가 completed 로 끝나게 됩니다.

왜냐하면 to 의 상태전환에 step1 이 존재하기 때문에 completed 가 기본적으로 추가됩니다.

이 부분은 위에서 설명드린 소스인 

private void addDanglingEndStates() {
Set<String> froms = new HashSet<>();
for (StateTransition transition : transitions) {
froms.add(transition.getState().getName());
}
if (tos.isEmpty() && currentState != null) {
tos.put(currentState.getName(), currentState);
}
Map<String, State> copy = new HashMap<>(tos);
// Find all the states that are really end states but not explicitly declared as such
for (String to : copy.keySet()) {
if (!froms.contains(to)) {
currentState = copy.get(to);
if (!currentState.isEndState()) {
addTransition("COMPLETED", completedState);
addTransition("*", failedState);
}
}
}
copy = new HashMap<>(tos);
// Then find the states that do not have a default transition
for (String from : copy.keySet()) {
currentState = copy.get(from);
if (!currentState.isEndState()) {
if (!hasFail(from)) {
addTransition("*", failedState);
}
if (!hasCompleted(from)) {
addTransition("*", completedState);
}
}
}
}

을 디버깅 하시면서 파악하시면 이해하시는데 도움이 됩니다.

소스를 보시면 tos 변수에 해당 스텝이 존재해야 그 스텝에 대해서 상태전환값을 추가하도록 되어 있습니다.

1

정수원님의 프로필 이미지
정수원
지식공유자

네 이부분은 스프링 배치의 내부 로직에서 참조를 해야 하는데요

스프링 배치는 Flow 의 조건에 따른 전환이 명확하게 명시가 되어 있을 경우에는 별도의 추가적인 전환조건처리를 하지 않지만 그렇지 않을 경우 기본적인 후속 처리가 이루어 집니다.

가령 스텝이 성공했을 경우와 그렇지 않은 모든 경우를 명시하면 설정한 그대로 처리를 하지만 만약 스텝이 성공 혹은 실패했을 경우의 한개만 전환 조건을 했을 경우 나머지 전환조건을 기본적으로 추가해 버립니다.

아래는 FlowBuilder 의 소스 중 일부입니다.

private void addDanglingEndStates() {
Set<String> froms = new HashSet<>();
for (StateTransition transition : transitions) {
froms.add(transition.getState().getName());
}
if (tos.isEmpty() && currentState != null) {
tos.put(currentState.getName(), currentState);
}
Map<String, State> copy = new HashMap<>(tos);
// Find all the states that are really end states but not explicitly declared as such
for (String to : copy.keySet()) {
if (!froms.contains(to)) {
currentState = copy.get(to);
if (!currentState.isEndState()) {
addTransition("COMPLETED", completedState);
addTransition("*", failedState);
}
}
}
copy = new HashMap<>(tos);
// Then find the states that do not have a default transition
for (String from : copy.keySet()) {
currentState = copy.get(from);
if (!currentState.isEndState()) {
if (!hasFail(from)) {
addTransition("*", failedState);
}
if (!hasCompleted(from)) {
addTransition("*", completedState);
}
}
}
}

위 구문의 의미는 명확하게 어떤 조건적인 상태를 설정에 명시하지 않는 모든 Flow 를 탐색해서 만약 그런 Flow 가  존재한다면 completeState 와 failedState 를 기본적으로 addTransition 하고 있음을 의미합니다.

그리고  아래쪽 구문을 보시면 

if (!currentState.isEndState()) {
if (!hasFail(from)) {
addTransition("*", failedState);
}
if (!hasCompleted(from)) {
addTransition("*", completedState);
}
}

만약 현재 스텝의 조건 전환이 끝나지 않았다면 실패 상태조건이 없을 경우 실패상태조건을 추가하고 완료 상태조건이 없을 경우 완료 상태조건을 추가하고 있습니다.

그렇기 때문에

.from(step1())
.on("*")
.to(step2())
.on("FAILED")
.stop()

위 구문을 설명하자면 

스텝1은 COMPLETED 를 제외한 모든 경우에 스텝2를 실행하고 스텝2는 실패할 경우 중지하라고 되어 있지만 실패하지 않은 경우에는 어떠한 조건이 없기 때문에 스프링 배치가 

if (!hasCompleted(from)) {
addTransition("*", completedState);
}

구문을 실행시켜서 완료상태가 아닌 경우 완료상태조건을 추가해서 실행하도록 처리하고 있습니다.

만약 설정에서 스텝 2에 실패 및 완료 설정을 모두 했다면 위의 구문이 실행되지 않을 것입니다.

그래서 되도록이면 조건적 전환을 할 경우 명확하게 경우의 수를 모두 설정해서 예측 가능한 시나리오를 작성하는 것이 더 좋다고 생각합니다.

0

최준성님의 프로필 이미지
최준성
질문자

자세한 답변 정말 감사드립니다.

완벽하게 이해 되네요 !!

0

최준성님의 프로필 이미지
최준성
질문자

정말 자세한 답변 감사드립니다. 

한 가지 더 궁금한 점이 있습니다.

설명해주신 대로 생각했을때는 아래와 같이 동작하는 경우에도 step1에 대해 자동으로 COMPLETED 코드가 추가되었을 것이기 때문에 step1이 성공한다면 Job이 COMPLETED로 끝날 것이라고 예상했는데 결과값은 FAILED로 나오고 있습니다. 이부분은 왜 그런건가요?

@Configuration
@RequiredArgsConstructor
public class TestJobConfiguration {

private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;

@Bean
public Job helloJob() {
return jobBuilderFactory.get("job")
.start(step1())
.on("FAILED")
.stop()
.end()
.build();
}

@Bean
public Step step1() {
return stepBuilderFactory.get("step1")
.tasklet((contribution, chunkContext) -> {
System.out.println("step1 completed");
return RepeatStatus.FINISHED;
})
.build();
}
}
최준성님의 프로필 이미지
최준성

작성한 질문수

질문하기