👩🏻‍💻 Programming/Java

fetch() - 2 : 적정 fetchSize 분석, 그리고 limit의 개념

한국의 메타몽 2025. 9. 8. 14:50

목차

  1. fetchSize의 개념
  2. 적정 fetchSize를 찾아서
  3. global fetchSize와 custom fetchSize
  4. 결론 : limit과 fetchSize를 적절히 조정하자

이전 글인 fetch() -1 : ResultSetType 분석에서 ResultSetType의 개념 및 분석을 정리했다. 이번에는 fetchSize의 개념에 대해 분석해보자.


1. fetchSize의 개념

--- 커서(>)를 사용하여 다음 10개의 레코드를 가져오는 쿼리 (마지막 ID를 기준으로)
SELECT * FROM users WHERE id > 10 ORDER BY id LIMIT 10;
statement.setFetchSize(10);     // JDBC Statement 객체로 fetchSize 조정

상기 native QuerysetFetchSize의 코드를 예시로 각각 cursor limit fetchSize의 개념에 대해 이해해보자.

  • cursor : DB 서버 안에 존재하는 포인터와 같은 개념

    • 어플리케이션이 ResultSet.next()를 부를 때마다 커서가 한 칸씩 움직이며 row를 꺼내오는 구조
    • 즉, 커서는 결과의 집합을 가리키고 이동하는 지시봉의 역할
  • LIMIT : DB가 커서를 준비할 때 애초에 결과 집합을 10개만 만들도록 제약

    • 클라이언트는 10개 이상 볼 방법이 없음
    • 즉, LIMIT은 결과 집합의 크기를 자르는 장치 (DB 서버에서 컷)
  • fetchSize : 상기 native Query의 결과가 1,000만 건이라고 가정하자

    • 이때 커서는 1,000만 건 전체를 가리킬 수 있지만, 클라이언트는 fetchSize = 10 설정 덕분에 네트워크 왕복 시 10개씩 묶어서 전송받음
    • 어플리케이션에서 ResultSet.next()를 계속 호출하면, 10개 -> 다음 10개 -> ... 이렇게 목표 데이터인 1,000만건을 가져올때까지 배치 단위로 가져오게 됨
    • 즉, fetchSize는 커서에서 결과를 가져오는 단위 (네트워크 전송 batch)

이렇게 요약이 된다.
한 줄 요약하면 LIMIT은 DB가 결과를 생성하는 총 개수 제한 (최초 결과 크기)이 되고, fetchSize는 그 결과를 옮겨 담는 네트워크 단위 (왕복당 건수)가 되겠다.


2. 적정 fetchSize를 찾아서

적정 fetchSize는 상황 별로 다를 수 있을 것으로 고려된다.
예시로 한 페이지만 보여주는 페이지네이션의 경우, fetchSizeLIMIT을 같게 설정하는 것이 권장될 수 있다.

그러나 스트리밍이나 배치 처리의 경우 (= 많은 rows를 끝까지 훑음), LIMIT은 사용하지 않되 fetchSize는 GC의 압박이 되지 않는 선에서 수백 ~ 수천 단위로 튜닝이 필요될 수 있다.

내가 겪은 실무의 경우, 페이지네이션 없이 한 번에 데이터를 출력하는 조회 API였는데
실 유저에게 노출되는 환경 기준, 한 번에 가장 많은 데이터를 출력하는 유저의 케이스를 본보기로 삼아 적정 fetchSize를 선정했다.


3. global fechSize와 custom fetchSize

사용하는 DB, JPA 사용 여부, JPA의 wrapper 사용 여부(ex : QueryDSL) 등등에 따라 fetchSize의 적용 방법이 달라질 수 있다.


(1) Global FetchSize


    // DataSourceConfig.java 내부
    @Bean
    @Primary
    public DataSource dataSource(DataSourceProperty property) {
        HikariConfig config = new HikariConfig();
        DataSourceProperty.HikariProperty hikari = property.getHikari();

        config.setJdbcUrl(hikari.getJdbcUrl());
        // ... other Hikari CP settings...

        // Default FetchSize 적용
        config.addDataSourceProperty("defaultRowPrefetch", "50");

        return new HikariDataSource(config);
    }

Oracle DBHikari CP를 사용하는 경우, 상기 코드처럼 디폴트 Hikari CP 값을 적용할 수 있다.


(2) Custom FetchSize

구현하는 방법은 다양하겠지만, 나의 경우 아래와 같은 방식을 이용했다.

  1. 현재 스레드에 설정된 fetchSize 값을 가져옴
  2. 스코프 단위로 fetchSize 적용
  3. 작업 실행이 끝나면 기존 값으로 복원 or 값이 없으면 ThreadLocal을 제거

[ 예시 코드 ]

@Slf4j
@RequiredArgsConstructor
public class FetchSizeListener extends SQLBaseListener {
    @Override
    public void preExecute(SQLListenerContext ctx) {
        Integer fetchSize = FetchSizeScope.getCurrentThread();
        if (fetchSize == null) {
            return;
        }

        PreparedStatement ps = ctx.getPreparedStatement();
        if (ps == null) {
            return;
        }

        try {
            ps.setFetchSize(fetchSize);
        } catch (Exception exception) {
            log.error("FetchSizeListener error", exception);
        }
    }
}

위 코드를 활용하려면 별도의 FetchScope 클래스를 작성한 다음, (JPA 사용시) JPA Wrapper Config 클래스에 Listener를 등록해야한다.
AOP로 구현을 하려고했는데, 막상 로직을 작성해보니 스코프 단위로 구현하는 것이 간결하여 위 방법을 채택했다.


4. 결론 : limit과 fetchSize를 적절히 조정하자

분석을 해본 결과, limitfetchSize 같이 고려하여 적정 값을 선정하는 것이 베스트임을 깨달았다.
성능 개선때 잘 참고해보자!