Spring Batch Step Flow next() 의 함정

WIZ·2023년 11월 3일

TroubleShooting

목록 보기
4/7

사건의 발단


현재 회사에서 Spring Batch 를 이용해 다양한 배치 어플리케이션을 개발하고 있다.
그 과정에서 Step Flow 가 내가 예상했던대로 흘러가지 않는 이슈가 발생했는데, 해당 포스팅에서는 그 이슈에 대해서 소개하려한다.

여기서 Step Flow 란 여러 Step 들을 순차적으로 처리하거나, 분기 처리하는 등의 흐름을 만드는 것을 말한다.
단순하게 단위가 Step 인 Flow 를 작성한다고 이해하고 넘어가면 될 것 같다.

Spring Batch 의 Step Flow 에서는 이전 Step 에서 반환하는 ExitStatus 에 따라서 분기문을 만들어낼 수 있다.

[Decider Step] 실행
 - 그 결과가 TRUE 라면?
   [B Step] - [E Step] - [F Step] - [G Step] 순서로 실행
 - 그 결과가 FALSE 라면?
   [A Step] - [B Step] - [C Step] - [D Step] 순서로 실행

이를 이용해 위와같은 흐름을 만들었다.
여기서 이슈가 발생했는데 Decider Step 의 결과가 FALSE 가 나왔는데 [A Step] - [B Step] - [C Step] - [D Step] 의 흐름이 아니라 [A Step] - [B Step] - [E Step] - [F Step] - [G Step] 의 흐름이 실행된 것이다.

왜 이런일이 발생했을까?
조금 더 간단한 예제를 통해 문제를 정확히 이해해보자.

들어가기에 앞서 해당 포스팅은 Job 을 생성하는 Builder 에서 Step, Flow, Decider 등이 어떻게 처리하는지에 대해서 깊이있게 다루지 않는다.
오로지 내가 경험한 이슈에 포커스를 맞춰 작성된 글이며, 조금 더 정확한 동작방식은 별도로 공부하면 좋을 것 같다.


사건 속으로..


@Bean  
fun test1Job(  
   jobRepository: JobRepository,  
   decider: Step,  
   step1: Step,  
   step2: Step,  
   step3: Step,  
   step4: Step,  
): Job {  
    return JobBuilder("test1Job", jobRepository)  
        .incrementer(RunIdIncrementer())  
        .start(decider)  
        .on("TRUE")  
            .to(step1)  
            .next(step2)  
            .next(step3)  
        .from(decider)  
        .on("FALSE")  
            .to(step2)  
            .next(step4)  
        .end()  
        .build()  
}

Decider Step 은 ExitStatus 로 TRUE, FALSE 를 반환할 수 있고, 그 반환 상태에 따른 분기문이 작성되어 있다.
그럼 TRUE, FALSE 를 각각 반환했을 때 어떤 Step 들이 실행되는지 예상해보자.

먼저 TRUE 가 반환됐을 대 [Step 1] - [Step 2] - [Step 3] 이 실행될거라 예상할 수 있다.
실행 후 BATCH_STEP_EXECUTION 테이블을 통해 그 결과를 확인해보자.

Step 들이 예상한대로 실행된 것을 확인할 수 있다.

그렇다면 FALSE 를 반환했을 때는 어떨까?
나는 [Step 2] - [Step 4] 가 실행될거라 예상했다.
그 결과를 확인해보자.

예상과 다르게 [Step 2] - [Step 3] 가 실행된 것을 확인할 수 있다..!!!
왜 이런 문제가 생기는걸까..?

공식 문서에 나와있는 next(Step) 에 대한 설명을 살펴보자.

Transition to the next step on successful completion of the current step.

현재 Step 이 성공했을 때 실행할 다음 Step 을 설정한다는 것이다.
정확하진 않지만 이 의미를 통해 이유를 유추해보면, next(Step) 을 통해 다음 Step 을 지정하는 것은 Step 객체 자체에 인라인으로 적용되는 것이기 때문에 하나의 Step 객체에 여러번 흐름을 지정하는 것은 의미가 없기 때문에 이런 문제가 생긴것으로 생각해볼 수 있다.

따라서 이미 next(step2).next(step3) 를 하는 시점에서 Step 객체의 다음이 [Step 3] 로 정해지게되고, 그로 인해서 to(step2).next(step4) 는 의미가 없는 동작이라는 것이다.


많은 블로그에서 start(), from() 으로 분기를 처리하고 to(), next() 를 이용해 그에 대한 Step Flow 를 구현하는 방식을 소개하는데, 사실 이 것만 가지고 Step Flow 를 구현하면 이와 같은 실수를 하게될 확률이 매우 높다.

다행히도 해당 블로그 포스팅들에서 동일한 Step 객체를 분기처리된 서로 다른 Step Flow 에 재사용한 케이스가 없었기 때문에 정상적으로 동작한다.
하지만, 이를 이용해 실제 복잡한 Step Flow 를 작성하다보면 자신도모르게 위에서 소개한 실수를 저지르기 매우 쉽고, 이를 발견하는 것 또한 시간이 많이 필요하다. (내가 그랬다..😢)

또 다른 예시코드를 보면서 확실하게 해당 문제에 대해서 이해하고 넘어가보도록 하자!

@Bean  
fun test1Job(  
   jobRepository: JobRepository,  
   decider: Step,  
   step1: Step,  
   step2: Step,  
   step3: Step,  
   step4: Step,  
): Job {  
    return JobBuilder("test1Job", jobRepository)  
        .incrementer(RunIdIncrementer())  
        .start(decider)  
        .on("TRUE")  
            .to(step1)  
            .next(step2)  
            .next(step3)  
        .from(decider)  
        .on("FALSE")  
            .to(step1)  
            .next(step2)  
        .end()  
        .build()  
}

위 코드에서 [Decider Step] 의 결과가 FALSE 라면, [Step 1] - [Step 2] 가 실행될거라 기대하겠지만 실제 결과는 [Step 1] - [Step 2] - [Step 3] 이 실행된다.

이제 문제에 대해서 정확히 파악했으리라 생각하고,
그렇다면 이 문제를 해결하고 Step Flow 를 구현하고 싶을 때 어떻게 해야할지에 대해서 살펴보자.


해결방법


문제에 대해서 이해했지만 나는 여전히 특정 조건에 따라서 분기되는 Step Flow 를 구성하고 싶은 상황이다.
이럴 때 사용할 수 있는게 바로 Flow 이다.

FlowFlowBuilder 를 통해서 쉽게 생성할 수 있다. 예시 코드를 살펴보자.

val flow = FlowBuilder<Flow>("sampleFlow")
	.start(step2)
	.next(step4)
	.build()

제일 위에서 소개했던 (문제있는) 예제 코드를 위 Flow 를 적용해 수정해보자.

@Bean  
fun test1Job(  
   jobRepository: JobRepository,  
   decider: Step,  
   step1: Step,  
   step2: Step,  
   step3: Step,  
   step4: Step,  
): Job {  
    return JobBuilder("test1Job", jobRepository)  
        .incrementer(RunIdIncrementer())  
        .start(decider)  
        .on("TRUE")  
            .to(step1)  
            .next(step2)  
            .next(step3)  
        .from(decider)  
        .on("FALSE")  
            .to(flow)  
        .end()  
        .build()  
}

과연 이슈는 해결되었을까?
[Decider Step]FALSE 를 반환하도록해서 해당 배치를 실행한 후 결과를 살펴보자.

기존과는 달리 내가 예상했던대로 [Step 2] - [Step 4] 가 실행된 것을 확인할 수 있다.
처음에도 말했듯이 해당 포스팅은 이슈에 포커싱을 하고 있기 때문에 Flow 가 정확히 어떤 방식으로 돌아가는지까지 설명하진 않는다.
필요하다면 추가로 공부하고 넘어가면 더 좋을 것 같다!!


결론


Spring Batch Step Flow 를 다루는 많은 포스팅에서 예제로 Flow 를 사용하지 않고 단순히 next(Step) 으로 Step 들을 나열하는 코드를 사용하고 있다. 물론 SimpleJob 을 구성하는 경우에는 이러한 방식이 맞긴하다. 하지만 우리와 같이 Step Flow 를 구성하고 싶은 상황에서 이를 따라하게된다면 정말 찾기힘든 오류에 맞닥뜨릴 수 있다.

실제로 낟도 그랬고 이를 발견하고 해결하는데까지 많은 시간이 필요했다.

이전 Step 의 ExitStatus 에 따라 분기되는 Step Flow 를 구현하고 싶다면 Flow 를 이용해 아예 격리된 Step Flow 를 만들고 이를 이용해 Job 을 생성하는게 안전할 것이라 생각한다.

나와 같은 시행착오를 겪는 사람이 없길 바라며, FlowBuilderFlow 에 대해서 명확히 이해하고 Job 을 만들어 의도대로 Step 이 흘러갈 수 있도록 코드를 작성할 수 있도록하자..!! 🥺

0개의 댓글