1. 개요
JpaPagingItemReaderBuilder를 사용해 Paging Query를 처리하려 했는데, 다음과 같은 에러가 발생했다. 결론부터 말하자면 메서드의 반환타입이 문제였다.
java.lang.NullPointerException: null
at org.springframework.batch.item.database.JpaPagingItemReader.doReadPage(JpaPagingItemReader.java:192) ~[spring-batch-infrastructure-4.3.8.jar:4.3.8]
...
ItemReader는 다음과 같이 선언했다.
@Bean
@StepScope
public ItemReader<VipTrafficEntity> trafficAggregationItemReader() {
return new JpaPagingItemReaderBuilder<VipTrafficEntity>()
.name("trafficAggregationItemReader")
.entityManagerFactory(Objects.requireNonNull(trafficEntityManagerFactory.getObject()))
.pageSize(CHUNK_SIZE)
.queryString("SELECT t FROM VipTrafficEntity t ORDER BY t.createdAt")
.saveState(false)
.build();
}
doReadPage() 메서드에 브레이크 포인트를 잡고 디버깅을 해보니 entityManager가 null이여서 트랜잭션을 가져오지 못하고, 이로 인해 NPE가 발생했다. 그런데 의아했던 점은 분명 entityManagerFactory는 정상적으로 주입됐다는 것이었다.
doOpen()에서 entityManagerFactory를 통해 entityManager를 초기화 시켜준다는 것을 확인한 후, 다시 브레이크 포인트를 잡고 실행했으나 브레이크가 걸리지 않았다.
2. 해결
문제의 원인은 @StepScope이었다. ItemReader 타입의 메서드에 @StepScope를 사용하면 @Scope의 proxyMode = ScopedProxyMode.TARGET_CLASS 옵션에 의해 ItemReader 인터페이스의 프록시 객체가 리턴된다.
이 ItemReader 인터페이스는 read() 메서드밖에 없기 때문에 doOpen() 메서드가 실행되지 않는다. doOpen 메서드는
AbstractItemCountingItemStreamItemReader 인터페이스가 갖고 있다.
package org.springframework.batch.item;
public interface ItemReader<T> {
@Nullable
T read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException;
}
package org.springframework.batch.item.support;
public abstract class AbstractItemCountingItemStreamItemReader<T> extends AbstractItemStreamItemReader<T> {
protected abstract T doRead() throws Exception;
protected abstract void doOpen() throws Exception;
protected abstract void doClose() throws Exception;
// ...
}
상속관계를 보면 좀 더 이해가 쉬울 것이다.
해결 방법은 간단하다. 구체 타입인 JpaPagingItemReader를 반환하게 수정하면 된다.
@Bean
@StepScope
public JpaPagingItemReader<VipTrafficEntity> trafficAggregationItemReader() {
return new JpaPagingItemReaderBuilder<VipTrafficEntity>()
.name("trafficAggregationItemReader")
.entityManagerFactory(Objects.requireNonNull(trafficEntityManagerFactory.getObject()))
.pageSize(CHUNK_SIZE)
.queryString("SELECT t FROM VipTrafficEntity t ORDER BY t.createdAt")
.saveState(false)
.build();
}
이러한 문제로 스프링 배치는 @StepScope를 사용할 땐 구현체를 리턴타입으로 정하는 걸 권고하고 있다.
+ 참고로 ItemReader, ItemProcessor, ItemWriter를 인터페이스로 리턴할 시엔 스프링은 다음과 같은 warn 로그를 발생시킨다.
org.springframework.batch.item.ItemReader is an interface. The implementing class will not be queried for annotation based listener configurations. If using @StepScope on a @Bean method, be sure to return the implementing class so listener annotations can be used.