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

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
청크 작업은 보편적인 문제이며, 이제 깔끔하고 재사용 가능하며 스트림 친화적인 해결책이 생겼습니다.
“왜 이런 내장 기능이 없을까?” 라고 생각해 본 적이 있다면, 이제는 직접 사용할 차례입니다.
한 번 써보고, 레포에 ⭐를 달고, 피드백을 남겨 주세요 — 여러분이 어떻게 활용하고 있는지 궁금합니다.