개요
이번에는 Spring Batch에서 작업의 흐름을 설정할 때 사용하는 Flow, Split을 학습하고 프로젝트에 적용해 보겠습니다.
Flow
Flow는 Spring Batch에서 작업 흐름을 정하는데 사용됩니다.
작업의 단계인 Step을 그룹화하여 복잡한 작업 흐름을 생성할 수 있습니다.
Flow는 Step을 순차적으로 실행되게 하며 필요한 경우 조건부로 실행 흐름을 제어 할 수 있습니다.
예를 들어, 작업이 실패한 경우 다른 단계로 이동하게 하거나 특정 조건이 충족되면 다음 단계를 실행하게 하는 등 여러 조거분 실행 흐름을 있습니다.
Flow 예시
@Bean
public Flow userUpdateFlow() {
return new FlowBuilder<SimpleFlow>("userUpdateFlow")
.start(userUpdateUsedMoneyStep())
.next(userUpdateTierStep())
.build();
}
Flow를 이용해 userUpdateUsedMoneyStep()을 실행한 후 usurUpdateTierStep()을 실행하도록 작업 흐름을 설정한 코드입니다.
Split
Split은 Spring Batch에서 여러 작업을 병렬적으로 실행할 때 사용됩니다.
Split은 여러 개의 Flow나 Step을 병렬로 동시에 실행할 수 있게 해줍니다.
이는 Spring Batch에서 병렬 작업을 처리해야 할 경우 유용하게 사용됩니다.
Split 예시
@Bean
public Job jobV5() {
return jobBuilderFactory.get("jobV5")
.start(userUpdateFlow())
.split(taskExecutor)
.add(userProvideRewardFlow(), userProvideCouponFlow())
.end().build();
}
split을 이용하여 userProvideRewardFlow()와 userProvideCouponFlow() 두 개의 Flow를 병렬 실행시키는 코드입니다.
.split() 안에는 taskExecutor를 넣어 작업을 병렬로 실행시켜 줍니다.
.add() 안에는 병렬로 실행시킬 Flow나 Step을 넣어줍니다.
Split 와 TaskExecutor
// step에서 사용한 taskExecutor는 step의 작업을 병렬 처리해준다.
@Bean
public Step userProvideRewardStep() {
return stepBuilderFactory.get("userProvideRewardStep")
.<User, User>chunk(100)
.reader(userItemReader)
.processor(userProvideRewardProcessor)
.writer(userItemWriter)
.taskExecutor(taskExecutor)
.build();
}
// split은 작업의 단위로 병렬 처리해준다.
@Bean
public Job jobV5() {
return jobBuilderFactory.get("jobV5")
.start(userUpdateFlow())
.split(taskExecutor)
.add(userProvideRewardFlow(), userProvideCouponFlow())
.end().build();
}
split와 taskExecutor 모두 병렬로 작업을 처리하게 해줍니다. 하지만 이 둘의 병렬 작업은 차이가 있습니다.
Split
Split은 Flow나 Step을 병렬 처리하게 합니다. 즉 작업 단위로 병렬 처리를 진행합니다.
taskExecutor
Step에서 사용된 taskExecutor는 step 내부에서 Chunk 단위의 병렬로 처리해줍니다. 즉 step 작업을 병렬 처리해줍니다.
Split와 TaskExecutor가 각각 유리한 상황
Split을 사용하는 경우
- 독립적인 작업들: 여러 Step 또는 Flow가 서로 독립적이고, 별도로 실행될 때 효율적입니다. 각 Step 사이에 데이터 종속성이 없을 때 좋습니다.
- 다양한 리소스 사용: 서로 다른 Step들이 다른 리소스(예: 데이터베이스, 네트워크, 파일 시스템 등)를 사용하는 경우 병렬로 실행함으로써 전체 리소스 사용률을 최적화할 수 있습니다.
- 배치 작업 분할: 큰 배치 작업을 여러 작은 작업으로 분할하려는 경우 유용할 수 있습니다. 예를 들어, 한 Step이 데이터베이스에서 데이터를 읽고 다른 Step이 네트워크 API 호출을 수행하는 경우에 유용하게 사용될 수 있습니다.
TaskExecutor을 사용하는 경우
- 대량의 동일한 작업: 하나의 Step 내에서 많은 양의 데이터를 처리할 때, 그 데이터를 chunk 단위로 나누어 병렬 처리하려는 경우에 유용합니다.
- 스텝 내부의 병목 해소: 특정 Step 내에서 병목이 발생하는 경우, 그 Step 내부의 처리를 병렬화하여 병목을 해소할 수 있습니다.
- 세밀한 제어가 필요한 경우: taskExecutor를 사용하면 스레드 풀의 크기, 큐의 크기, 스레드의 우선순위 등 스레드 관련 설정을 세밀하게 제어할 수 있습니다. 따라서 성능 튜닝이 필요한 경우에 유용하게 사용될 수 있습니다.
- 코어 수 활용: 특정 Step이 CPU 집중적인 작업을 수행하는 경우, 여러 코어를 활용하여 그 작업을 빠르게 완료하려는 경우에 유용합니다.
즉, 프로젝트의 데이터 양, 작업의 흐름, step들이 사용하는 리소스 등을 고려하여 설정하면 되겠습니다.
성능 체크
사실, split과 TaskExecutor를 공부한 이유는 split을 도입하여 병렬 처리를 했는데 응답 시간이 오히려 증가하는 상황을 만나서 였습니다.
결론부터 말하자면 Step들이 독립적이지 않아 응답 시간이 늘어났습니다. 저의 작업 흐름은 모두 User 객체를 다루고 있었습니다.
제가 프로젝트에서 만든 작업 흐름입니다.
처음에는 순차적으로 작업을 진행했습니다.
데이터는 User 만개, user 별로 UsedMoneyLog 10개로 만들어 10만개로 진행했습니다.
순차 처리
병렬 처리를 하지 않은 순차적 성능 측정 결과 입니다.
2023-09-01 01:42:18.981 INFO 8328 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-09-01 01:42:18.981 INFO 8328 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2023-09-01 01:42:18.984 INFO 8328 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 3 ms
2023-09-01 01:42:19.068 INFO 8328 --- [nio-8080-exec-1] o.s.b.c.l.support.SimpleJobLauncher : Job: [FlowJob: [name=jobV4]] launched with the following parameters: [{time=1693500139038}]
2023-09-01 01:42:19.096 INFO 8328 --- [nio-8080-exec-1] o.s.batch.core.job.SimpleStepHandler : Executing step: [userUpdateUsedMoneyStep]
2023-09-01 02:08:23.743 INFO 8328 --- [nio-8080-exec-1] o.s.batch.core.step.AbstractStep : Step: [userUpdateUsedMoneyStep] executed in 26m4s646ms
2023-09-01 02:08:24.471 INFO 8328 --- [nio-8080-exec-1] o.s.batch.core.job.SimpleStepHandler : Executing step: [userUpdateTierStep]
2023-09-01 02:08:48.197 INFO 8328 --- [nio-8080-exec-1] o.s.batch.core.step.AbstractStep : Step: [userUpdateTierStep] executed in 23s726ms
2023-09-01 02:08:48.869 INFO 8328 --- [nio-8080-exec-1] o.s.batch.core.job.SimpleStepHandler : Executing step: [userRegistRewardLogStep]
2023-09-01 02:09:17.648 INFO 8328 --- [nio-8080-exec-1] o.s.batch.core.step.AbstractStep : Step: [userRegistRewardLogStep] executed in 28s779ms
2023-09-01 02:09:18.458 INFO 8328 --- [nio-8080-exec-1] o.s.batch.core.job.SimpleStepHandler : Executing step: [userProvideRewardStep]
2023-09-01 02:09:50.517 INFO 8328 --- [nio-8080-exec-1] o.s.batch.core.step.AbstractStep : Step: [userProvideRewardStep] executed in 32s59ms
2023-09-01 02:09:51.267 INFO 8328 --- [nio-8080-exec-1] o.s.batch.core.job.SimpleStepHandler : Executing step: [userProvideCouponStep]
2023-09-01 02:10:19.369 INFO 8328 --- [nio-8080-exec-1] o.s.batch.core.step.AbstractStep : Step: [userProvideCouponStep] executed in 28s102ms
2023-09-01 02:10:19.724 INFO 8328 --- [nio-8080-exec-1] o.s.b.c.l.support.SimpleJobLauncher : Job: [FlowJob: [name=jobV4]] completed with the following parameters: [{time=1693500139038}] and the following status: [COMPLETED] in 28m0s538ms
2023-09-01 02:10:19.761 INFO 8328 --- [nio-8080-exec-1] com.dotd.user.aop.LoggingAspect : 위치 : BatchController.batchV4() / 걸린 시간 : 1680715 ms
대략 28분이 걸렸습니다.
병렬 처리
총 3개의 Flow로 진행했습니다.
전체 작업의 흐름은 유저 별로 사용 금액 내역을 전부 가져와 총 사용 금액 내용을 계산하고 user의 usedMoney를 업데이트 했습니다.
업데이트된 usedMoney를 토대로 user의 Tier를 업데이트 했습니다.
그 다음 2개의 Flow를 병렬 실행시켰습니다.
UserProvideCouponFlow는 user에게 티어 별로 Coupon을 지급하는 Flow 입니다.
UserProvideRewardFlow는 user에게 티어 별로 적립금을 지급하고 RewardLog에 지급 사실을 기록하는 Flow 입니다.
병럴 처리한 성능 결과 입니다.
2023-09-01 01:13:47.761 INFO 18552 --- [nio-8080-exec-9] o.s.b.c.l.support.SimpleJobLauncher : Job: [FlowJob: [name=jobV5]] launched with the following parameters: [{time=1693498427740}]
2023-09-01 01:13:47.785 INFO 18552 --- [ taskExecutor-1] o.s.batch.core.job.SimpleStepHandler : Executing step: [userRegistRewardLogStep]
2023-09-01 01:13:47.789 INFO 18552 --- [ taskExecutor-3] o.s.batch.core.job.SimpleStepHandler : Executing step: [userUpdateUsedMoneyStep]
2023-09-01 01:13:47.792 INFO 18552 --- [ taskExecutor-4] o.s.batch.core.job.SimpleStepHandler : Executing step: [userProvideCouponStep]
2023-09-01 01:16:54.632 INFO 18552 --- [ taskExecutor-1] o.s.batch.core.step.AbstractStep : Step: [userRegistRewardLogStep] executed in 3m6s846ms
2023-09-01 01:16:54.662 INFO 18552 --- [ taskExecutor-1] o.s.batch.core.job.SimpleStepHandler : Executing step: [userProvideRewardStep]
2023-09-01 01:17:03.082 INFO 18552 --- [ taskExecutor-4] o.s.batch.core.step.AbstractStep : Step: [userProvideCouponStep] executed in 3m15s290ms
2023-09-01 01:18:13.025 INFO 18552 --- [ taskExecutor-1] o.s.batch.core.step.AbstractStep : Step: [userProvideRewardStep] executed in 1m18s363ms
2023-09-01 01:18:13.407 INFO 18552 --- [ taskExecutor-3] o.s.batch.core.step.AbstractStep : Step: [userUpdateUsedMoneyStep] executed in 4m25s618ms
2023-09-01 01:18:13.439 INFO 18552 --- [ taskExecutor-3] o.s.batch.core.job.SimpleStepHandler : Executing step: [userUpdateTierStep]
2023-09-01 01:18:17.575 INFO 18552 --- [ taskExecutor-3] o.s.batch.core.step.AbstractStep : Step: [userUpdateTierStep] executed in 4s136ms
2023-09-01 01:18:17.586 INFO 18552 --- [nio-8080-exec-9] o.s.b.c.l.support.SimpleJobLauncher : Job: [FlowJob: [name=jobV5]] completed with the following parameters: [{time=1693498427740}] and the following status: [COMPLETED] in 4m29s820ms
2023-09-01 01:18:17.586 INFO 18552 --- [nio-8080-exec-9] com.dotd.user.aop.LoggingAspect : 위치 : BatchController.batchV5() / 걸린 시간 : 269846 ms
대략 4분 29초가 걸렸습니다.
이번에도 병렬 처리로 인해 압도적인 성능 향상을 경험할 수 있었습니다.
하지만 여기서 순차적 처리에 step마다 taskExecutor를 설정하여 step을 병렬적으로 처리한 것이 더 빨랐습니다.
순차적 처리에 taskExecutor를 적용한 측정 결과
2023-09-01 01:08:11.010 INFO 18552 --- [nio-8080-exec-5] o.s.b.c.l.support.SimpleJobLauncher : Job: [FlowJob: [name=jobV4]] launched with the following parameters: [{time=1693498090981}]
2023-09-01 01:08:11.043 INFO 18552 --- [nio-8080-exec-5] o.s.batch.core.job.SimpleStepHandler : Executing step: [userUpdateUsedMoneyStep]
2023-09-01 01:11:42.855 INFO 18552 --- [nio-8080-exec-5] o.s.batch.core.step.AbstractStep : Step: [userUpdateUsedMoneyStep] executed in 3m31s812ms
2023-09-01 01:11:42.877 INFO 18552 --- [nio-8080-exec-5] o.s.batch.core.job.SimpleStepHandler : Executing step: [userUpdateTierStep]
2023-09-01 01:11:50.609 INFO 18552 --- [nio-8080-exec-5] o.s.batch.core.step.AbstractStep : Step: [userUpdateTierStep] executed in 7s732ms
2023-09-01 01:11:50.631 INFO 18552 --- [nio-8080-exec-5] o.s.batch.core.job.SimpleStepHandler : Executing step: [userRegistRewardLogStep]
2023-09-01 01:11:53.867 INFO 18552 --- [nio-8080-exec-5] o.s.batch.core.step.AbstractStep : Step: [userRegistRewardLogStep] executed in 3s236ms
2023-09-01 01:11:53.888 INFO 18552 --- [nio-8080-exec-5] o.s.batch.core.job.SimpleStepHandler : Executing step: [userProvideRewardStep]
2023-09-01 01:12:01.530 INFO 18552 --- [nio-8080-exec-5] o.s.batch.core.step.AbstractStep : Step: [userProvideRewardStep] executed in 7s642ms
2023-09-01 01:12:01.551 INFO 18552 --- [nio-8080-exec-5] o.s.batch.core.job.SimpleStepHandler : Executing step: [userProvideCouponStep]
2023-09-01 01:12:04.575 INFO 18552 --- [nio-8080-exec-5] o.s.batch.core.step.AbstractStep : Step: [userProvideCouponStep] executed in 3s23ms
2023-09-01 01:12:04.586 INFO 18552 --- [nio-8080-exec-5] o.s.b.c.l.support.SimpleJobLauncher : Job: [FlowJob: [name=jobV4]] completed with the following parameters: [{time=1693498090981}] and the following status: [COMPLETED] in 3m53s570ms
2023-09-01 01:12:04.586 INFO 18552 --- [nio-8080-exec-5] com.dotd.user.aop.LoggingAspect : 위치 : BatchController.batchV4() / 걸린 시간 : 233605 ms
대략 3분 53초가 걸렸습니다.
저는 왜 순차적 처리가 split을 쓰지 않은 것보다 빨랐는지 이유를 알고 싶었고 위에서 언급한 것처럼 split이 같은 리소스를 활용하면 불리할 수 있고 독립적인 작업을 하는 step을 사용할 때 유리하다는 것을 배웠습니다.
마무리
이번에는 split과 flow를 사용하기 위해 여러 작업들을 만들었습니다.
그 과정에서 한 파일안에 대량의 내용들이 들어가니 코드의 가독성이 떨어졌고 또한 여러 Step이 같은 자원인 User를 사용하여 리소스 경쟁 문제가 생기는 듯 여러 문제를 마주쳤습니다.
코드 가독성을 높이기 위해 리팩토링하여 reader, processor, writer 별로 패키지를 두어 따로 관리하여 가독성을 높였습니다.
리소스 경쟁 문제는 각 스텝마다 별도의 인스턴스를 사용할 수 있게 하여 문제를 해결했습니다.
다음 글에는 리팩토링과 별도의 인스턴스 생성에 대해 글을 써보려 합니다.
항상 감사합니다.