신규 도메인에서 배치 작업을 하는 도중 해결했던 문제 경험에 대해서 공유하고자 합니다.

 

예시)

배치로 일련의 작업을 수행한 뒤 엔티티를 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 실행 부분

최종적으로 테스트가 정상적으로 수행되고 쿼리가 정상적으로 나오는 걸 확인할 수 있습니다.

 

해당 코드는 깃허브에서 확인 가능합니다.

 

GitHub - brick0123/spring-boot-in-action: 📚 spring pratice repository

📚 spring pratice repository. Contribute to brick0123/spring-boot-in-action development by creating an account on GitHub.

github.com