Spring Batch 입문 34편 — Database Reader · Writer · Cursor vs Paging

2026-05-17Spring Batch 입문에서 운영까지

Spring Batch 입문 34편. Database 처리의 두 길 — Cursor 와 Paging. JdbcCursorItemReader vs JdbcPagingItemReader 정밀 비교, StoredProcedureItemReader, JpaPagingItemReader, RowMapper · PagingQueryProvider · SqlPagingQueryProviderFactoryBean, JdbcBatchItemWriter · JpaItemWriter, transaction 안 자동 commit, flush 시점 함정·N+1 까지 정리한 학습 노트. Part 6 마무리.

📚 Spring Batch 입문에서 운영까지 · 34편 — Database Reader · Writer · Cursor vs Paging

이 글은 Spring Batch 입문에서 운영까지 시리즈 48편 중 34편이에요. 33편 까지 file 시리즈 를 마쳤다면, 이번 34편은 batch 의 가장 흔한 데이터 소스 — Database. Part 6 의 마지막 편이에요.

왜 DB 가 batch 의 중심인가

대부분 enterprise 환경 의 데이터 = DB. — 공식 reference

배치가 다루는 건 대용량이에요. 그런데 단순 SQL 한 줄로 100만 row 를 읽으면 ResultSet(query 결과를 담는 객체) 이 전체를 메모리에 적재해 OOM(Out Of Memory) 으로 죽어요. Spring Batch 는 이걸 두 가지로 풀어요. 하나는 Cursor 기반 — DB cursor(결과를 한 행씩 가리키는 포인터) 를 한 번 열어 streaming(한 행씩 흘려보내는 방식) 으로 처리. 다른 하나는 Paging 기반 — 여러 query 로 page 단위로 잘라서 fetch.

전략 메커니즘
Cursor-based DB cursor 한 번 열어 streaming
Paging-based 여러 query 로 page 단위 fetch

Cursor-based — JdbcCursorItemReader

원리

ResultSet rs = stmt.executeQuery("SELECT * FROM CUSTOMER");
while (rs.next()) {       // ← cursor 가 한 row 씩 전진
    Customer c = mapRow(rs);
    process(c);
}

DB cursor 가 streaming 의 표준 메커니즘이에요. ItemReader 의 read() 안에서 rs.next() + mapRow() 가 호출되는 구조죠.

사용 예제

@Bean
public JdbcCursorItemReader<CustomerCredit> reader(DataSource dataSource) {
    return new JdbcCursorItemReaderBuilder<CustomerCredit>()
        .name("creditReader")
        .dataSource(dataSource)
        .sql("SELECT id, name, credit FROM customer")
        .rowMapper(new CustomerCreditRowMapper())
        .build();
}

RowMapper

public class CustomerCreditRowMapper implements RowMapper<CustomerCredit> {
    @Override
    public CustomerCredit mapRow(ResultSet rs, int rowNum) throws SQLException {
        CustomerCredit cc = new CustomerCredit();
        cc.setId(rs.getInt("id"));
        cc.setName(rs.getString("name"));
        cc.setCredit(rs.getBigDecimal("credit"));
        return cc;
    }
}

Spring JDBC 의 표준 RowMapper 그대로예요. 컬럼명이 그대로 setter 와 매칭되면 BeanPropertyRowMapper.newInstance(CustomerCredit.class) 로 자동 매핑이 더 짧고요.

Cursor 의 핵심 옵션

Property 의미
fetchSize JDBC driver 에게 한 번에 fetch 할 row 수 힌트
maxRows ResultSet 최대 row 수 한도
queryTimeout query timeout (초)
ignoreWarnings SQLWarning log vs 예외
verifyCursorPosition RowMapper 안 rs.next() 호출 검증
driverSupportsAbsolute ResultSet.absolute() 지원 driver → 큰 dataset 재시작 성능 ↑
useSharedExtendedConnection cursor connection 을 Step 처리에 공유
saveState ExecutionContext 저장 여부

fetchSize 의 의미

.fetchSize(500)

JDBC driver 가 한 번 round-trip 으로 가져올 row 수예요. 너무 작으면 network overhead 가 늘고, 너무 크면 메모리 부담이 커요. 운영 권장은 100~1000.

Cursor 의 함정

가장 큰 게 connection 점유예요. cursor 가 Step 전체 동안 connection 을 잡고 있어서, 큰 batch 면 connection 1개가 long-running 으로 묶여요. 그래서 DB 의 connection idle timeout 을 넘기는 사고가 자주 나죠. 다음은 thread-safety. JdbcCursorItemReader 는 thread-safe 가 아니라, multi-threaded Step 에서 쓰려면 SynchronizedItemStreamReader 로 감싸거나 partitioning 으로 분할해야 해요. 마지막으로 JDBC driver 마다 cursor 지원 방식이 달라요 — MySQL 은 Statement.setFetchSize(Integer.MIN_VALUE) 같은 특수 처리를 따로 해줘야 해요.

Paging-based — JdbcPagingItemReader

원리

-- 페이지 1
SELECT * FROM customer WHERE status='NEW' ORDER BY id LIMIT 1000 OFFSET 0;
-- 페이지 2
SELECT * FROM customer WHERE status='NEW' ORDER BY id LIMIT 1000 OFFSET 1000;
-- 페이지 3
...

매 페이지가 독립 query 예요. connection 을 매 페이지 open/close 하니까 DB 의 connection idle timeout 영향을 받지 않아요.

사용 예제

@Bean
public JdbcPagingItemReader<CustomerCredit> reader(
        DataSource dataSource, PagingQueryProvider queryProvider) {

    Map<String, Object> params = new HashMap<>();
    params.put("status", "NEW");

    return new JdbcPagingItemReaderBuilder<CustomerCredit>()
        .name("creditReader")
        .dataSource(dataSource)
        .queryProvider(queryProvider)
        .parameterValues(params)
        .rowMapper(new CustomerCreditRowMapper())
        .pageSize(1000)
        .build();
}

PagingQueryProvider — DB 별 dialect

@Bean
public SqlPagingQueryProviderFactoryBean queryProvider() {
    SqlPagingQueryProviderFactoryBean provider = new SqlPagingQueryProviderFactoryBean();
    provider.setSelectClause("select id, name, credit");
    provider.setFromClause("from customer");
    provider.setWhereClause("where status = :status");
    provider.setSortKey("id");
    return provider;
}

DB 마다 LIMIT/OFFSET(페이지 잘라 가져오는 SQL 문법) 문법이 달라요. SqlPagingQueryProviderFactoryBean 이 DataSource 의 dialect(DB 별 SQL 방언) 를 자동 감지해서 맞춰줘요. 지원 DB 는 MySQL·PostgreSQL·H2 가 LIMIT ? OFFSET ?, Oracle 은 ROWNUM 또는 ROW_NUMBER() OVER, DB2 는 ROWNUMBER(), SQL Server 는 OFFSET ? ROWS FETCH NEXT ? ROWS ONLY. 거의 모든 주요 DB 가 들어 있어요.

sortKey 의 중요성

여기서 시험 함정이 하나 있어요.

provider.setSortKey("id");

sortKey 가 없거나 unique 하지 않으면 page 간 row 중복 또는 누락이 생겨요. 그래서 sortKey 는 unique 컬럼으로 — 보통 PK 가 안전해요. 비-unique 컬럼을 써야 한다면 Map<String, Order> 로 다중 정렬을 걸어주세요 (예: id DESC, createdAt ASC).

Paging 의 함정 — Deep Pagination

SELECT * FROM customer ORDER BY id LIMIT 1000 OFFSET 9999000;

OFFSET 이 아주 클 때, DB 는 9,999,000 row 를 정렬한 뒤 그만큼 skip 해요. 성능이 폭락하죠. 해결은 Keyset Pagination(마지막 키 이후만 가져오는 cursor 변형) 이에요.

SELECT * FROM customer WHERE id > :lastId ORDER BY id LIMIT 1000;

Spring Batch 6+ 가 일부 지원하고, 그 외엔 custom Paging Reader 로 직접 만들어요.

Cursor vs Paging — 정밀 비교

항목 Cursor Paging
Connection 1개 long-running 매 페이지 open/close
메모리 row 1개씩 (저점유) page 크기만큼
Long-running batch timeout 위험 ✓ 안전
재시작 안전성 위치 추적 page 번호
DB 부하 작음 (단일 query) 많음 (page 마다 query)
Deep pagination 영향 없음 성능 폭락 위험
Thread-safety X (synchronized wrap 필요)
JDBC driver 의존성 큼 (cursor 지원) 작음

대부분 운영은 Paging 권장이에요 — connection timeout 안전 + thread-safe + 단순. 예외는 진짜 streaming 이 필요하고 batch 가 길지 않은 경우, 이때만 Cursor 가 살아요.

StoredProcedureItemReader

@Bean
public StoredProcedureItemReader<CustomerCredit> spReader(DataSource ds) {
    StoredProcedureItemReader<CustomerCredit> reader = new StoredProcedureItemReader<>();
    reader.setDataSource(ds);
    reader.setProcedureName("sp_customer_credit");
    reader.setRowMapper(new CustomerCreditRowMapper());
    return reader;
}

StoredProcedure(DB 에 미리 저장된 SQL 함수) 에서 cursor 를 받는 방식은 셋이에요. ResultSet 으로 돌려주는 방식은 SQL Server·Sybase·DB2·Derby·MySQL 이 쓰고, ref-cursor(out parameter 로 cursor 반환) 는 Oracle·PostgreSQL 이 쓰는데 이땐 setRefCursorPosition(1) 을 줘야 해요. stored function 의 반환값으로 받을 때는 setFunction(true) 로 알려줘요.

Parameter 전달

List<SqlParameter> parameters = List.of(
    new SqlOutParameter("newId", OracleTypes.CURSOR),
    new SqlParameter("amount", Types.INTEGER),
    new SqlParameter("custId", Types.INTEGER)
);

reader.setParameters(parameters);
reader.setPreparedStatementSetter(parameterSetter());

쓸 일은 거의 legacy DB integration 에서예요. 신규 프로젝트라면 일반 SQL 이 무난해요.

JPA Reader — JpaPagingItemReader

@Bean
public JpaPagingItemReader<CustomerCredit> jpaReader(EntityManagerFactory emf) {
    return new JpaPagingItemReaderBuilder<CustomerCredit>()
        .name("creditReader")
        .entityManagerFactory(emf)
        .queryString("select c from CustomerCredit c where c.status = :status")
        .parameterValues(Map.of("status", "NEW"))
        .pageSize(1000)
        .build();
}

JPA 는 Hibernate StatelessSession 같은 게 없어서, paging 이 자연스러운 선택이에요.

JPA의 detach + clear

After each page is read, the entities become detached and the persistence context is cleared, to allow the entities to be garbage collected once the page is processed. — 공식 reference

매 page 끝마다 persistence context(영속성 컨텍스트, JPA 1차 캐시) 를 clear 해서 GC 가 정상으로 돌아가게 만들어요. 1차 캐시 누적을 막는 JPA batch 의 고정 패턴이고, cursor 와 갈리는 지점이기도 해요.

JPA Reader — N+1 함정

.queryString("select o from Order o")     // Order 만 select

Order 의 연관 collection (예: @OneToMany items) 이 lazy 로딩이면 각 Order 마다 추가 query 가 한 번씩 날아가서 N+1(질의 1번 + 행마다 추가 질의 N번) 이 터져요. 해결은 fetch join(연관 객체까지 한 query 로 끌어오는 JPQL) 으로 한 query 에 collection 까지 같이 가져오는 거예요.

.queryString("select o from Order o left join fetch o.items")

단 paging + fetch join 은 또 다른 함정이 있어요. Hibernate 가 메모리 페이징으로 처리한다는 경고가 떠요. 그래서 대량 batch 에서 fetch join 은 신중하게, 대안은 JdbcCursorItemReader 로 가거나 별도 ItemProcessor 단계에서 추가 query 를 거는 쪽이에요.

Database Writer — Transaction 자체가 답

Database 의 경우 별도 ItemWriter 가 필요 X (transaction 자체가 보장). — 공식 reference

Flat file 이나 XML 은 transaction 비슷한 동작을 흉내 내야 했는데, DB 는 이미 transaction 안에 있어요. 단순 INSERT 가 chunk(한 번에 처리하는 item 묶음) transaction 안에 자동으로 묶여요.

JdbcBatchItemWriter

@Bean
public JdbcBatchItemWriter<CustomerCredit> writer(DataSource ds) {
    return new JdbcBatchItemWriterBuilder<CustomerCredit>()
        .dataSource(ds)
        .sql("INSERT INTO customer (id, name, credit) VALUES (:id, :name, :credit)")
        .beanMapped()
        .build();
}

NamedParameterJdbcTemplate.batchUpdate() 를 써서 chunk 전체를 1 batch 로 INSERT 해요. DB writer 중에선 가장 빨라요.

beanMapped() vs columnMapped()

옵션 동작
beanMapped() property 이름 (예: :id) → bean getter
columnMapped() item 이 Map<String, Object>

도메인 객체를 쓴다면 거의 beanMapped() 예요.

UPSERT 패턴

.sql("""
    INSERT INTO customer (id, name, credit) VALUES (:id, :name, :credit)
    ON CONFLICT (id) DO UPDATE SET
        name = EXCLUDED.name,
        credit = EXCLUDED.credit
    """)

UPSERT(없으면 INSERT, 있으면 UPDATE) 는 DB 마다 문법이 달라요. PostgreSQL 은 ON CONFLICT, MySQL 은 ON DUPLICATE KEY UPDATE, Oracle 은 MERGE INTO. 23편에서 본 idempotency(같은 작업을 여러 번 돌려도 결과가 같은 성질) 패턴이에요.

JpaItemWriter

@Bean
public JpaItemWriter<CustomerCredit> jpaWriter(EntityManagerFactory emf) {
    return new JpaItemWriterBuilder<CustomerCredit>()
        .entityManagerFactory(emf)
        .build();
}

EntityManager.merge() 를 써서 persistence context 에 없는 entity 도 merge 해줘요. 다만 한계가 있어요. JdbcBatchItemWriter 보다 batch 성능이 떨어지고, 진짜 batch INSERT 가 나가려면 hibernate.jdbc.batch_size property 에 entity 의 @GeneratedValue(strategy = SEQUENCE) 조합이 맞아야 해요. IDENTITY 전략이면 batch insert 가 아예 불가능해요.

Flush 시점 함정 — Error on Flush vs Error on Write

여기서 시험 함정이 하나 있어요.

flush 전 buffer:
  item 1, item 2, ..., item 14, [item 15 = bad], item 16, ..., item 20

session.flush()  ← 여기서 DataIntegrityViolationException

문제는 이거예요. 15번째 item 이 bad 인데, Spring Batch 는 flush 전까지 그걸 몰라요. flush 시점에 20개 item 이 다 write 시도된 뒤에야 발견되니까 chunk 전체가 rollback 되고, skip 도 적용이 안 돼요.

The simple guideline for implementations of ItemWriter is to flush on each call to write(). — 공식 reference

해결은 Writer 가 매 write() 호출 끝에 flush 를 하는 거예요.

@Override
public void write(Chunk<? extends Foo> items) {
    for (Foo item : items) {
        session.save(item);
    }
    session.flush();    // ★ chunk 끝에 flush
}

이러면 Spring Batch 가 어느 item 이 실패했는지 정확히 추적하고, skip 로직도 정상 작동해요. JdbcBatchItemWriterJpaItemWriter 는 내부에서 자동 flush 라 신경 안 써도 되고, custom Writer 만 조심하면 돼요.

End-to-end 예제

DB → DB 가공

@Bean
@StepScope
public JdbcPagingItemReader<Customer> sourceReader(DataSource ds) {
    SqlPagingQueryProviderFactoryBean qp = new SqlPagingQueryProviderFactoryBean();
    qp.setDataSource(ds);
    qp.setSelectClause("select id, name, email");
    qp.setFromClause("from customer_source");
    qp.setSortKey("id");

    return new JdbcPagingItemReaderBuilder<Customer>()
        .name("sourceReader")
        .dataSource(ds)
        .queryProvider(qp.getObject())
        .pageSize(1000)
        .rowMapper(BeanPropertyRowMapper.newInstance(Customer.class))
        .build();
}

@Bean
public JdbcBatchItemWriter<Customer> targetWriter(DataSource ds) {
    return new JdbcBatchItemWriterBuilder<Customer>()
        .dataSource(ds)
        .sql("INSERT INTO customer_target (id, name, email) VALUES (:id, :name, :email)")
        .beanMapped()
        .build();
}

@Bean
public Step etlStep(JobRepository repo, PlatformTransactionManager tx,
                    JdbcPagingItemReader<Customer> reader,
                    JdbcBatchItemWriter<Customer> writer) {
    return new StepBuilder("etlStep", repo)
        .<Customer, Customer>chunk(500, tx)
        .reader(reader)
        .writer(writer)
        .build();
}

다른 DataSource 환경

@Bean
public Step crossDbStep(JobRepository repo, PlatformTransactionManager tx,
                        JdbcPagingItemReader<Customer> sourceReader,
                        JdbcBatchItemWriter<Customer> targetWriter) { ... }

Source DataSource 와 Target DataSource 가 다르면 transaction 도 따로예요. XA(여러 DB 를 한 트랜잭션으로 묶는 2단계 커밋 프로토콜) 없이는 target 만 commit 되고 source 는 그대로 남는 상황이 나올 수 있어요. 멱등성을 보장해두고 retry 로 푸는 게 현실적이에요.

자주 만나는 사고

사고 1: 1M row OOM

RowMapper 가 단순 query 를 돌리면 전체 ResultSet 이 메모리에 적재돼요 (특히 MySQL). JdbcCursorItemReaderfetchSize(Integer.MIN_VALUE) 를 줘서 MySQL streaming 으로 가거나, JdbcPagingItemReader 로 갈아타면 풀려요.

사고 2: Connection idle timeout

Cursor 가 Step 내내 connection 을 점유하니까 DB 의 idle timeout (보통 30분~1시간) 을 넘겨요. Paging 으로 전환하거나, 정 안되면 connection timeout 을 늘려야 해요.

사고 3: Paging 의 duplicate / missing row

sortKey 가 non-unique 인 경우예요 (예: createdAt). PK 같은 unique sortKey 로 바꾸거나 multi-column sort 로 가요.

사고 4: Deep pagination 성능 폭락

OFFSET 이 10M 이상으로 커지면 폭락해요. Keyset pagination (WHERE id > :lastId LIMIT 1000) 으로 바꾸거나 partitioning 으로 잘게 쪼개세요.

사고 5: N+1 query

JPA Reader 에서 연관 관계가 lazy 일 때 터져요. fetch join 으로 풀되 paging 과 같이 쓸 땐 신중하고, 안되면 JdbcCursorItemReader 로 가서 명시 join SQL 을 직접 적는 쪽이 안전해요.

사고 6: JpaItemWriter batch insert 안 됨

@GeneratedValue(strategy = IDENTITY)hibernate.jdbc.batch_size 가 안 잡혀 있어요. SEQUENCE 전략으로 바꾸고 spring.jpa.properties.hibernate.jdbc.batch_size=100 을 잡아주세요.

사고 7: Skip 적용 안 됨

Custom Writer 가 flush 를 안 해서 flush 시점에 chunk 전체가 rollback 돼요. Writer 의 매 write() 끝에 명시적으로 flush 를 걸어주세요.

사고 8: Multi-threaded JdbcCursorItemReader

Cursor reader 는 thread-safe 가 아니에요. SynchronizedItemStreamReader 로 감싸거나, JdbcPagingItemReader (thread-safe) 로 가거나, partitioning 으로 분할해요.

운영 권장 패턴

Pattern 1: 표준 Paging reader

@Bean
@StepScope
public JdbcPagingItemReader<Customer> customerReader(
        DataSource ds, @Value("#{jobParameters['status']}") String status) throws Exception {

    SqlPagingQueryProviderFactoryBean qp = new SqlPagingQueryProviderFactoryBean();
    qp.setDataSource(ds);
    qp.setSelectClause("select id, name, email");
    qp.setFromClause("from customer");
    qp.setWhereClause("where status = :status");
    qp.setSortKey("id");

    return new JdbcPagingItemReaderBuilder<Customer>()
        .name("customerReader")
        .dataSource(ds)
        .queryProvider(qp.getObject())
        .parameterValues(Map.of("status", status))
        .pageSize(1000)
        .rowMapper(BeanPropertyRowMapper.newInstance(Customer.class))
        .build();
}

대부분 운영에서 — Paging + Late Binding parameter 조합이에요.

Pattern 2: 표준 JdbcBatchItemWriter + UPSERT

@Bean
public JdbcBatchItemWriter<Customer> customerWriter(DataSource ds) {
    return new JdbcBatchItemWriterBuilder<Customer>()
        .dataSource(ds)
        .sql("""
            INSERT INTO customer (id, name, email)
            VALUES (:id, :name, :email)
            ON CONFLICT (id) DO UPDATE SET
                name = EXCLUDED.name,
                email = EXCLUDED.email
            """)
        .beanMapped()
        .build();
}

23편의 idempotent design 그대로예요.

Pattern 3: Keyset pagination (custom)

public class KeysetPagingReader<T> extends AbstractItemCountingItemStreamItemReader<T> {

    private long lastId = 0;
    private Queue<T> buffer = new LinkedList<>();

    @Override
    protected T doRead() {
        if (buffer.isEmpty()) {
            buffer.addAll(jdbc.query(
                "SELECT * FROM customer WHERE id > ? ORDER BY id LIMIT 1000",
                rowMapper, lastId));
            if (!buffer.isEmpty()) {
                lastId = ((Customer) buffer.peek()).getId();
            }
        }
        return buffer.poll();
    }
    // open / update / close 구현
}

Deep pagination 성능 문제를 회피해요. 26편의 Custom Reader 패턴이에요.

Pattern 4: Cross-DB ETL

@Bean
public Job etlJob(JobRepository repo, Step sourceToStaging, Step stagingToTarget) {
    return new JobBuilder("etlJob", repo)
        .start(sourceToStaging)      // Source DB → Staging (idempotent)
        .next(stagingToTarget)        // Staging → Target DB
        .build();
}

Source 와 Target 을 분리해 2 단계 ETL 로 만들고, 멱등성을 얹어 안전하게 돌려요.

Pattern 5: JPA cursor 대체

@Bean
public JpaCursorItemReader<Customer> jpaCursorReader(EntityManagerFactory emf) {
    return new JpaCursorItemReaderBuilder<Customer>()
        .name("jpaCursorReader")
        .entityManagerFactory(emf)
        .queryString("select c from Customer c where c.active = true")
        .build();
}

JPA 5+ 부터 JpaCursorItemReader 가 들어와서 JPA 환경에서도 cursor 스타일이 돼요. 단 paging 처럼 자동 detach·clear 가 일어나지 않아서, 필요하면 수동으로 clear 를 걸어줘야 해요.

시험 직전 한 번 더 — Database Reader/Writer 함정 압축 노트

  • 두 전략 = Cursor (streaming, 1 connection 유지) · Paging (page 단위 query)
  • JdbcCursorItemReader = DataSource·sql·RowMapper
  • Cursor 옵션 — fetchSize·maxRows·queryTimeout·verifyCursorPosition·driverSupportsAbsolute·useSharedExtendedConnection
  • Cursor 함정 — Connection idle timeout · NOT thread-safe · JDBC driver 의존성
  • JdbcPagingItemReader = DataSource·PagingQueryProvider·pageSize·RowMapper
  • PagingQueryProvider = DB 별 LIMIT/OFFSET dialect
  • SqlPagingQueryProviderFactoryBean = DB 자동 감지
  • sortKey = unique 컬럼 (PK 권장) — non-unique 면 page 간 duplicate/missing
  • multi-column sort 지원 (Map<String, Order>)
  • Deep pagination 함정 — OFFSET 큼 → 성능 폭락 → Keyset pagination 권장
  • Cursor vs Paging 비교 — connection · 메모리 · long-running · thread-safety · driver 의존성
  • 대부분 운영 = Paging 권장
  • StoredProcedureItemReader = 3 return 방식 (ResultSet · ref-cursor · function)
  • JpaPagingItemReader = entityManagerFactory·queryString·pageSize
  • JPA = 매 page 끝 detach + clear 자동 (1차 캐시 누적 방지)
  • JPA N+1 함정 — fetch join + paging 신중 (Hibernate 메모리 페이징 경고)
  • JpaCursorItemReader (Spring Batch 5+) = JPA 의 cursor 스타일
  • Database Writer = transaction 자체가 답 (별도 file writer 없음)
  • JdbcBatchItemWriter = NamedParameterJdbcTemplate batch update (최고 성능)
  • beanMapped() vs columnMapped()
  • UPSERT 권장 (ON CONFLICT·ON DUPLICATE KEY·MERGE INTO)
  • JpaItemWriter = EntityManager.merge() — batch insert 약함
  • batch insert 활성화 = SEQUENCE 전략 + hibernate.jdbc.batch_size
  • Flush 시점 함정 — Error on flush 시 chunk 전체 rollback → skip 불가
  • 해결 = Writer 의 매 write 끝 flush (JdbcBatchItemWriter·JpaItemWriter 자동)
  • 함정 — 1M row OOM (Cursor + fetchSize(Integer.MIN_VALUE) MySQL streaming)
  • 함정 — Connection idle timeout (Paging 으로 전환)
  • 함정 — non-unique sortKey
  • 함정 — Deep pagination (Keyset 권장)
  • 함정 — N+1 (fetch join + paging 신중)
  • 함정 — JpaItemWriter batch (IDENTITY 전략 = batch 불가)
  • 함정 — Custom Writer flush 누락 → skip 적용 불가
  • 함정 — Cursor multi-threaded → Synchronized wrap 또는 Paging
  • 패턴 — 표준 Paging + Late Binding
  • 패턴 — JdbcBatch + UPSERT
  • 패턴 — Keyset custom (deep pagination)
  • 패턴 — Cross-DB ETL (2단계 + 멱등성)
  • 패턴 — JpaCursorItemReader (JPA 환경 cursor)
  • Part 6 마무리 — 다음 글부터 Part 7 (ItemProcessor)

공식 문서: Database Item Readers and Writers 에서 원문을 확인할 수 있어요.

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!