이 구조의 핵심은 **읽기는 동기, 처리는 비동기, 쓰기는 동기(Future Unwrapping)**이다.
TaskExecutor에 작업을 제출(Submit)한다.Future<T>를 리턴하고 다음 데이터를 받는다.ExecutorService(TaskExecutor)가 필요하다.ASyncItemWriter는 List<Future<T>>를 받는다.Future.get()을 호출해 결과가 나올 때까지 기다린다.핵심: Writer가
Future.get()으로 대기하는 동안, Processor의 스레드 풀에서는 병렬로 로직이 수행된다.
Writer는 단지 "결과 수집기" 역할을 할 뿐이다.
AsyncItemProcessor를 사용할 떄 성능 병목은 Chunk Size와 Thread Pool Size의 불일치에서 발생한다.
Thread Pool Size >= Chunk SizeChunk=100, Pool=10이라면?Thread Pool Size가 100이어도 HikariCP Maximum Pool Size가 10이라면, 나머지 90개 스레드는 DB 커넥션을 얻기 위해 블락 상태가 된다. 컨텍스트 스위칭 비용만 낭비하게 된다.delegate writer를 실행한다.만약 프로젝트가 Java 21 + Spring Boot 3.2 이상이라면, 복잡한 스레드 풀 튜닝 없이 **가상 스레드(Virtual Threads)**를 쓰자.
I/O Bound 작업에서 압도적인 효율을 보여준다.
@Bean
fun taskExecutor(): TaskExecutor {
return SimpleAsyncTaskExecutor().apply {
setVirtualThreads(true)
}
}
이 경우 Chunk Size만 API 허용량에 맞춰 조절하면 된다.
병렬 처리를 극대화하면 Confluence API나 임베딩 서버에서 429 Too Many Requests를 뱉을 수 있다.
RateLimiter나 Retry를 Processor 내부 로직에 적용하여, 요청 실 패 시 잠깐 대기했다가 재시도하도록 안전장치를 마련하자.