콘텐츠로 이동

JDBI: @FetchSize 옵션과 가상 스레드(Virtual Thread) 최적화

JDBI 사용 시 @FetchSize 어노테이션을 통해 데이터 로딩 성능을 튜닝할 수 있습니다. 특히 Java 21의 가상 스레드 환경에서 이 옵션이 왜 중요한지 정리합니다.


1. @FetchSize란?

  • 정의: 데이터베이스에서 결과를 가져올 때, 한 번의 네트워크 왕복(Round-trip)으로 가져올 행(Row)의 개수를 지정하는 설정입니다.
  • 작동 원리: 기본적으로 JDBC 드라이버는 매우 작은 단위(보통 10개)로 데이터를 가져옵니다. 1만 건의 데이터를 조회할 때 FetchSize가 10이라면 1,000번의 네트워크 요청이 발생하지만, 1,000으로 설정하면 10번의 요청으로 끝납니다.

2. 가상 스레드(Virtual Thread)와의 관계 및 이점

가상 스레드 환경에서 @FetchSize(1000)과 같은 설정은 단순한 속도 향상 이상의 의미를 갖습니다.

2.1 I/O 효율성 및 Unmounting 극대화

  • 가상 스레드는 I/O 작업 시 캐리어 스레드를 양보(Unmount)합니다.
  • FetchSize가 너무 작으면 빈번한 네트워크 I/O 발생으로 인해 가상 스레드의 컨텍스트 스위칭(Mount/Unmount) 오버헤드가 증가할 수 있습니다.
  • 적절한 FetchSizeI/O 대기 시간을 집약시켜 가상 스레드가 더 효율적으로 CPU 자원을 반납하고 재점유할 수 있게 돕습니다.

2.2 메모리 점유 및 스택 관리

  • 가상 스레드는 수만 개가 동시에 실행될 수 있습니다.
  • 만약 모든 가상 스레드가 FetchSize를 너무 크게(예: 10만) 설정하면, JVM 힙 메모리에 데이터가 급격히 쌓여 OutOfMemoryError가 발생할 수 있습니다.
  • 따라서 가상 스레드 환경에서는 "속도(네트워크 횟수 감소)"와 "메모리(동시 실행 수 고려)" 사이의 균형을 맞춘 적절한 FetchSize(예: 500~1000) 설정이 필수적입니다.

3. 유사한 튜닝 옵션들

JDBI 및 JDBC에서 함께 고려할 수 있는 옵션들입니다.

  • @MaxRows(n): 결과 셋의 전체 최대 행 수를 제한합니다. (메모리 보호 용도)
  • @QueryTimeout(n): 쿼리 실행 시간이 너무 길어질 경우 차단합니다. 가상 스레드가 특정 쿼리에 영원히 묶여 있는 것을 방지합니다.
  • Stream<T> 반환: JDBI에서 ResultIterable.stream()을 사용하면 전체 데이터를 메모리에 올리지 않고 FetchSize 단위로 읽으며 처리할 수 있어 가상 스레드와 궁합이 매우 좋습니다.

4. 실무 권장 설정 예시

public interface UserDao {
    @SqlQuery("SELECT * FROM users WHERE status = :status")
    @FetchSize(1000) // 한 번에 1000개씩 읽어옴
    @QueryTimeout(10) // 10초 타임아웃
    List<User> findByStatus(@Bind("status") String status);
}

5. 결론

가상 스레드를 사용한다면 "동시성(Concurrency)"이 비약적으로 높아지므로, 각 스레드가 사용하는 자원을 정교하게 제어해야 합니다. @FetchSize는 네트워크 비용을 줄이면서도 힙 메모리 폭발을 막는 중요한 스로틀링(Throttling) 장치 역할을 합니다.