spring-batch로 배치 애플리케이션을 만들다 JdbcPagingItemReader와 MySqlPagingQueryProvider의 pagination 전략 때문에 겪은 두 이슈와 원인, 해결을 다룬다. Chunk 크기만큼만 발행되던 문제와 group by + table alias 조합에서 터진 SQL 문법 오류가 모두 "MySqlPagingQueryProvider는 offset/limit이 아니라 sort key를 where 조건에 넣어 페이지를 나눈다"는 동작에서 비롯됐다.
핵심 포인트- MySqlPagingQueryProvider는 offset/limit이 아니라 이전 chunk의 마지막 sort key를 기억해 그보다 큰 값만 다음 chunk로 읽는 keyset 방식으로 동작한다.
- sort key를 created_at처럼 중복 가능한 컬럼으로 잡으면 같은 값 구간이 통째로 건너뛰어져 일부 row가 발행되지 않는다.
- group by가 있으면 쿼리를 MAIN_QRY inline view로 감싸는데, sort key에 t1 같은 table alias를 붙이면 바깥에서 그 alias를 못 찾아 SQLSyntaxErrorException이 난다.
- 해결은 sort key를 auto_increment PK(id)로 바꾸고, group by 케이스에선 alias 없이 컬럼명만 넣는 것.
상세 정리- 배경: 지류 상품권 상태 변경 이벤트를 Back Office로 보내려 Transactional Outbox 패턴을 적용하고, 실시간성이 낮아 CDC 대신 배치 polling으로 outbox 테이블을 읽었다.
- 첫 증상: created_at을 now()로 100건씩 넣는 프로시저를 여러 번 호출해 테스트하니 한 번에 만든 100건 중 chunk 크기만큼(예: 10건)만 발행되고 나머지가 누락됐다.
- 원인 추적: generateRemainingPagesQuery를 따라가니 remaining query가 limit만 쓰는 게 아니라 buildSortConditions로 sort key 조건을 where에 추가하고 있었다.
- 동작 이해: 마지막으로 읽은 row의 sort key를 PagingRowMapper가 startAfterValues에 저장하고, 다음 페이지는 created_at 이 이전 마지막값보다 큰 조건으로 조회한다.
- 왜 누락됐나: A1~A100의 created_at이 전부 동일해, A10까지 읽은 뒤 created_at 이 A10보다 큰 조건이 A11~A100을 통째로 걸러내고 B1~B10으로 건너뛴다.
- 1차 해결: outbox의 id가 auto_increment라 순서도 보장되고 유일하므로 sort key를 id로 바꿔 해결. 공식 문서도 sort key에 unique 제약 컬럼을 쓰라고 명시한다.
- 두 번째 이슈: AML 배치에서 파트너별 취소 금액을 group by로 집계하며 sort key에 t1.merchant_member_id처럼 alias를 붙였더니 첫 페이지만 되고 두 번째부터 SQLSyntaxErrorException.
- 원인: group clause가 있으면 generateLimitGroupedSqlQuery가 쿼리를 MAIN_QRY inline view로 감싸고 그 바깥에 sort key 조건을 붙이는데, inline view 바깥엔 t1 alias가 없어 컬럼을 못 찾는다.
- 2차 해결: sort key에 table alias를 빼고 컬럼명만 세팅해 해결. sortKeys 맵의 key가 조건 생성과 order by에 그대로 쓰이기 때문.
왜 읽나spring-batch의 JdbcPagingItemReader로 대량 데이터를 페이징하는 백엔드 개발자가 sort key 선택(유일성·alias)에서 데이터 유실·쿼리 오류를 피하려 할 때 참고할 실전 사례.