현재 진행하고 있는 프로젝트에서 유저는 주문이라는 액션을 취할 수 있습니다
유저가 주문을 하면 해당 내역이 DB에 추가되고, 지정된 인터벌(1분, 5분...)마다 추가된 주문에 대한 정산 처리를 하게 됩니다
저는 인터벌마다 정산 처리하는 부분을 Quartz
와 Spring 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
를 등록하려면 어떻게 해야 될까요?
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
를 등록하여 사용할 수 있습니다
여기서 주의해야 할 부분 중 하나는 CompositeItemWriterBuilder
의 delegates
메소드에 빈 리스트 또는 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();
}
위와 같이 CompositeItemWriterBuilder
의 delegates
에 넘겨지는 리스트의 마지막 ItemWriter
를 null
로 설정하고 실행했습니다
실행 결과, 위와 같이 리스트 앞의 두 개의 ItemWriter
는 정상적으로 실행 된 후 마지막의 null
값이 들어가있는 ItemWriter
를 호출할 때 NullPointerException
이 발생하는 것을 알 수 있습니다