수억 건의 데이터를 맛있게 쪼개 처리하는 방법 (with Partitioning)
핵심 내용
Spring Batch Partitioning과 Cursor Reader로 수억 건 처리의 OOM을 해결했다.
자세히 보기
수억 건의 원장 통계 데이터를 재생성하는 과정에서, 한 번에 읽어들이는 구조 때문에 OOM(Out of Memory) 이 발생했다. 해결의 출발점은 전체 기간을 월 단위로 먼저 나누고, 배치 내부에서는 다시 일 단위로 쪼개 병렬 처리하는 2차 분할 전략이었다.
Spring Batch의 병렬 처리 방식 중에서는 단일 JVM 안에서 데이터를 명확하게 나눌 수 있는 Partitioning을 선택했다. Manager Step이 전체 분할을 관리하고, Worker Step은 각자 별도의 ItemReader, ItemProcessor, ItemWriter를 가진 채 독립적으로 실행된다. 이 구조를 통해 데이터 경합을 줄이면서 병렬성을 확보했다.
Partitioning의 핵심은 Partitioner와 PartitionHandler다.
- Partitioner: 날짜 범위 같은 기준으로 작업 범위를 나누고
ExecutionContext를 만든다. - PartitionHandler:
gridSize와TaskExecutor를 바탕으로 파티션을 병렬 실행한다. gridSize,corePoolSize,queueCapacity를 함께 튜닝해 병렬 효율을 맞춘다.
실제로는 DateRangePartitioner로 하루 단위 파티션을 만들고, TaskExecutorPartitionHandler와 ThreadPoolTaskExecutor를 연결해 실행했다. queueCapacity는 0으로 두고, corePoolSize는 gridSize와 맞춰 즉시 병렬 처리되도록 설계했다. CPU 사용률이 낮으면 gridSize를 늘리고, 메모리나 DB 커넥션 병목이 보이면 줄이는 식으로 조정했다.
하지만 파티션을 잘게 나눠도, 각 Worker가 하루치 수백만 건을 한꺼번에 메모리에 올리면 다시 문제가 생겼다. 그래서 읽기 단계는 MongoPagingItemReader 대신 MongoCursorItemReader로 바꿨다. skip() 기반 페이지 조회는 뒤로 갈수록 비용이 커지지만, 커서 기반 스트리밍은 메모리 점유가 낮고 대규모 데이터에서도 일정한 성능을 유지한다.
결국 핵심은 세 가지였다.
- Partitioning으로 대상을 먼저 분할한다.
- Cursor 기반 ItemReader로 메모리 부담을 줄인다.
- Bulk Write로 쓰기 효율까지 확보한다.
대량 데이터를 다룰 때는 단순히 병렬로 돌리는 것만으로는 부족하고, 분할 방식과 읽기·쓰기 전략을 함께 설계해야 안정적으로 처리할 수 있다는 점을 보여준다.
이 한국어 요약은 AI가 자동으로 만들었습니다. 원문의 주장과 맥락은 원문에서 확인해 주세요. 저작권은 원저작자에게 있습니다.