Spring Batch 5.0 테스트 전략 가이드: 단위부터 통합까지 완벽 정복

안녕하세요, Code Camp 독자 여러분!
지난 시간에는 배치의 성능을 극한으로 끌어올리는 방법에 대해 이야기했었죠. 하지만 속도보다 더 중요한 것이 있습니다. 바로 ‘안전성’입니다.

수백만 건의 데이터를 처리하는 도중에 코드가 멈추거나, 잘못된 계산 결과가 DB에 저장된다면? 상상만 해도 끔찍한 일입니다.
오늘은 여러분의 배치가 365일 안전하게 돌아갈 수 있도록 돕는 Spring Batch 테스트 전략의 모든 것을 파헤쳐 보겠습니다.


목차

  1. 배치 테스트가 어려운 이유와 해결책
  2. 단위 테스트: Reader, Writer, Processor 고립시키기
  3. 통합 테스트의 핵심: @SpringBatchTest와 JobLauncherTestUtils
  4. Scope가 적용된 빈(Bean) 테스트하기
  5. 테스트 속도 향상: 가벼운 환경 구성 팁
  6. 실무 트러블슈팅: Dirty Context와 리소스 정리

1. 배치 테스트가 어려운 이유와 해결책

일반적인 웹 애플리케이션 테스트와 달리 배치 테스트는 몇 가지 진입 장벽이 있습니다.

  1. 복잡한 설정: Job, Step, Reader, Writer 등 수많은 빈(Bean)들이 얽혀 있습니다.
  2. 실행 시간: 전체 Job을 실행하려면 시간이 오래 걸립니다.
  3. 데이터 의존성: DB에 특정 상태의 데이터가 있어야만 테스트가 가능한 경우가 많습니다.

해결책:
우리는 ‘피라미드 테스트 전략’을 사용해야 합니다.
Unit Test (70%): 비즈니스 로직(Processor)만 떼어내어 빠르게 검증.
Slice Test (20%): 특정 Step 하나만 실행하여 Reader/Writer 연결 검증.
End-to-End Test (10%): 전체 Job 실행 검증.

2. 단위 테스트: Reader, Writer, Processor 고립시키기

가장 쉬우면서도 효과적인 방법은 비즈니스 로직이 들어있는 ItemProcessor를 POJO(Plain Old Java Object)로 보고 테스트하는 것입니다. Spring 컨텍스트를 띄울 필요가 없어 속도가 매우 빠릅니다.

예제: VIP 고객 선정 로직 테스트

// 비즈니스 로직 (POJO)
public class VipUserProcessor implements ItemProcessor<User, User> {
    @Override
    public User process(User user) {
        if (user.getTotalAmount() >= 1_000_000) {
            user.setGrade(UserGrade.VIP);
            return user;
        }
        return null; // 100만원 미만은 필터링
    }
}

// 단위 테스트 (JUnit 5)
class VipUserProcessorTest {

    @Test
    void 구매금액이_100만원_이상이면_VIP가_된다() {
        // given
        VipUserProcessor processor = new VipUserProcessor();
        User user = new User("tester", 1_000_000);

        // when
        User result = processor.process(user);

        // then
        assertThat(result).isNotNull();
        assertThat(result.getGrade()).isEqualTo(UserGrade.VIP);
    }
}

3. 통합 테스트의 핵심: @SpringBatchTest와 JobLauncherTestUtils

Spring Batch 4.1부터 도입된 @SpringBatchTest 어노테이션은 테스트 설정을 획기적으로 줄여줍니다.
이 어노테이션을 사용하면 JobLauncherTestUtilsJobRepositoryTestUtils를 자동으로 주입받을 수 있습니다.

전체 Job이 아닌 ‘특정 Step’만 실행하기

배치 테스트의 핵심은 Job 전체를 돌리지 않고, 내가 수정한 Step 하나만 돌리는 것입니다.

@SpringBatchTest
@SpringBootTest(classes = {VipUserJobConfig.class, TestBatchConfig.class})
class VipUserJobTest {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Autowired
    private JobRepositoryTestUtils jobRepositoryTestUtils;

    @AfterEach
    public void tearDown() {
        jobRepositoryTestUtils.removeJobExecutions(); // 테스트 데이터 정리
    }

    @Test
    void VIP_선정_Step만_실행_테스트() {
        // given: 테스트용 JobParameter 설정
        JobParameters jobParameters = new JobParametersBuilder()
                .addString("targetDate", "2024-01-01")
                .toJobParameters();

        // when: 특정 Step 실행
        JobExecution jobExecution = jobLauncherTestUtils.launchStep("vipUserStep", jobParameters);

        // then
        assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED);
    }
}

4. Scope가 적용된 빈(Bean) 테스트하기

@StepScope@JobScope가 붙은 빈은 Job 실행 시점에 생성되기 때문에, 일반적인 단위 테스트로는 빈을 가져올 수 없어 에러(ScopeNotActiveException)가 발생합니다.

이때는 StepScopeTestUtils라는 유틸리티를 사용해야 합니다.

@Test
void StepScope_Reader_테스트() throws Exception {
    // given
    JbdcPagingItemReader<User> reader = vipUserJobConfig.vipUserReader(null); // 파라미터 null 주입

    // when
    int count = StepScopeTestUtils.doInStepScope(
        stepExecution, 
        () -> {
            reader.open(new ExecutionContext());
            int readCount = 0;
            while (reader.read() != null) {
                readCount++;
            }
            return readCount;
        }
    );

    // then
    assertThat(count).isEqualTo(5); // DB에 5명 있다고 가정
}

5. 테스트 속도 향상: 가벼운 환경 구성 팁

배치 통합 테스트가 느린 주범은 @SpringBootTest가 무거운 애플리케이션 컨텍스트를 띄우기 때문입니다.

  1. 최소한의 설정 로드: @SpringBootTestclasses = ...를 명시하여, 테스트하려는 Job 설정 파일과 필수 인프라 설정만 로드하세요. 전체 앱을 띄우지 마세요.
  2. H2 DB 사용: 테스트는 인메모리 DB(H2)에서 수행하여 I/O 비용을 최소화하세요.

6. 실무 트러블슈팅: Dirty Context와 리소스 정리

문제: 테스트 간 간섭 발생

테스트 A에서 변경한 싱글톤 빈의 상태가 테스트 B에 영향을 주는 경우입니다. 특히 배치에서는 JobLauncher 등이 공유될 때 문제가 됩니다.

해결책: @DirtiesContext

테스트 메서드나 클래스에 @DirtiesContext를 붙이면, 해당 테스트가 끝난 후 스프링 컨텍스트를 강제로 재생성합니다. 속도는 느려지지만, 테스트 격리성을 확실하게 보장합니다.

@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
class ComplexJobTest {
    // ...
}

마치며

“테스트 코드가 없는 배치는 시한폭탄과 같다.”
제가 주니어 시절 선배님께 들었던 말입니다.

오늘 소개한 JobLauncherTestUtils단위 테스트 기법을 활용한다면, 수정 사항이 생겨도 두려움 없이 배포할 수 있는 탄탄한 시스템을 만들 수 있을 것입니다.

다음 시간에는 Spring Batch와 Jenkins/Airflow를 연동하는 스케줄링 전략에 대해 알아보겠습니다.
Code Camp와 함께, 오늘도 안전한 배포 되세요!

관련 글 보기