드디어 Spring batch의 기나긴 여정을 마무리하는 단계에 접어들었다.
첫번째 과정에서 Spring batch는 application과 별도로 간주해야 하는 별개의 영역이라 기술하였는데, 이러한 말에 상응하듯 Spring Batch는 Spring Batch만을 위한 테스트 도구를 제공한다.
다만, 큰 흐름에서는 기존 application TDD와 거의 비슷하다. 크게 End to End(E2E, Integration Test), 작게는 단위(Unit Test)로 나누어지며 Batch 역시도 이를 위한 구성분리 및 도구들이 존재한다.
통합테스트를 위한 JobLauncherTestUtils, 단위 테스트를 위한 ItemReader/ItemProcessor/ItemWriter 컴포넌트 전용 테스트 도구들을 제공한다.
이를 위해 전체 Job에 대한 테스트, 내부의 step에 대한 별도 테스트 등 batch만을 위해 제공하는 batch 프레임워크의 TDD 요소들에 대해 분석해보고자 한다.
분석대상은 오로지 Spring Batch Test 로직 작성에 대해서만 이루어지기에, TDD 로직 작성(testImplementation)을 위해 필요한 라이브러리, 컴포넌트에 대해 살펴보고자 한다.
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.batch:spring-batch-test'
기본적으로 boot test, 그리고 batch test를 위해 필요한 batch test 의존성을 추가한다.
testImplementation 'com.h2database:h2'
그리고, 중요한 점은 테스트 전용 환경을 위해 Postgresql과 같은 RDB 대신, H2 데이터베이스를 사용하는 것을 권장한다는 점이다. 실제 실무에서도, 운영 및 개발 중인 데이터베이스를 사용하여 테스트를 진행하기 보다는, 테스트 전용 환경 구성이 필요할 것이기 때문이다.
테스트 환경이 존재하더라도, 주로 application에 대한 전용 테스트 환경을 제공하고 있으며 batch를 위한 전용 테스트 데이터베이스를 구축하기에는 사용빈도, 중요성 측면에서 그리 좋지 못한 선택일 수 있기에 batch 전용 테스트 환경은 가장 편하고 편리한 H2 데이터베이스를 활용하여 구축하도록 한다.
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
이를 위해 JVM 메모리 상에 임베디드 되어있는 h2 데이터베이스를 사용하는 기본 설정이 필요하며, DB_CLOSR_DELAY를 -1로 설정하여 커넥션이 끊어졌을때 test db가 없어지지 않고 지속할 수 있도록 구성해준다.
참고로 EXIT 설정의 경우 JVM 종료 시 DB도 같이 close 하도록 한다(close).
batch:
job:
enabled: false
Spring boot환경에서 JobLauncherApplicationRunner를 통해 실행하듯이, 테스트 역시 JobLauncher를 통해 진행하며 테스트 진행 시에만 job을 실행하도록 테스트 환경을 구성한다.
Application의 integration test에 상응할 수 있는 end-to-end test는 spring batch job 전체, 각 itemReader, itemProcessor, itemWriter 컴포넌트 모두 제대로 연동되는지 검증하는 테스트이다.
전체적인 컴포넌트들의 연계동작, 실제 운영환경에서의 배치 동작을 테스트 및 평가하는 통합 테스트로, Spring batch 에서는 상태관리, step의 흐름 등 컴포넌트 간의 연계동작이 단순 application보다는 많고 중요하기에, 어떻게 보면 batch에서 가장 중요한 테스트라 볼 수 있겠다.
@SpringBatchTest
class InFearLearnStudentsBrainWashJobTest {
spring batch test 그 자체라고도 볼 수 있는, SpringBatchTest 어노테이션은 내부적으로 아래 4가지 batch 전용 테스트 도구들을 제공한다.

이 jobLauncherTestUtils를 사용하기 위해서는
@SpringBatchTest
@SpringBootTest
@ActiveProfiles("test")
class InFearLearnStudentsBrainWashJobTest {
@Autowired
private JobLauncherTestUtils jobLauncherTestUtils;
위와 같이 SpringBatchTest 어노테이션을 테스트 클래스에 선언해주고, jobLauncherTestUtils에 대한 주입을 autowired를 통해 받는다.
참고로, 테스트 도메인에서의 의존성 주입은 테스트 프레임워크에 강하게 의존되어있는 구조이기에 생성자 주입과 같은 표준적 구성보다는, 편의성에 초점을 맞춘 구성이 더 편할 수 있다(위와 같은 멤버변수에 autowired를 통한 주입과 같이).
그리고,
@ActiveProfiles("test")
프로파일을 "test"로 설정하여, 테스트 전용 환경을 구성하는 것이 좋다.
개발/운용환경에서 적용한 환경설정을 테스트까지 전파하여, 실제 운용/개발 DB에 접속하거나 Kafka 메시지를 발행한다던가, 테스트 용 컨텍스트를 flush 하는 등의 불필요한 차질이 발생할 수 있다.
이처럼 활성화할 도메인을 test로 설정하여, application-test.yml의 환경설정을 로딩하도록 구성할 수 있으며, 참고로 profile("test")가 기재되어있지 않은 객체들은 모두 스캔 대상이 아니다.
즉, 온전히 운영/개발환경과 분리하여 테스트를 위한 테스트 환경 구성이 이루어지도록 한다.
@PostConstruct
public void configureJobLauncherTestUtils() throws Exception {
jobLauncherTestUtils.setJob(inFearLearnStudentsBrainWashJob);
}
이후, spring batch 테스트를 위한 job 의존성 주입, job을 실행하기 위한 job 객체 등록을 위해 jobLauncherTestUtils에 테스트 대상 job 객체를 등록한다.
이 메서드는 spring container 측에서 해당 빈객체를 등록하기 위해, 빈 객체(의존성) 주입 후(postConstruct) 및 테스트 시작 직전에 최초 단 한번 일어난다.
그리고, 본격적으로 test 로직을 구성하도록 한다.
JobParameters jobParameters = jobLauncherTestUtils.getUniqueJobParametersBuilder()
.addString("filePath", tempDir.toString())
.toJobParameters();
먼저 위와 같이 jobLauncherTestUtils를 테스트 로직에 사용하여, jobParameters 구성하며, getUniqueJobParametersBuilder를 통해 내부적으로 unique한 job instance를 생성하고 중복을 피할 수 있다. 이 의미는 결국, 동일한 job을 여러번 실행해도 job parameter 중복에 대한 걱정없이 여러번 테스트를 진행할 수 있다는 의미이다.
참고로 tempDir의 경우,
@TempDir
private Path tempDir;
위와 같이 @TempDir 어노테이션을 활용하여, tempDir 변수에
C:\Users\user\AppData\Local\Temp\junit567890123456
위와 같이 실제 존재하는, 임의의 경로를 만들어 변수에 경로를 할당한다.
JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameters);
그리고 최종적으로 testUtils를 통해 launchJob 메서드를 실행하여, 실제 운영/개발 환경에서 배치를 실행하는 것과 동일한 상태, 방식으로 job을 실행하고 그 결과를 jobExecution으로 반환받는다.
이때 postConstruct를 통해 주입받은 job 객체를 실행하게 된다.
참고로, 운영/개발 application 내에 batch job 객체가 여러개 등록이 되어있다면, test 코드에서는 이 중 테스트 대상 job 1개만 유일하게 지정해주어야 한다. 실무에서는 당연히 1개 이상의 job이 등록되어있을 것이므로, testUtil에 테스트 대상 1개의 job만 등록하고 이에 따라 테스트 도메인 역시 적절하게 구성해주어야겠다.
그리고 가장 중요한 테스트 방식은 아래와 같다.
- jobExecution의 배치 job 종료 상태 및 최종 exit status 상태
- 만들어진 파일의 총 줄(line) 개수 및 파일의 상태
List<String> expectedLines = Files.readAllLines(expectedFile);
List<String> actualLines = Files.readAllLines(actualFile);
Assertions.assertLinesMatch(expectedLines, actualLines);
테스트 결과 만들어진 파일은 실제 기대한 파일(expected file)과 비교하면서 실행 결과의 정당성을 확인한다. 위의 경우엔 줄 수로 확인하였는데, 이 외 파일의 이름이나 파일의 파싱 등으로 비교하면서 확인가능하겠고 이외 jobRepository 상태내역을 확인하여 간접적으로 테스트 결과를 확인할 수도 있겠다.
테스트용 샘플은 jdbcTemplate을 이용하거나, instacio를 활용하여 구성하도록 하며, 테스트 샘플 구성 시 중요한 점은 어느 환경에서나 독립된 테스트가 가능하여 테스트 결과의 일관성을 확보하는 것이다.

테스트 로직을 실행해보고 정상적으로 e2e test가 good result를 내는지 확인한다.
참고로,

테스트 실행 시마다 위와 같이 test용 db를 초기화(drop) 및 create하는 스키마 sql를 실행하도록 할 수 있다.
//테스트 종료 후
@AfterEach
void cleanup() {
jdbcTemplate.execute("TRUNCATE TABLE infearlearn_students RESTART IDENTITY");
}
이에 대한 환경 초기화에 대한 안전장치로 truncate를, 테스트 종료 시 마다 실행하도록 구성할 수 있겠다.
이러한 사소한 점 하나하나가 맞물려 독립적으로 실행되는 테스트 환경 구성에 도움이 될 수 있음을 기억하자.
jobLauncherTestUtils는 위와 같은 e2e test뿐만 아니라, step이 여러개일때 각 step에 대한 개별적인 동작, 안정성을 테스트하기 위해 spring batch에서 특별하게 제공해주는 테스트 전용 도구이다. 뿐만 아니라, step 내부적으로 ItemReader, ItemProcessor, ItemWriter 등 각각의 컴포넌트 별 독립적인 테스트 환경 및 도구를 제공한다.
바로 이 부분에서 batch test util이 빛을 발한다.
위 step 로직에서 compositeStepExecutionListener를 구성하여, 다수의 stepExecutionListener를 등록하였으며 이에 대한 Step을 별도로 추가 구성하였다고 가정하자.

이 다수의 Step 중, 단 하나의 step과 여기에 수반되어있는 listener에 대해 그 동작을 테스트하고자, step에 대한 unit test를 작성해야 한다.
이때 unit test의 테스트 대상은 아래와 같다.
이러한 테스트 내용에 대해 정리하고, 검증대상에 맞는 테스트 로직을 설계하여 구성하도록 한다.
먼저, step 내부의 itemWriter를 실행하기 위해 필요한 jobParameter를 구성한다.
JobParameters jobParameters = jobLauncherTestUtils.getUniqueJobParametersBuilder()
.addString("filePath", tempDir.toString())
.toJobParameters();
이후 step을 실행한 후, jobExecution 인스턴스를 추출한다.

참고로, jobLauncherTestUtils의 launchStep은 위와 같이 jobExecution 형태로 반환하여, step 내부적으로 stepExecution 변수를 jobExecution 변수로 공유를 잘 하였는지 확인할 수 있겠다.
public synchronized List<Throwable> getAllFailureExceptions() {
Set<Throwable> allExceptions = new HashSet<>(failureExceptions);
for (StepExecution stepExecution : stepExecutions) {
allExceptions.addAll(stepExecution.getFailureExceptions());
}
return new ArrayList<>(allExceptions);
}
jobExecution은 내부 멤버변수에 stepExecution 정보들을 set 자료구조로 담고있다.
step 테스트코드의 유의해야할 점은, jobExecution 내부에서, step을 1개 실행한 정보, 즉 1개의 stepExecution 정보에 대한 HashSet으로부터 추출하여 확인을 해야한다는 점이다.
이를 iterator를 활용하여 set으로부터 stepExecution을 추출해오면 된다.
StepExecution stepExecution = jobExecution.getStepExecutions().iterator().next();
이 stepExecution을 통해 검증 항목을 테스트한다.
assertThat(stepExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED);
assertThat(ExecutionContextTestUtils.getValueFromJob(jobExecution, "brainwashResistanceCount")).isEqualTo(1L);
Assertions.assertLinesMatch(expectedLines, actualLines);
이때 ExecutionContextTestUtils을 활용하여, stepExecution을 jobExecution으로 변수공유가 잘 이루어졌는지 확인이 가능하다.
테스트로직에서 자체적으로 생성한 jobExecution을 활용해도 되고, testUtils에서 제공하는 getValueFromJob(Step)과 같은 context 정보를 활용해도 된다.

이에 대한 테스트를 진행하여 정상적인 동작이 이루어졌는지 확인할 수 있다.
다른 테스트 대상 step의 경우, tasklet으로 구성되어있다고 가정하여 jobParameter 대신 jobExecutionContext를 구성하여 전달할 수도 있다.
JobExecution stepJobExecution =
jobLauncherTestUtils.launchStep("brainwashStatisticsStep", jobExecutionContext);
이 jobExecutionContext에 저장된 변수를, taskletstep 실행 이후에 stepExecution에 저장된 변수를 추출하도록 하며,
Double brainwashSuccessRate = ExecutionContextTestUtils.getValueFromStep(stepExecution, "brainwashSuccessRate");
이후 stepExecutionContext를 통해, 예상한 값과 실제 step 결과와 같은지 테스트 코드를 작성하고, 이를 확인해본다.
assertThat(brainwashSuccessRate).isEqualTo(80.0);
참고로, launchStep을 통해 단독적으로 step을 여러번 실행하더라도, 위에서 기술하였듯이 내부적으로 job parameter를 난수로 생성하기에 job instance의 중복 생성은 방지할 수 있다.

이에 대한 unit step test를 실행하여, 정상동작을 확인해보도록 한다.

여기서 주목해야할 점은 SimpleJob의 실행 job 이름이 testJob으로 되어있다는 점이다.
테스트 대상이 개별적인 unit step에 대한 테스트 로직이므로, 이를 실행하기 위해 임의의 testJob을 생성, 실행하는 구조에 대해 유의하도록 하자.
참고로, testUtils의 launchStep은 내부적으로 StepRunner라는 구현체의 launchStep 메서드를 호출하여 단독적으로 step을 실행할 수 있는데,
public JobExecution launchStep(Step step, JobParameters jobParameters,
@Nullable final ExecutionContext jobExecutionContext) {
//
// Create a fake job
//
SimpleJob job = new SimpleJob();
job.setName(JOB_NAME);
job.setJobRepository(this.jobRepository);
...
이때 SimpleJob 구현체를 생성하여 임시적으로 testJob을 생성한 후, launch step을 통해 step 단독 실행 및 테스트가 가능하도록 batch 프레임워크가 구성되어있다.
지금까지 spring batch job, step에 대한 검증을 목적으로 하는 통합 및 단위 테스트에 대해 살펴보았다면, Step 내부의 ItemReader, ItemProcessor, ItemWriter 컴포넌트에 대한 단위 테스트에 대해서도 구성 및 테스트 동작에 대해 분석해보고자 한다.
아래와 같은 itemWriter 컴포넌트를 살펴보자.
@Bean
@StepScope
public FlatFileItemWriter<BrainwashedVictim> brainwashedVictimWriter(
@Value("#{jobParameters['filePath']}") String filePath) {
여기서 중요한 점은 stepScope 어노테이션이 사용되었다는 점, 즉 itemWriter라는 객체는 최초 프록시 객체로 생성되었다가, step 측에서 해당 writer 실 인스턴스를 참조할때 전달받은 job parameter를 통해 온전한 객체를 참조한다는 것이다.
stepScope의 프록시 객체 생성 대상의 경우, step 실행 이후에 "활성화", 즉 참조가 이루어진다는 점에서, 일반적인 spring application unit test와 매우 큰 차이점이 발생한다.
stepscope 대상 객체를 제외하고, 나머지 컴포넌트들에 대해서는 application과 동일하게 편의상 autowired를 통해 객체를 주입받아 해당 객체의 동작을 테스트할 수 있겠지만, stepScope 대상의 경우 단순한 주입만으로는 batch 특유의 바인딩/활성화 과정을 재현하지 못하기에 특정 도구를 활용하여 테스트해야만 한다.
scope 대상 빈을 테스트하기 위해, @SpringBatchTest에서는 @TestExecutionListener를 통해 해당 객체를 테스트 하기 위한 jobExecution, stepExecution을 자동적으로 생성하고 테스트 실행 전에 활성화(바인딩)한다.
..
@TestExecutionListeners(listeners = { StepScopeTestExecutionListener.class, JobScopeTestExecutionListener.class },
mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
@ExtendWith(SpringExtension.class)
public @interface SpringBatchTest {
}
여기서 중요한 부분은 StepScopeTestExecutionListener.class, JobScopeTestExecutionListener.class인데,
JobScope 대상 객체의 경우 Listener가 이를 감지하여
@Override
public void beforeTestMethod(org.springframework.test.context.TestContext testContext) {
if (testContext.hasAttribute(JOB_EXECUTION)) {
JobExecution jobExecution = (JobExecution) testContext.getAttribute(JOB_EXECUTION);
JobSynchronizationManager.register(jobExecution);
}
}
위와 같이 jobExecution을 만들어 바인딩(Manager.register)하는 것을 확인할 수 있고,
StepScope 대상 객체의 경우 Listener가 이를 역시 감지하여
@Override
public void beforeTestMethod(TestContext testContext) {
if (testContext.hasAttribute(STEP_EXECUTION)) {
StepExecution stepExecution = (StepExecution) testContext.getAttribute(STEP_EXECUTION);
StepSynchronizationManager.register(stepExecution);
}
}
위와 같이 stepExecution을 만들어 바인딩(Manager.register)하는 것을 확인할 수 있다.
즉, 실제 운용환경에서 batch 측에서 scope 해석이 이루어지기 위해 step객체를 바인딩해야 하는데, 이러한 전제조건을 지키기 위해 test 환경에서도 그대로 이를 적용하고 있다(운용환경과 동일한 조건 및 상황이다).
따라서 일전의 환경구성과 동일하게, springBatchTest 어노테이션을 정의하여 사용한다면 scope 빈을 autowired하여 사용할 수 있다.
stepScopeTestExecutionListener를 통해 stepExecution을 생성하는 것까지는 좋은데, job parameters는 어떻게 전달할 수 있을까?
springBatchTest의 StepScopeTestExecutionListener.class, JobScopeTestExecutionListener.class는 각각 getStepExecution, getJobExecution 메서드를 감지하여, stepExecution 및 jobExecution에서 jobParameter를 멤버변수로 구성한다.
정리하면,
Public JobExecution/StepExecution getJobExecution/getStepExecution(){
...
return ...Job/StepExecution;
}
위와 같은 execution 메서드를 testExecutionListener 측에서 감지하여,
protected JobExecution getJobExecution(TestContext testContext) {
...
return (JobExecution) invoker.invoke();
}
혹은
protected StepExecution getStepExecution(TestContext testContext) {
...
return (StepExecution) invoker.invoke();
}
의 과정을 통해 Job/StepExecution을 생성한다. 이때,
public class JobExecution extends Entity {
private final JobParameters jobParameters;
public class StepExecution extends Entity {
private final JobExecution jobExecution;
위와 같이 내부적으로, JobParameters가 설정되어 JobExecution, StepExecution으로 참조관계가 테스트 환경 상에서 구성이 된다. 즉, stepExecution에서 참조하는 jobExecution, jobParameters를 설정하여 jobParameter 구성을 해준다는 의미이다.
JobParameters jobParameters = jobLauncherTestUtils.getUniqueJobParametersBuilder()
.addString("filePath", tempDir.toString())
.toJobParameters();
물론 위와 같이 testUtil를 통해 jobParameter를 직접 구성하여 매개변수로 전달할 수도 있다.
결론은 운영환경에서의 동적인 jobParameter 전달 이외에, springBatchTest 어노테이션 및 여러 환경설정등을 통해 운영환경에 준하는 scope 대상 객체 및 jobParameter 등의 테스트가 가능하다.
그런데 문제가 있다. 지금은 step 차원에서의 테스트였지만, 해당 컴포넌트 단독적으로 테스트를 진행하기 위해서는 jobExecution, jobParameter 구성만으로는 어림도 없다.
하나의 컴포넌트 실행을 위해
JobInstance = ~
JobParameters = ~
JobExecuton = ~
StepExecution = ~
등, 위와 같이 jobInstance부터 stepExecution까지 순차적인 참조관계 및 객체를 생성하고 컴포넌트를 실행해야 정상적인 동작 및 테스트가 가능해진다.
다행히도 spring batch test에서는 이에 대한 방안을 제공한다.
JobScopeTestExecutionListener 클래스에서는 아래와 같이,
return MetaDataInstanceFactory.createJobExecution();
StepScopeTestExecutionListener 클래스에서는 아래와 같이,
return MetaDataInstanceFactory.createStepExecution();
execution 객체를 생성하여 제공하는데, 공통적으로 MetaDataInstanceFactory라는 팩토리 패턴들을 제공한다.
바로 이 MetaDataInstanceFactory를 통해 위의 번거로움과 일련의 과정들을 한번에 해결할 수 있다.
factory라는 변수명에서 유추할 수 있듯, JobInstance/JobExecution/StepExecution과 같은 batch 실행을 위해 필요한 객체 및 메타데이터 정보를 손쉽게 생성하고 의존관계를 한번에 해결할 수 있는 Spring Batch Test에서 제공하는 테스트 도구이다.
운용환경 상에서는 jobRepository를 통해 상태데이터를 영속화한 상태에서 execution 객체를 생성해야 했겠지만, factory를 통해 복잡한 객체 의존관계 설정 및 생성없이 간편하게 test 환경 및 객체를 구성할 수 있게 된다.

위와 같이 MetaDataInstanceFactory를 통해 JobExecution, stepExecution, jobInstance를 customized, 자세하게 말하면 test 환경 상에서 간편하게 임의로 구성하고 필요한 부분에 전달할 수 있게 된다.
writer component 단독으로 테스트하고자 한다면, 먼저 writer 컴포넌트를 autowired를 통해 주입받고, Path 경로를 주입할 변수를 생성해주도록 한다.
@Autowired
private FlatFileItemWriter<BrainwashedVictim> brainwashedVictimWriter;
private Path writeTestDir;
이후,
public StepExecution getStepExecution() throws IOException {
writeTestDir = Files.createTempDirectory("write-test");
JobParameters jobParameters = new JobParametersBuilder()
.addString("filePath", writeTestDir.toString())
.addLong("random", new SecureRandom().nextLong())
.toJobParameters();
return MetaDataInstanceFactory.createStepExecution(jobParameters);
}
위와 같이 jobParameters를 생성 후, MetaDataInstaneFactory의 stepExecution에 그대로 전달해준다. 이는 spring batch test 측에서 자동으로 탐지하는 대상 메서드로, 이 메서드를 구성하는 것 자체만으로 jobParameter, stepExecution 생성이 이루어진다.
테스트 로직 구성 시 중요한 점은 itemWriter 컴포넌트의 자원 open, close를 테스트 환경상에서도 동일하게 진행해주어야 한다는 점이다.
brainwashedVictimWriter.open(new ExecutionContext());
brainwashedVictimWriter.write(new Chunk<>(brainwashedVictims));
brainwashedVictimWriter.close();
이와 같이, itemStream의 open, close 를 테스트 로직 전후로 반드시 구성해주도록 한다.
위와 같이 StepScopeTestUtils에서 stepScope를 대신, 활성화보다는 대신 주입해주는 형태로 execution나 Chunk리스트를 전달하여, itemWriter의 단독 실행 및 이에 대한 테스트가 가능하도록 구성한다.

이렇게 한다면 위와 같이 jobParameter의 동적 생성이나 scope 활성화와 같은 번거로운 작업없이 MetaDataInstanceFactory만을 활용하여 동일한 결과를 기대하고, 작동할 수 있게 된다.
이와 같은 방법은 물론 기존에 비해서는 편하지만, 여러개의 컴포넌트들을 테스트해야하는 환경적 제약사항으로 인해, 각 stepExecution 별로 다른 jobParameter를 제공하기에는 비효율적인 방법이다.
그리고, 위의 방법은 기껏 사용한 @TempDir의 생성 이전에, stepExecution에 해당 내용을 넣어야 하는 문제로 writeTestDir라는 별도의 path 변수를 만들어 넣어주는 다소 번거롭고 부자연스러운 구성을 취하고 있다(getExecution 호출 시점에는 tempDir 변수 사용 불가, 반드시 직접 넣어주어야 한다).
테스트 stepExecution 마다 별도의 jobParameter, execution을 생성할 수 있는 방법이 없을까?
이에 대한 방안으로 StepScopeTestUtils라는 아주 편리한 도구를 batch 측에서는 제공한다.
List<BrainwashedVictim> brainwashedVictims = createBrainwashedVictims();
JobParameters jobParameters = new JobParametersBuilder()
.addString("filePath", tempDir.toString()) // 이제 @TempDir 사용 가능
.addLong("random", new SecureRandom().nextLong())
.toJobParameters();
StepExecution stepExecution = MetaDataInstanceFactory.createStepExecution(jobParameters);
위와 같이 테스트 로직 내부에 jobParameters를 구성하여 factory를 통해 stepExecution을 생성해두고,
StepScopeTestUtils.doInStepScope(stepExecution, () -> {
brainwashedVictimWriter.open(new ExecutionContext());
brainwashedVictimWriter.write(new Chunk<>(brainwashedVictims));
brainwashedVictimWriter.close();
return null;
});
이를 stepScopeTestUtils를 통해, 해당 컴포넌트를 위한 개별적인 stepExecution을 생성할 뿐만 아니라, tempDir를 정상적으로 주입받은 상태에서 테스트를 온전하게 진행할 수 있게 된다.

위와 같이 정상적인 테스트 동작 및 결과를 확인할 수 있고, 당연히 jobScope에서도 동일한 적용과 결과 확인이 가능하겠다.
Spring batch에서 제공하는 "특별한 맞춤형" 도구들과 설계, 사상 등에 대해서도 분석해보면서, 단순히 spring batch 로직을 구성하는 것을 넘어 spring batch의 품질을 향상할 수 있는 방안, 즉 spring batch TDD에 대해서도 살펴보았다.
Spring batch는 단순히 새벽에 진행하는 for 반복문, 무한 루프, 시간이 오래 소요되는 스케쥴러가 아니다.
우리는 spring batch 분석을 통해 spring batch는 분명한 설계, 구조, 사상을 바탕으로 체계적으로 진행이되는 컴포넌트이며, 스케쥴러와는 명백히 다르고 application 역시 확연하게 그 의미와 목적이 다른 또 하나의 큰 세계라는 것을 확실하게 배우고, 이해하고, 숙지하였다.
application에서는 한계가 있고 규모적으로 내결함성, 지속성 등을 확보해야 하는 환경에서, 탄탄하게, 온전하게 갖춘 좀 더 견고하고 안정적인 체계를 구성하여, 긴 시간을 통해 원하는 결과를 천천히 확보하는 Eventually Consistency의 목적을 달성할 수 있는 것이 바로 Spring Batch이다.
spring batch, 드디어 그 긴 여정을 마무리 하였다. 이를 끝이 아닌, 더 나은 출발을 위한 시작점으로 생각하자.