JpaPagingItemReader 사용 시 문제

러블리소피·2025년 1월 7일

JpaPagingItemReader를 사용하여 reader 에서 읽은 데이터를, writer 에서 해당 데이터 state 값을 업데이트하는 경우라면, 아래와 같은 상황을 고려해야 합니다.

⚠️ 문제 상황

JpaPagingItemReader는 내부적으로 OFFSET 기반 페이징을 사용하기 때문에, 데이터가 처리 도중 변경되면 페이징이 어긋날 수 있습니다. 예를 들어, writer에서 state를 변경하면 다음 페이지 조회 시 이미 처리된 데이터가 다시 조회되거나, 일부 데이터가 누락될 수 있습니다.

✅ 해결 방법 1: WHERE 조건에 변경 대상만 포함시키기

reader 쿼리에 이미 처리된 데이터를 제외하는 조건을 추가하는 것이 가장 쉬운 방법입니다.

📌 개선된 쿼리

state가 이미 변경된 데이터는 제외하고 조회하도록 조건을 추가합니다.

String query = """
    SELECT d
    FROM OrderDelivery d
    JOIN FETCH d.order o
    WHERE d.cancelFlag = 'N'
    AND o.status = '03'
    AND d.state = '6'  -- 상태가 6인 데이터만 조회
    AND d.updatedAt <= :dateLimit
    """;

📌 writer에서 상태 변경

ItemWriter에서 state 값을 변경하고 저장합니다.

public class OrderDeliveryWriter implements ItemWriter<OrderDelivery> {

    @Override
    public void write(List<? extends OrderDelivery> items) {
        for (OrderDelivery delivery : items) {
            delivery.setState("7");  // 상태 변경
        }
    }
}

✅ 해결 방법 2: Cursor 기반 처리로 변경

JpaPagingItemReader 대신 JpaCursorItemReader를 사용하는 것도 좋은 방법입니다.
Cursor 방식은 한 번에 데이터를 로드하지 않고 스트리밍 방식으로 처리하기 때문에 데이터 변경으로 인한 페이징 문제를 피할 수 있습니다.

📌 JpaCursorItemReader 설정

@Bean
public JpaCursorItemReader<OrderDelivery> jpaCursorItemReader(EntityManagerFactory entityManagerFactory) {
    return new JpaCursorItemReaderBuilder<OrderDelivery>()
        .name("jpaCursorItemReader")
        .entityManagerFactory(entityManagerFactory)
        .queryString("""
            SELECT d
            FROM OrderDelivery d
            JOIN FETCH d.order o
            WHERE d.cancelFlag = 'N'
            AND o.status = '03'
            AND d.state = '6'
            AND d.updatedAt <= :dateLimit
        """)
        .parameterValues(Map.of("dateLimit", LocalDateTime.now().minusDays(7)))
        .build();
}

✅ 해결 방법 3: 스냅샷 테이블 사용

데이터 변경으로 인한 페이징 문제를 완전히 해결하려면, 스냅샷 테이블을 사용하는 것도 고려할 수 있습니다.

📌 프로세스

  1. 배치 실행 전에 OrderDelivery 데이터를 스냅샷 테이블에 저장합니다.
  2. 배치 작업은 스냅샷 테이블을 기준으로 데이터를 읽고 처리합니다.
  3. 처리 완료 후 스냅샷 테이블을 삭제하거나 갱신합니다.

✅ 해결 방법 4: 벌크 업데이트 쿼리 사용

데이터가 매우 많고 단순한 상태 변경 작업이라면 벌크 업데이트 쿼리를 사용하는 것도 고려할 수 있습니다.
Spring Batch의 JdbcBatchItemWriter를 사용하면 더 빠르게 처리할 수 있습니다.

📌 벌크 업데이트 쿼리

String updateQuery = """
    UPDATE OrderDelivery d
    SET d.state = '7'
    WHERE d.cancelFlag = 'N'
    AND d.state = '6'
    AND d.updatedAt <= :dateLimit
    """;

📌 JdbcBatchItemWriter 설정

@Bean
public JdbcBatchItemWriter<OrderDelivery> jdbcBatchItemWriter(DataSource dataSource) {
    return new JdbcBatchItemWriterBuilder<OrderDelivery>()
        .dataSource(dataSource)
        .sql(updateQuery)
        .beanMapped()
        .build();
}

✅ 각 방법 비교

✅ 추천 방법

  • 데이터 양이 많지 않다면 → WHERE 조건 추가
  • 데이터 변경이 빈번하다면 → JpaCursorItemReader
  • 데이터가 매우 많고 성능이 중요하다면 → 벌크 업데이트 쿼리

제 상황에서는 WHERE 조건 추가 또는 JpaCursorItemReader를 추천합니다! 😊

profile
발전하는 개발자가 되고싶어요

0개의 댓글