CompositeItemWriter를 사용하여 하나의 Step에 여러개의 ItemWriter 등록하기

Gongmeda·2023년 1월 15일
1
post-thumbnail

이슈

현재 진행하고 있는 프로젝트에서 유저는 주문이라는 액션을 취할 수 있습니다

유저가 주문을 하면 해당 내역이 DB에 추가되고, 지정된 인터벌(1분, 5분...)마다 추가된 주문에 대한 정산 처리를 하게 됩니다

저는 인터벌마다 정산 처리하는 부분을 QuartzSpring Batch 를 사용한 배치 애플리케이션으로 구현하기로 했는데, 정산 결과를 저장(write)하는 부분에서 한 가지 문제에 봉착하게 됩니다

정산 결과를 하나의 테이블에만 저장하는 것이 아니고 관련된 여러개의 테이블에 UPDATE 또는 INSERT 작업을 수행해야 하는데 기본적으로 하나의 Step에는 Reader , Processor , Writer 각각 하나씩만 등록이 가능하다는 문제였습니다

public Step orderResultCalculationStep() {
	return stepBuilderFactory.get("orderResultCalculationStep")
    	.<Order, Order>chunk(CHUNK_SIZE)
        .reader(orderReader())
        .writer(orderWriter1())
        .writer(orderWriter2())
        .writer(orderWriter3())
        .build();
}

위와 같이 StepBuilderFactory 에서 writer를 여러개 등록하는 듯한 코드를 작성하면 컴파일 오류 없이 정상적으로 실행이 되는데요

이는 사실 여러개의 writer를 등록하는 것이 아닌, 이전에 등록한 writer를 덮어 씌우는 코드입니다

stepBuilderFactory.get().chunk() 를 통해 반환되는 SimpleStepBuilder 의 코드를 확인해보면 알 수 있습니다

public class SimpleStepBuilder<I, O> extends AbstractTaskletStepBuilder<SimpleStepBuilder<I, O>> {
	// ...
    
    private ItemReader<? extends I> reader;

	private ItemWriter<? super O> writer;

	private ItemProcessor<? super I, ? extends O> processor;
    
    // ...
    
    public SimpleStepBuilder<I, O> reader(ItemReader<? extends I> reader) {
		this.reader = reader;
		return this;
	}

	public SimpleStepBuilder<I, O> writer(ItemWriter<? super O> writer) {
		this.writer = writer;
		return this;
	}

	public SimpleStepBuilder<I, O> processor(ItemProcessor<? super I, ? extends O> processor) {
		this.processor = processor;
		return this;
	}
}

위 코드는 SimpleStepBuilder 클래스를 축약한 코드입니다

해당 클래스는 멤버 변수로 Reader , Processor , Writer 를 하나씩 가지고 있으며 .reader , .processor , .writer 메소드는 각 멤버 변수에 인자로 받은 객체를 할당한다는 것을 알 수 있습니다

따라서 빌더에서 .writer 를 여러번 호출하는 것은 빌더의 멤버 변수를 덮어쓰는 행위일 뿐, 여러개의 writer를 등록할 수는 없다는 것을 알 수 있습니다

그렇다면 하나의 Step에 여러개의 Writer 를 등록하려면 어떻게 해야 될까요?

CompositeItemWriter

public class CompositeItemWriter<T> implements ItemStreamWriter<T>, InitializingBean {

	private List<ItemWriter<? super T>> delegates;
    
    // ...
    
    @Override
	public void write(List<? extends T> item) throws Exception {
		for (ItemWriter<? super T> writer : delegates) {
			writer.write(item);
		}
	}
    
    // ...
}

CompositeItemWriter 는 위에서 필요했던 요구사항을 충족하는 클래스입니다

내부적으로 delegates 라는 ItemWirter 의 리스트를 가지고 있으며, write 메소드 호출 시 리스트 순서대로 등록된 ItemWriter 들의 write 메소드를 호출합니다

이는 스프링 배치에서는 흔히 사용되는 패턴 중 하나인 Delegation pattern 을 활용한 방법입니다

@Configuration
public class Config {
	
    // ...
    
    @JobScope
    @Bean
    public Step orderResultCalculationStep() {
        return stepBuilderFactory.get("orderResultCalculationStep")
            .<Order, Order>chunk(CHUNK_SIZE)
            .reader(orderReader())
            .writer(orderWriter())
            .build();
    }

    @StepScope
    @Bean
    public CompositeItemWriter<Order> orderWriter() {
        List<ItemWriter<? super Order>> writers = Stream.of(
            orderResultUpdateWriter(),
            userPointUpdateWriter(),
            pointRecordWriter()
        ).collect(Collectors.toList());

        return new CompositeItemWriterBuilder<Order>()
            .delegates(writers)
            .build();
    }

    @StepScope
    @Bean
    public JdbcBatchItemWriter<Order> orderResultUpdateWriter() {
        // ...
    }

    @StepScope
    @Bean
    public JdbcBatchItemWriter<Order> userPointUpdateWriter() {
        // ...
    }

    @StepScope
    @Bean
    public JdbcBatchItemWriter<Order> pointRecordWriter() {
        // ...
    }
}

위와 같이 여러개의 JdbcBatchItemWriter 를 등록하여 사용할 수 있습니다

여기서 주의해야 할 부분 중 하나는 CompositeItemWriterBuilderdelegates 메소드에 빈 리스트 또는 null 을 포함한 리스트를 넣으면 안된다는 부분인데요

CompositeItemWriter 코드를 살펴보면 위와 같은 내용을 통해 해당 부분을 알 수 있습니다

@Bean
public CompositeItemWriter<Order> orderWriter() {
	List<ItemWriter<? super Order>> writers = Stream.of(
    	orderResultUpdateWriter(),
        userPointUpdateWriter(),
        null // null 값 삽입
    ).collect(Collectors.toList());

	return new CompositeItemWriterBuilder<Order>()
    	.delegates(writers)
        .build();
}

위와 같이 CompositeItemWriterBuilderdelegates 에 넘겨지는 리스트의 마지막 ItemWriternull 로 설정하고 실행했습니다

실행 결과, 위와 같이 리스트 앞의 두 개의 ItemWriter 는 정상적으로 실행 된 후 마지막의 null 값이 들어가있는 ItemWriter 를 호출할 때 NullPointerException 이 발생하는 것을 알 수 있습니다

참고

profile
백엔드 깎는 장인

0개의 댓글