[Spring Batch] ItemProcessor에서의 데이터 필터링 문제 해결
1. 서론
최근 프로그래머스 데브코스 프로젝트에서 Spring Batch를 사용하여 사용자 맞춤형 채용 공고 필터링 작업의 테스트를 진행하던 중, 두 번째 Job 실행 시 필터링된 데이터가 정상적으로 저장되지 않는 문제를 경험했다. 이 문제는 Spring Batch의 기본 특성과 관련된 것이었고, 이를 해결하는 과정에서 많은 것을 배우게 되었고, 이를 기록하기 위해 글을 작성한다.
참고로 아래에서 설명할 Spring Batch는 상세한 설명보단 간략히 요약한 정도이다. 트러블 슈팅을 다룬 글인 만큼 문제 발생과 이를 해결하는 과정에 집중해서 글을 작성하기 위해 Spring Batch의 상세한 설명은 Spring 공식 문서를 참고하길 바란다.
2. Spring Batch란?
Spring Batch 기본 개념
Spring Batch는 대용량 데이터 처리를 위한 프레임워크로, 배치 작업을 효율적으로 처리할 수 있게 도와준다. 주로 데이터베이스에서 데이터를 읽어와 처리한 후 다시 저장하는 등의 작업에 적합하며, 대규모 데이터를 한 번에 처리해야 하는 경우 많이 사용된다.
Spring Batch는 크게 세 가지 단계로 구성된다.
1. ItemReader: 데이터를 읽어오는 단계다. 여기서 데이터베이스, 파일 또는 API에서 데이터를 가져올 수 있다.
2. ItemProcessor: 읽어온 데이터를 처리하는 단계다. 필요한 비즈니스 로직을 적용하여 데이터를 변환하거나 필터링할 수 있다.
3. ItemWriter: 처리된 데이터를 저장하는 단계다. 데이터베이스나 파일, 또는 다른 시스템에 데이터를 저장할 수 있다.
Job과 Step
Spring Batch에서는 작업 단위를 Job이라고 부르고, 이 Job은 여러 개의 Step으로 구성된다. 각 Step은 읽기(Read), 처리(Process), 쓰기(Write)의 단계를 거치며, 이를 통해 데이터를 순차적으로 처리한다. Job은 여러 번 실행될 수 있으며, Job의 고유성을 JobParameter를 통해 구분한다.
3. 문제 발생 상황
Spring Batch를 사용하여 사람인 채용 정보 API에서 가져온 채용 공고 리스트를 사용자가 지정한 키워드에 맞게 필터링하는 작업을 수행하고 있었다. 첫 번째 Job 실행에서는 데이터가 정상적으로 필터링되어 filtered_job_posting 테이블에 저장되었다.
하지만 두 번째 Job 실행 시 필터링된 데이터가 테이블에 저장되지 않았고, 빈 테이블이 출력되었다.
참고로 스케줄러가 실행될 때마다 filtered_job_posting을 비우고 실행하게 된다. 그렇다 하더라도 두 번째 Job을 실행해서 필터링된 데이터는 저장이 되어야 하는 것이 정상이다.
위에서 설명한 대로 Spring Batch는 JobParameter로 Job을 구분한다. 현재 내가 JobParameter로 보내는 userIds는 항상 동일하다. 그렇기에 각각의 Job의 고유성을 부여해 주기 위해 현재 시간을 JobParameter에 추가해서 보내준다.
// JobParameters 설정
JobParameters jobParameters = new JobParametersBuilder()
.addString("userIds", String.join(",", userIds.stream().map(String::valueOf).collect(Collectors.toList()))) // 모든 userId를 문자열로 변환
.addString("time", String.valueOf(System.currentTimeMillis())) // 고유성을 위해 현재 시간 추가
.toJobParameters();
그러면 다른 Job으로 인식하게 된다. 이 점에 대해서는 확인이 되었다.
Spring batch를 통해 작업한 내용을 기록하는 batch_step_execution 테이블이다.
보게 되면 첫 번째 Job과 두 번째 Job을 실행한 것이 다른 Job으로 구분되어 있지만 두 번째 Job은 read, process, write가 모두 0인 것을 확인할 수 있다.
원인 분석
원인은 Spring Batch가 가지는 기본 특성에 있었다.
다른 JobParameter를 전달해 줬다 하더라도 ItemReader에서 읽어온 데이터가 이전에 읽었던 것과 동일하다면, itemProcessor에서는 이를 데이터가 "변경되지 않은" 상태 (즉 이미 필터링된 데이터가 있다고 인지)로 간주하고 null을 반환한다고 한다.
현재 나의 로직 상 itemReader에서 읽어오는 데이터는 사람인 채용 정보 API를 호출하여 받아온 job_posting 데이터다.
- 첫 번째 Job에서 읽어 온 job_posting과 두 번째 Job에서 읽어 온 job_posting이 동일했기 때문에 itemProcessor에서는 이미 필터링을 했던 데이터라고 인식하여 null을 반환하게 된다.
- null이 반환되었기에 itemWriter는 아무 작업도 수행하지 않게 되고 중간에 에러가 없었으니 Job은 Completed 상태가 되게 된다.
- 그렇기에 아무런 에러도 발생하지 않고 Job은 Completed가 되었지만 filtered_job_posting 테이블은 비워지게 된 것이다.
해결 방법
스케줄러를 통해 두 번째 Job을 실행하기 전 기존의 job_posting 데이터를 삭제하고 새로운 job_posting 데이터를 받아왔다.
이렇게 함으로써 ItemReader가 이전과 다른 데이터를 읽어오게 되었고, ItemProcessor에서도 데이터를 정상적으로 처리할 수 있었다.
결론
Spring Batch의 기본 특성의 이해에 대한 부재로 인해 발생한 문제였다. 같은 데이터가 들어왔을 때 ItemProcessor에서 자동으로 null을 반환하는 특성은 데이터 처리에서 유용할 수 있지만, 이번 테스트에서는 문제가 되었다.
실제 시나리오 상에서는 매번 job_posting을 업데이트한 후 스케줄러를 통해 Spring Batch를 수행할 것이기 때문에 수정할 로직은 없다. 단지 테스트하던 도중 만나던 이슈였다.
하지만 테스트 과정에서 발생한 이슈를 해결함으로써, Spring Batch의 동작 방식을 더욱 깊이 공부할 수 있었고, 이를 통해 앞으로 안정적인 배치 작업의 구현에 있어서 더 많은 학습의 필요성을 느꼈다.
Spring Batch는 복잡한 대용량 데이터를 효율적으로 처리할 수 있는 훌륭한 도구이지만, 그 동작 방식을 충분히 이해하고 사용해야만 예상치 못한 문제를 피할 수 있다는 것을 느끼게 되는 이슈였다.