[Spring Batch] 하나의 Step에서 여러개의 ItemWriter를 사용하는 방법
신규 도메인에서 배치 작업을 하는 도중 해결했던 문제 경험에 대해서 공유하고자 합니다.
예시)
배치로 일련의 작업을 수행한 뒤 엔티티를 JpaItemWriter를 통해 저장 / 변경하고,
JdbcBatchItemWriter를 이용해서 연관 관계가 없는 단순 데이터용 테이블을 저장 / 변경한다는 가정을 해보겠습니다.
처음에 생각한건 하나의 Job에다가, 두 개의 Step을 만드는 것이었는데, 하나의 Step에서 두 개의 ItemWriter를 사용할 방법이 없을까?
찾다보니 CompositeItemWriter를 발견했습니다.
예제를 참고할 만한 자료가 없었지만, 다행이게도 내부 구현이 복잡하지 않아서 직접 코드를 짜면서 실험해도 금방 만들 수 있었습니다.
CompositeItemWriter는 여러개의 ItemWriter를 갖고 있으며, write가 실행되면 등록한 ItemWriter들을 순차적으로 실행하는 구조입니다.
예제)
- Product리스트를 가져와서 가격을 update하고 (JpaItemWriter) , JdbcBatchItemWriter로 Order를 새로 생성하는 배치 작업이 있다고 해보겠습니다.
(예제가 현실적이지 않더라고 CompositeItemWriter를 테스트하는 용도니 가정하고 진행해보겠습니다.)
@Slf4j
@Configuration
@RequiredArgsConstructor
public class CompositeItemWriterJobConfig {
private static final String BEAN_NAME = "compositeItemWriterJob";
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
private final EntityManagerFactory entityManagerFactory;
private final DataSource dataSource;
@Value("${chunkSize:1000}")
private int chunkSize;
@Bean(BEAN_NAME)
public Job CompositeItemWriterJob() {
return jobBuilderFactory.get(BEAN_NAME)
.start(compositeItemWriterStep())
.build();
}
@Bean
public Step compositeItemWriterStep() {
return stepBuilderFactory.get("compositeItemWriterJobStep")
.<Product, Product>chunk(chunkSize)
.reader(compositeItemWriterReader())
.processor(compositeItemWriterProcessor())
.writer(compositeItem())
.build();
}
@Bean
public JpaCursorItemReader<Product> compositeItemWriterReader() {
return new JpaCursorItemReaderBuilder<Product>()
.name("compositeItemWriterReader")
.entityManagerFactory(entityManagerFactory)
.queryString("select p from Product p")
.build();
}
@Bean
public ItemProcessor<Product, Product> compositeItemWriterProcessor() {
return Product::increaseAmount;
}
@Bean
public CompositeItemWriter<Product> compositeItem() {
final CompositeItemWriter<Product> compositeItemWriter = new CompositeItemWriter<>();
compositeItemWriter.setDelegates(Arrays.asList(updateProduct(), insertOrder())); // Writer 등록
return compositeItemWriter;
}
@Bean
public JpaItemWriter<Product> updateProduct() {
final JpaItemWriter<Product> itemWriter = new JpaItemWriter<>();
itemWriter.setEntityManagerFactory(entityManagerFactory); // update product
return itemWriter;
}
@Bean
public JdbcBatchItemWriter<Product> insertOrder() {
return new JdbcBatchItemWriterBuilder<Product>()
.dataSource(dataSource)
.sql("INSERT INTO orders(product_id, amount) VALUES (:id, :amount)") // insert order
.beanMapped()
.build();
}
}
테스트코드
@SpringBatchTest
@SpringBootTest(classes = {CompositeItemWriterJobConfig.class, TestBatchConfig.class})
class CompositeItemWriterJobConfigTest {
@Autowired
JobLauncherTestUtils jobLauncherTestUtils;
@Autowired
ProductRepository productRepository;
@Autowired
OrderRepository orderRepository;
@Test
@DisplayName("product를 업데이트하고 order를 insert한다.")
void name() throws Exception {
final LocalDate today = LocalDate.now();
for (int i = 0; i < 10; i++) {
productRepository.save(new Product(i * 1000L, today));
}
jobLauncherTestUtils.launchJob();
final List<Product> all = productRepository.findAll();
assertThat(all.size()).isEqualTo(10);
final List<Order> orders = orderRepository.findAll();
assertThat(orders.size()).isEqualTo(10);
}
}
디버그를 통해 테스트를 실행해보면 CompositeItemWriter를 확인해보면
등록해놓은 ItemWriter들이 차례대로 동작하는 것을 확인할 수 있습니다.
1. JpaItemWriter 실행 부분
2. JdbcBatchItemWriter 실행 부분
최종적으로 테스트가 정상적으로 수행되고 쿼리가 정상적으로 나오는 걸 확인할 수 있습니다.
해당 코드는 깃허브에서 확인 가능합니다.
'Spring Data & Batch' 카테고리의 다른 글
Spring Batch 통합 테스트 환경 구축하기 (0) | 2022.08.03 |
---|---|
스프링 배치에서 JobParameters을 더 편하게 사용하는 방법 (0) | 2022.06.23 |
스프링과 EntityManager의 동시성 비밀 (0) | 2021.11.20 |
Optimistic Lock으로 선착순 한 명 쿠폰을 만들어보자 (0) | 2021.08.28 |
One-to-One 관계에서 Lazy 로딩은 언제 동작되는 것일까? (0) | 2021.06.17 |