spring batch 프로그램을 개발하는 도중 NullPointerException 이 발생했다.
멀쩡히 잘 돌아가던 코드에서 갑자기 발생한 NPE 는 정말 찾기 힘들었다.
문제상황
디버깅을 해보니 reader에서 cursorIterator 값 자체가 null 로 들어가서 doRead() 를 읽어오지 못했다.
public MyBatisCursorItemReader() {
setName(getShortName(MyBatisCursorItemReader.class));
}
@Override
protected T doRead() throws Exception {
T next = null;
if (cursorIterator.hasNext()) { // cursorIterator 에 값이 null 로 들어감
next = cursorIterator.next();
}
return next;
}
그래서 cursorIterator 을 찾아서 doOpen() 까지 더 거슬러 올라가보니 sqlSessionFactory 에는 값이 잘 들어가지만, sqlSession 에 값이 들어가지 못하고 있었다. 그리고 애초에 doOpen() 자체가 실행되지 않고 있었다.
@Override
protected void doOpen() throws Exception {
Map<String, Object> parameters = new HashMap<>();
if (parameterValues != null) {
parameters.putAll(parameterValues);
}
Optional.ofNullable(parameterValuesSupplier).map(Supplier::get).ifPresent(parameters::putAll);
// sqlSessionFactory 에는 값이 있으나, sqlSession 에 null 이 들어감
sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
cursor = sqlSession.selectCursor(queryId, parameters);
cursorIterator = cursor.iterator();
}
원인 분석
db 설정 자체가 문제인 것인가? 싶어서 엉뚱한 곳에서 거의 이틀간 삽질을 했었다......
그러다 이동욱 개발자님께서 과거에 올려주셨던 포스팅을 보고 해결할 수 있었다.
https://jojoldu.tistory.com/132?category=902551
Spring Batch에서 @StepScope 사용시 주의사항
안녕하세요? 이번 시간에는 SpringBatch를 처음 사용하면서 했던 실수 중 하나를 정리하려고 합니다. 모든 코드는 Github에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다. (공부한 내용을
jojoldu.tistory.com
위 블로그 내용과 정확히 일치하는 문제가 발생했다. 그리고 에러가 나는 과정도 비슷했다.
1. Spring EL 을 사용한 JobParamter 주입
초반에는 파라미터를 받는 코드가 없었다. 중간부터 Spring EL 을 이용해서 파라미터 필드 주입 코드를 추가하고 @StepScope 애노테이션을 추가했다.
@Bean
@StepScope
public ItemReader<MyData> myBatisCursorReader(
@Value("#{jobParameters[baseDt]}") String baseDt) {
MyBatisCursorItemReader<MyData> reader = new MyBatisCursorItemReader<>();
reader.setQueryId("com.example.mapper.MyDataMapper.fetchDataByBaseDt");
reader.setParameterValues(Map.of("baseDt", baseDt));
return reader;
}
그러자 NPE 가 발생했다. 아마 블로그 올려주시지 않으셨다면 한 2~3일은 더 삽질하지 않았을까..?
2. @StepScope 주입 설정과 JobParameter 주입 시점 불일치
Spring 빈의 기본 스코프는 Singleton 이다. Singleton 빈은 애플리케이션 시작 시점에 즉시 생성되며, 이후 재사용된다.
그렇다면 빈이 생성될 때는 jobParameters 값은 없는 상태이다.
jobParameters 값은 런타임 시점에 동적으로 주입되기 때문에 빈과 스코프가 맞지 않다. 그래서 jobParameter 는 @StepScope 와 함께 사용된다.
@StepScope과 프록시 객체
@StepScope 는 빈을 Step 실행 시점에 동적으로 생성할 수 있도록 도와준다. @StepScope 가 붙은 빈은 프록시 객체로 우선 등록된다. 프록시 객체의 경우 실제 빈에 대한 레퍼런스만 가지고 있고, 바로 초기화하지 않는다. Step이 실행될 때 jobParameters가 전달되며 프록시 객체는 이 값을 기반으로 실제 빈을 생성한다.
리턴타입으로 명시한 ItemReader 의 경우 인터페이스에 read() 만 있고 open, close 가 없기 때문에 open 이 필요했던 MybatisCursorItemReader 의 경우 NPE 가 발생하는 것이었다.
해결 방법
1. 구체적인 구현체 리턴 타입 지정
아래와 같이 구현체를 리턴타입으로 지정해주면 해결할 수 있다.
@Bean
@StepScope
public MybatisCurosrItemReader<MyData> myBatisCursorReader(
@Value("#{jobParameters[baseDt]}") String baseDt) {
MyBatisCursorItemReader<MyData> reader = new MyBatisCursorItemReader<>();
reader.setQueryId("com.example.mapper.MyDataMapper.fetchDataByBaseDt");
reader.setParameterValues(Map.of("baseDt", baseDt));
return reader;
}
깨달음
이런 문제를 겪을 때마다 스프링 배치 프로그램의 구조적 설계와 동작 원리를 깊이 있게 이해하는 것이 중요하다는 것을 깨닫게 된다. 단순히 무지성 reader, processor, writer 코드 작성하는 데 그치지 않고, 각 구성 요소의 동작 원리를 공부하며 신중히 설계해야겠다고 다짐한다.
1. Spring Batch 의 빈 생명주기와 스코프 이해의 필요성
2. 프록시 객체와 Lazy Initialization 에 대한 개념
출처
'공부 > Spring batch' 카테고리의 다른 글
spring batch 의 스코프 - @JobScope, @StepScope (0) | 2024.12.19 |
---|