배경 상황
Spring Batch MultiThread 환경에서 ItemReader에서는 @OneToMany(fetch = FetchType.LAZY)로 설정된 단방향 연관관계의 "One" 엔티티만 조회했습니다.
ItemProcessor 단계에서 해당 연관 엔티티의 데이터에 접근하려 하자 LazyInitializationException 발생하였습니다.
# 기본적인 chunk 기반 Spring Batch 프로세스를 이해하고 있어야 함
## ItemReader의 구현체로 JpaPagingItemReader 사용
## Single Thread로 작업 처리 시 해당 예외는 발생하지 않았으나, MultiThread 로 처리 시에만 발생
우선 LazyInitializationException은 어떤 오류인가?
영속성 컨텍스트에서 조회하고자 하는 엔티티 객체를 더 이상 관리하지 않게 되어 발생하는 예외
그렇다면 영속성 컨텍스트 관리는 언제까지 되는걸까?
선언전 트랜잭션(@Transactional) 경우 트랜잭션이 시작되면 영속성 컨텍스트가 생성되고 트랜잭션 종료 시 영속성 컨텍스트는 사라지게 된다.
명시적 트랜잭션 경우 개발자가 직접 트랜잭션과 영속성 컨텍스트를 관리(flush,clear 등)한다.
그렇다면 Spring Batch에선 어떠한 트랜잭션을 사용할까?
TaskletStep.doExecute

해당 메소드의 핵심은 stepOperations.iterate이며, iterate메소드의 인자는 StepContextRepeatCallbak의 익명 클래스 인스턴스가 들어가 있다. (참고로 stepOperations 객체는 ReptOperations의 구현체인 RepeatTemplate다.)
익명 클래스에 구현된 doInChunkContext 를 반복적으로 수행하게 되며,
doChunkContext 메소드 내에선 TransactionTemplate.execute 메소드를 호출한다.
TransactionTemplate.execute

여기서 첫 트랜잭션이 확인된다!!!
명시적 트랜잭션을 시작하고 (this.transactionManager.getTransaction 디버깅하여 확인)
action.doInTransaction 메소드 호출을 통하여 실제 tasklet(ItemReader,Processor,Writer)를 실행시키며
작업 정상 종료 시 커밋, 예외 발생 시 롤백을 처리한다.
즉, 실제 tasklet(read,process,write) 실행 전후로 트랜잭션의 생명주기가 확인되게 된다.
참고로 이 때 트랜잭션의 전파 속성은 required이다.
명시적 트랜잭션을 사용하므로 영속성 컨텍스트를 직접 관리할테고 어디서 초기화를 할까??
또 다른 트랜잭션이 chunkorientedTasklet 처리 도중 존재하더라도 위에서 발견한 트랜잭션 범위 내에 chunkorientedTasklet 작업(read,process,write)이 포함되기 때문에 영속성 컨텍스트에서 chunkorientedTasklet 처리 내 조회된 엔티티는 모두 영속성 컨텍스트를 괸리하게 되기 때문에 영속성 컨텍스트를 건드리는 부분이 없다면 LazyInitializationException은 절대 발생할 수 없다.
따라서, 직접 영속성 컨텍스트를 초기화하는 부분이 있을 것이라고 판단했다.
JpaPagingItemReader.doReadPage

해당 메소드에서 영속성 컨텍스트 초기화가 확인된다.
데이터를 읽기 전 트랜잭션 시작하고 이전 읽은 엔티티들을 DB 반영 및 현재 조회할 엔티티만 영속성 컨텍스트에서 관리하기 위해 영속성 컨텍스트 초기화(entityManager.clear)한다!!
또 하나의 트랜잭션이 열리고 닫히고 영속성컨텍스트 flush,clear를 확인하였다.
ChunkOrientedTasklet.execute

ChunkOrientedTasklet은 내부적으로 ItemReader,ItemProcessor,ItemWriter를 가지며 이들이 Chunk 프로세스 처리를 진행한다.
chunkPrivider.provide 메소드에서 Item을 ChunkSize만큼 반복해서 읽은 뒤 Input Chunk로 만들어서 반환한다.
chunkProcessor.provide 메소드에서 반환받은 InputChunk를 가공한다.
SimpleChunkProvider.provide

repeateOperation.iterate chunk size만큼 반복 실행한다.
여기서 중요한 것은 itemReader는 모든 스레드가 공유한다는 것이다.
AbstractPagingItemReaedr.doRead

doReadPage는 AbstractJpaPagingItemReader 구현체인 JpaPagingItemItemReader에 구현되어 있으며 위에서 언급되었던 메소드이다.
쿼리 결과(조회된 로우들)가 없거나 현재 인덱스(current)가 페이지 크기(pageSize=chunkSize) 이상이면 doReadPage가 호출된고, 읽을 데이터가 남아있으면 해당 데이터를 반환하게 된다.
즉, 하나의 스레드만 해당 메소드에 접근하여 한번에 하나의 item을 가져가도록 하는 메소드이다.
자 그렇다면 Multi Thread 환경에서 해당 Exception이 터진 이유는 뭘까?
JpaPagingItemReader는 모든 스레드에서 공유되며, 내부적으로 사용하는 EntityManager 또한 공유된다. 따라서, 모든 스레드는 동일한 영속성 컨텍스트를 가지게 된다.
이때 JpaPagingItemReader.doReadPage() 메소드에서 영속성 컨텍스트를 직접 초기화(clear) 하기 때문에,
한 스레드가 엔티티를 조회한 직후, 다른 스레드가 clear()를 호출하면 해당 엔티티는 detach 상태가 된다.
이후 원래 스레드에서 Lazy 연관 엔티티에 접근하려 할 때 영속성 컨텍스트에서 관리되지 않아 LazyInitializationException 발생하게 된다.
즉, Multi Thread 환경에선 clear()로 인해 영속성 컨텍스트를 통해 가질 수 있는 장점(지연초기화,더티체킹)을 사용하기가 어렵다고 본다.(= 영속성 관리가 제대로 이루어지지 않는다)
문제 발생 상황 예시
| 1번 스레드 | A 엔티티를 read()하여 processor()로 넘기려고 함 |
| 2번 스레드 | 동시에 다음 page를 read()하면서 entityManager.clear() 실행 |
| 결과 | 스레드 1번의 A 엔티티는 지연 로딩이 필요한 프록시 상태였는데, 영속성 컨텍스트에서 제거되어 lazy load 시도 → 💥 LazyInitializationException 발생 |
그렇다면 SingleThread에선 왜 발생하지 않았을까?
멀티스레드와는 달리, Single Thread 환경에서는 각 Chunk가 순차적으로 처리되며, ItemReader와 ItemProcessor가 같은 스레드 내에서 순서대로 실행된다. 이로 인해 ItemReader에서 조회한 엔티티는 영속성 컨텍스트가 유지된 상태로 Processor까지 전달되며, Lazy 로딩도 정상적으로 동작한다.
반면, Multi Thread 환경에서는 여러 Chunk가 서로 다른 스레드에서 동시에 실행되며, 공유된 JpaPagingItemReader 내부에서 entityManager.clear()가 발생하면 다른 스레드의 동일한 영속성 컨텍스트까지 영향을 받아 Lazy 로딩 시 LazyInitializationException 발생한다.
어떻게 해결해야 할까?
다음 장에서 살펴보자
참고
Spring Batch Multi-threaded Step 사용 시 chunk 구성에 대한 오해
개요 Spring Batch에서는 다양한 병렬 처리 방식을 지원하고 있습니다.
umbum.dev
+ 2025.06.26 추가
위 내용과 관련된 스택오버플로우 질문 모음
LazyInitializationException 발생 이유
'SpringBoot > 오류' 카테고리의 다른 글
| Spring Batch MultiThread 병렬 처리 시 특정 item이 n번 Process되는 이슈와 이유 (0) | 2025.05.19 |
|---|---|
| Spring Batch Partitioning에서 Job이 끝나지 않는다면 의심해보자 (1) | 2025.01.05 |
| SpringSecurity 순환 참조(circular references) 발생 (1) | 2024.09.11 |
| @Valid MethodArgumentNotValidException 처리 (0) | 2024.08.23 |
| SpringBatch JpaPagingReader 조건을 통한 조회 시 문제점과 해결방안 (0) | 2024.05.03 |