목차
- fetchSize의 개념
- 적정 fetchSize를 찾아서
- global fetchSize와 custom fetchSize
- 결론 : 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 Query와 setFetchSize의 코드를 예시로 각각 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는 상황 별로 다를 수 있을 것으로 고려된다.
예시로 한 페이지만 보여주는 페이지네이션의 경우, fetchSize와 LIMIT을 같게 설정하는 것이 권장될 수 있다.
그러나 스트리밍이나 배치 처리의 경우 (= 많은 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 DB와 Hikari CP를 사용하는 경우, 상기 코드처럼 디폴트 Hikari CP 값을 적용할 수 있다.
(2) Custom FetchSize
구현하는 방법은 다양하겠지만, 나의 경우 아래와 같은 방식을 이용했다.
- 현재 스레드에 설정된
fetchSize값을 가져옴 - 스코프 단위로
fetchSize적용 - 작업 실행이 끝나면 기존 값으로 복원 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를 적절히 조정하자
분석을 해본 결과, limit과 fetchSize 같이 고려하여 적정 값을 선정하는 것이 베스트임을 깨달았다.
성능 개선때 잘 참고해보자!
'👩🏻💻 Programming > Java' 카테고리의 다른 글
| fetch() - 1 : ResultSetType 분석 (0) | 2025.08.26 |
|---|---|
| 자바의 스레드(Daemon Thread vs Non-daemon Thread), 그리고 좀비 프로세스 (1) | 2025.08.25 |
| @EntityListener와 @PostLoad의 이슈 (0) | 2024.12.04 |
| 동기, 비동기, 그리고 Batch의 thread-pool : thread-pool은 다다익선이 아닐까? (2) (0) | 2024.11.03 |
| ThreadPoolExecutor vs ThreadPoolTaskExecutor (0) | 2024.10.29 |