Java 스트림을 올바르게 청킹하기 — JDK에 있어야 할 것 같은 컬렉터

발행: (2025년 12월 12일 오전 12:46 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

Cover image for Chunking Java Streams the Right Way — A Collector That Feels Like It Should Be in the JDK

Chunking Java Streams the Right Way — Finally, a Collector That Feels Like It Should Be in the JDK

큰 리스트나 스트림을 균등한 크기의 청크로 나눠야 할 때 겪는 고통은 이미 익숙할 겁니다:

  • 루프를 작성한다.
  • 더 나쁘게는 중첩 루프.
  • 카운터를 사용한다.
  • 임시 리스트를 만든다.
  • 거의 동작하지만 한 가지 경계 상황에서 깨지는 로직을 만든다.

요소를 청크로 나누는 작업은 일상적인 연산 중 하나인데 어쩐지 JDK에 포함되지 않았습니다. 개발자들은 매 프로젝트마다 같은 유틸리티 메서드를 조금씩 다르게 다시 작성하곤 합니다.

수천 개의 UUID를 작은 청크로 배치해야 하는 PostgreSQL 드라이버 제한에 부딪힌 뒤, 나는 결심했습니다:

이건 Collector여야 한다. 깔끔하고, 조합 가능하며, 스트림을 위해 만들어진.

그래서 직접 만들었습니다.

이것이 Chunking Collector — Java 8+용 경량 라이브러리로, 청크 작업을 표준 라이브러리에서 제공될 법한 형태로 표현할 수 있게 해줍니다.

🔥 The Old Way: Manual Chunking (A Bit of a Mess)

List<List<T>> chunks = new ArrayList<>();
List<T> current = new ArrayList<>();

for (T item : items) {
    current.add(item);
    if (current.size() == chunkSize) {
        chunks.add(current);
        current = new ArrayList<>();
    }
}

if (!current.isEmpty()) {
    chunks.add(current);
}

동작은 하지만… 안 될 때도 있습니다:

  • 읽기 어려움
  • 실수하기 쉬움
  • 재사용 불가
  • 병렬 처리에 부적합
  • 스트림 친화적이지 않음

또한 코드 흐름을 끊어 버려, 스트림 파이프라인으로 자연스럽게 표현하고 싶을 때 방해가 됩니다.

✨ The New Way: A Collector That Just Works

Chunking Collector를 사용하면 이렇게 간단합니다:

List<List<Integer>> chunks = numbers.stream()
    .collect(Chunking.toChunks(3));

출력

[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]

읽기 쉽고, 안전하며, 예측 가능합니다. 청크 작업이 이렇게 느껴져야 합니다.

🧩 Why a Collector?

청크 작업은 본질적으로 축소 연산입니다:

  • 스트림이 들어가고
  • 리스트의 리스트가 나옵니다
  • 부수 효과가 없습니다
  • 외부로 변이가 누출되지 않습니다
  • 정렬된, 병렬, 순차 스트림 모두와 자연스럽게 작동합니다

그리고 무엇보다 스트림 철학과 완벽히 맞아떱니다:

stream.collect(Chunking.toChunks(size));

한눈에 무슨 일을 하는지 알 수 있습니다.

📦 Installation

<dependency>
  <groupId>dev.zachmaddox</groupId>
  <artifactId>chunking-collector</artifactId>
  <version>1.1.0</version>
</dependency>

또는 Gradle을 사용할 경우:

implementation 'dev.zachmaddox:chunking-collector:1.1.0'

🧠 Practical Examples That Come Up All the Time

1. Batch Processing

Chunking.chunk(records, 100)
    .forEach(batch -> processBatch(batch));

2. Database Paging

var pages = results.stream()
    .collect(Chunking.toChunks(500));

3. Parallel Workloads

Chunking.chunk(items, 10)
    .parallelStream()
    .forEach(this::processChunk);

🔥 The Real Origin: Working Around PostgreSQL IN‑Clause Limits

PostgreSQL(및 다수의 JDBC 드라이버)은 하나의 SQL 문에 들어갈 인자 리스트 크기를 제한합니다. 청크를 사용하면 파라미터화된 SQL을 통해 이 문제를 깔끔하고 안전하게 해결할 수 있습니다:

NamedParameterJdbcTemplate named = new NamedParameterJdbcTemplate(jdbcTemplate);

Chunking.chunk(ids, 500)
    .parallelStream()
    .map(chunk -> named.query(
        "SELECT * FROM users WHERE id IN (:ids)",
        Map.of("ids", chunk),
        (rs, n) -> mapRow(rs)
    ))
    .flatMap(List::stream)
    .toList();

결과

  • 드라이버 오류 없음
  • 더 작고 빠른 쿼리
  • 명확하고 유지보수 쉬운 코드
  • 병렬 처리 가능

이 한 가지 이유만으로도 라이브러리를 만들 가치가 있었습니다.

⚡ Advanced Capabilities (When You Need Them)

Chunking Collector는 이제 유연한 툴킷으로 성장했습니다:

  • 잔여 정책 (INCLUDE_PARTIAL, DROP_PARTIAL)
  • 커스텀 리스트 팩토리
  • 지연 청크 스트리밍
  • 슬라이딩 윈도우
  • 경계 기반 청크
  • 가중치 청크
  • 원시 스트림 헬퍼

핵심 API는 여전히 매우 단순합니다.

🧩 Design Philosophy

“이 API가 언제든 JDK에 포함된다면, 아무도 놀라지 않을 것이다.”

  • 의존성 없음
  • 리플렉션 없음
  • 마법 없음
  • 순수 Java만 사용
  • 매우 작은 표면적
  • 경험 많은 Java 개발자가 기대하는 그대로 동작

📚 Full Documentation

  • JavaDoc:
  • GitHub:
  • Maven Central:

🎉 Final Thoughts

청크 작업은 보편적인 문제이며, 이제 깔끔하고 재사용 가능하며 스트림 친화적인 해결책이 생겼습니다.

“왜 이런 내장 기능이 없을까?” 라고 생각해 본 적이 있다면, 이제는 직접 사용할 차례입니다.

한 번 써보고, 레포에 ⭐를 달고, 피드백을 남겨 주세요 — 여러분이 어떻게 활용하고 있는지 궁금합니다.

Back to Blog

관련 글

더 보기 »

Java 8 (Stream API)

Stream API의 특징 - Declarative – 함수형 스타일을 사용하여 간결하고 가독성 높은 코드를 작성한다. - Lazy Evaluation – 연산은 terminal 연산이 호출될 때만 실행된다.

Spring Boot를 사용한 RESTful Web API 구현

REST API를 구축하는 것은 모든 백엔드 개발자에게 가장 필수적인 기술 중 하나입니다. Spring Boot는 production‑ready 환경을 제공함으로써 이를 매우 간단하게 만들어 줍니다.