
Spring Batch를 사용해 대용량 데이터를 처리할 때 가장 먼저 고민해야 할 것은 데이터를 어떻게 읽을 것인가라고 생각한다.
이때 많이 사용되는 두 가지 방식이 바로 Cursor 기반과 Paging 기반인데, 두 방식의 동작 방식, 장단점을 정리해봤다.
1. Cursor 기반
JDBC의 ResultSet을 활용하여, DB에서 쿼리 1번으로 결과셋을 열어두고, 그 결과를 한 줄씩 순차적으로 읽는 방식
동작 방식
- `JdbcCursorItemReader`가 쿼리를 단 1번 실행
- DB는 결과를 계산한 뒤 커서(cursor) 를 열고 기다림
- Spring Batch가 `read()`를 호출할 때마다 한 줄씩 커서를 통해 받아옴
- 메모리에는 항상 1~2개의 row만 존재
@Bean
public JdbcCursorItemReader<Customer> cursorReader(DataSource dataSource) {
JdbcCursorItemReader<Customer> reader = new JdbcCursorItemReader<>();
reader.setDataSource(dataSource);
reader.setSql("SELECT id, name FROM customer");
reader.setRowMapper(new BeanPropertyRowMapper<>(Customer.class));
return reader;
}
- 쿼리를 한 번만 하는데, 그럼 데이터를 메모리에 들고 있는거 아닌가요?
Cursor 기반은 `setSql()`의 쿼리를 실행한 후, DB 커서가 열린 상태로 연결을 유지하며, Spring Batch가 `read()`를 호출할 때마다 한 줄씩 데이터를 받아오는 구조입니다.
이때 DB는 내부적으로 커서 위치만 기억하고 있으며, 클라이언트(JVM)에서는 필요한 row만 하나씩 메모리에 올리게 됩니다.
일반적으로 fetchSize 설정에 따라 일부 row를 버퍼로 받아오기도 하지만, 실제로 처리 시점에 JVM에 존재하는 row는 1~2개 수준이며, 전체 데이터를 메모리에 들고 있지 않습니다.
+ 일부 JDBC 드라이버(MySQL, PostgreSQL 등)는 fetchSize 외에도 별도의 옵션 설정이 필요합니다. 예를 들어 MySQL은 `useCursorFetch=true`, PostgreSQL은 `autoCommit=false`로 설정해야 진짜 커서 기반 스트리밍이 동작합니다.
2. Paging 기반이란?
쿼리를 여러 번 실행하면서, 결과를 페이지 단위로 잘라서 반복적으로 조회하는 방식
동작 방식
- Spring Batch가 `pageSize` 만큼씩 데이터를 쿼리
- `JdbcPagingItemReader`는 내부적으로 정렬 키 기준 페이징 쿼리를 수행
- 각 페이지는 독립적으로 조회되므로, 병렬 처리에도 적합
@Bean
public JdbcPagingItemReader<Customer> pagingReader(DataSource dataSource) {
JdbcPagingItemReader<Customer> reader = new JdbcPagingItemReader<>();
reader.setDataSource(dataSource);
reader.setPageSize(1000);
MySqlPagingQueryProvider provider = new MySqlPagingQueryProvider();
provider.setSelectClause("SELECT id, name");
provider.setFromClause("FROM customer");
provider.setSortKeys(Map.of("id", Order.ASCENDING));
reader.setQueryProvider(provider);
reader.setRowMapper(new BeanPropertyRowMapper<>(Customer.class));
return reader;
}
3. Cursor vs Paging
| 항목 | Cursor 기반 | Paging 기반 |
| 쿼리 실행 횟수 | 1회 | 페이지마다 여러 번 |
| 처리 방식 | ResultSet 커서로 스트리밍 | 정렬 기반 페이징 쿼리 반복 |
| 메모리 점유 | 낮음 (1~2 row) | 페이지 단위 (N개 row) |
| 커넥션 점유 | 길게 유지 | 짧게 반복 사용 |
| 순서 보장 | 항상 순차 | 정렬 키 누락 시 위험 |
| 실패 복원력 | 낮음 | 높음 (페이지 단위 재시도 가능) |
| 멀티스레드 지원 | Thread-unsafe | Partitioning, 병렬 처리 가능 |
| 쿼리 복잡도 | 복잡한 쿼리도 그대로 사용 가능 | 정렬, OFFSET, LIMIT 등 쿼리 제약 존재 |
- Cursor 는 Thread safe 하지 않은데, Spring Batch 에서 쓸 이유가 있나요?
Spring Batch는 대량의 데이터를 안정적으로 `읽고-처리하고-저장`하는 구조를 제공하는 배치 프레임워크이며, 멀티스레드는 그 중 하나의 옵션일 뿐, 필수는 아닙니다.
복잡한 쿼리(JOIN, GROUP BY 등)를 사용하는 경우, Paging 방식은 매번 전체 쿼리를 실행해야 하기 때문에 성능 저하가 발생할 수 있습니다. 반면 Cursor 방식은 쿼리를 한 번만 실행하고, 커서를 통해 한 줄씩 순차적으로 읽어오기 때문에 이런 복잡한 쿼리에서는 오히려 더 나은 성능을 보일 수 있습니다.
수천만 건처럼 복원성과 병렬성이 중요한 데이터는 Paging이 안전할 수 있지만, 수십만 건 내외의 이관 작업이라면 Cursor로도 충분히 처리할 수 있으며, 쿼리 비용이 높은 서비스 환경에서는 오히려 불필요한 재쿼리를 줄이는 게 더 중요합니다.
| 상황 | 추천 방식 |
| 처리량 적당, 순서 중요, 복잡한 쿼리 | Cursor + 단일 스레드 |
| 초대용량, 빠른 처리, 병렬 확장 필요 | Paging + 멀티스레드 |
- 그럼 단일 스레드에서는 무조건 Cursor 가 좋은가요?
커서 기반 처리에서는 지속적으로 열린 커넥션을 장시간 유지해야 하므로 타임아웃이나 세션 끊김 위험이 항상 존재합니다.
이 리스크를 줄이려면 DB/커넥션풀/네트워크 설정을 조율하거나, 필요 시 Paging 기반 구조로 전환하는 게 현실적인 해결책입니다.
따라서 단일 스레드 환경에서도 무조건 Cursor가 더 좋다고 단정지을 수는 없습니다. 데이터 규모, 처리 시간, 실패 복원 여부 등을 종합적으로 고려해서 선택하는 것이 중요합니다.
| 조건 | 이유 |
| 처리 시간이 10~20분 이상 예상됨 | 커넥션을 너무 오래 잡고 있으면 세션 타임아웃 위험 |
| 중간 실패 시 재시도가 필요한 경우 | Cursor는 중간 실패 시 위치 복원 어려움 |
| DB 커넥션 수가 제한적인 환경 | 한 커넥션을 독점하면 다른 작업에 영향 가능 |
| 네트워크 환경이 불안정하거나, L4 등 중간단에서 세션이 끊기는 경우 | Cursor는 커넥션 끊기면 작업 실패 |
* 참고
'IT' 카테고리의 다른 글
| [SpringBoot/JSP] 다국어 처리 (1) | 2025.08.08 |
|---|---|
| [Spring] @Transactional 이 일어나지 않는 경우 (1) | 2025.07.23 |
| [Cloudflare Tunnel] 우리집 IP 노출 없이 노트북을 연결해보자 (1) | 2025.07.16 |
| [Oracle Cloud] 인스턴스 생성 자동화 (Out of host capacity) (1) | 2025.07.03 |
| [Oracle Cloud] 오라클 클라우드 프리티어(Free Tier) 가입 (0) | 2025.07.02 |