본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 21. 03:25

mapMulti()로 작업하기 – 스트림(Streams)을 고민 없이 변환하세요

요약

JDK 16 이상에서 도입된 Stream API의 mapMulti() 메서드 사용법을 설명합니다. 기존의 filter()와 map()을 체이닝하던 방식 대신, 단일 BiConsumer를 통해 요소를 필터링하거나 0개에서 여러 개로 매핑할 수 있는 효율적인 방법을 제시합니다.

핵심 포인트

  • mapMulti()는 단일 연산 내에서 필터링과 매핑을 동시에 수행할 수 있게 해줍니다.
  • 제네릭 타입 추론을 위해 mapMulti() 사용 시 type-witness(예: .<Double>)가 필요할 수 있습니다.
  • 기본 타입(primitive types) 작업 시에는 mapMultiToDouble()과 같은 전용 메서드를 사용하는 것이 효율적입니다.
  • flatMap()과 비교하여 일대다(one-to-many) 매핑 상황에서 유용하게 활용될 수 있습니다.

안녕, 개발자 여러분? 잘 지내고 계신가요? 만약 JDK 16 이상을 사용하고 있다면, Stream API에 새로운 장난감이 추가되었습니다: 바로 mapMulti() 메서드입니다. 처음에는 조금 생소해 보일 수 있지만, 일단 이해하고 나면 커피를 타는 데도 사용하고 싶어질 정도로 유용하다고 장담합니다 ☕. 아이디어는 간단합니다: 단일 BiConsumer 내에서 스트림의 각 요소를 0개, 1개 또는 여러 개의 새로운 요소로 매핑할 수 있게 해줍니다. filter()와 map()을 미친 듯이 체이닝(chaining)하던 방식에서 벗어날 수 있을까요? 음, 항상 그런 것은 아니지만, 많은 경우에 그렇습니다. 실제로 살펴봅시다. 🔍 클래식한 예시 vs mapMulti() 학생들의 성적 리스트가 있고, 7점 이상인 성적만 필터링한 뒤 각 성적에 10%의 보너스를 적용하고 싶다고 가정해 봅시다. 전통적인 방식 (filter + map): List < Double > notas = List . of ( 5.5 , 8.0 , 6.0 , 9.5 , 7.0 , 4.0 ); List < Double > notasComBonus = notas . stream () . filter ( n -> n >= 7 ) . map ( n -> n * 1.10 ) . collect ( Collectors . toList ()); // 결과: [8.8, 10.45, 7.7] 이제 mapMulti()를 사용하면 단 한 번의 연산으로 동일한 작업을 수행할 수 있습니다: List < Double > notasComBonusMM = notas . stream () .< Double > mapMulti (( nota , consumer ) -> { if ( nota >= 7 ) { consumer . accept ( nota * 1.10 ); } }) . collect ( Collectors . toList ()); 여기서 if 문이 filter()의 역할을 하고, accept()가 map()을 적용합니다. mapMulti() 앞의 .<Double>에 주목하세요 – 이것은 type-witness (까다로운 컴파일러가 요구하는 것)입니다. 이것이 없으면 코드가 컴파일되지 않습니다. 지불할 만한 작은 대가죠. 🧮 기본 타입(primitive types)으로 작업하기 – mapMultiToDouble() 만약 보너스가 포함된 이 성적들을 합산하고 싶다면, double 전용 버전을 사용하는 것이 좋습니다: double somaComBonus = notas . stream () . mapMultiToDouble (( nota , consumer ) -> { if ( nota >= 7 ) { consumer . accept ( nota * 1.10 ); } }) . sum (); System . out . println ( somaComBonus ); // 26.95 type-witness도 필요 없고, 군더더기도 없습니다. 만약 나중에 다시 Stream<Double>로 돌아가고 싶다면, boxed() 또는 mapToObj()를 사용하기만 하면 됩니다: Stream < Double > streamBonus = notas .

stream() . mapToDouble((nota, consumer) -> { if (nota >= 7) consumer.accept(nota * 1.10); }).boxed();

📦 flatMap() vs mapMulti() – 일대다(one-to-many)의 대결

더 실제적인 예제로 들어가 보겠습니다. 제품 카테고리들이 있고, 각 카테고리에는 여러 개의 제품이 있습니다. 우리는 '카테고리 이름 + 제품 이름'을 포함하는 ProdutoInfo의 단순한 리스트를 생성하고자 합니다.

class Categoria {
private String nome;
private List<Produto> produtos;
// getters...
}

class Produto {
private String nome;
private double preco;
// getters...
}

class ProdutoInfo {
private String categoria;
private String produto;
// 생성자, getters...
}

flatMap()을 사용하는 경우 (전통적인 방식):
List<ProdutoInfo> listaFlat = categorias.stream()
.flatMap(cat -> cat.getProdutos().stream()
.map(prod -> new ProdutoInfo(cat.getNome(), prod.getNome())))
.collect(Collectors.toList());

여기서 문제는 각 카테고리마다 중간 스트림(cat.getProdutos().stream())을 생성한다는 점입니다. 카테고리가 많다면 이는 부담이 됩니다.

mapMulti()를 사용하는 경우 (더 간결한 방식):
List<ProdutoInfo> listaMM = categorias.stream()
.<ProdutoInfo>mapMulti((categoria, consumer) -> {
for (Produto p : categoria.getProdutos()) {
consumer.accept(new ProdutoInfo(categoria.getNome(), p.getNome()));
}
})
.collect(Collectors.toList());

중간 스트림 없이 단순한 for 루프만 사용합니다. 성능이 더 뛰어나며, 제 생각에는 가독성도 더 좋습니다.

🎯 추가 필터링 – 50헤알(R$) 초과 제품만 추출하기

가격이 50보다 큰 제품만 원한다면, mapMulti()의 장점은 더욱 명확해집니다:

List<ProdutoInfo> listaCaros = categorias.stream()
.<ProdutoInfo>mapMulti((categoria, consumer) -> {
for (Produto p : categoria.getProdutos()) {
if (p.getPreco() > 50) {
consumer.accept(new ProdutoInfo(categoria.getNome(), p.getNome()));
}
}
})
.collect(Collectors.toList());

여기서는 추가적인 filter() 호출을 피하고 모든 로직을 한곳에 유지할 수 있습니다.

다시 수정해 봅시다. 각 카테고리에는 (보통) 제품이 몇 개 없으므로, 공식 권장 사항에 완벽하게 부합합니다: 스트림의 각 요소를 작은 수(0개일 수도 있음)의 요소로 교체할 때는 mapMulti()를 사용하세요. 🧠 명령형 접근 방식(Imperative approach)? 그것도 가능합니다! 모든 로직을 수행하고 Consumer를 받는 메서드가 이미 있다면... 그것을 mapMulti()에 직접 전달할 수 있습니다. Categoria 클래스에 다음을 추가하세요:

public void produtosCaros ( Consumer < ProdutoInfo > consumer , double limite ) { for ( Produto p : this . produtos ) { if ( p . getPreco () > limite ) { consumer . accept ( new ProdutoInfo ( this . nome , p . getNome ())); } } }

이제 당신의 스트림은 코코넛 워터처럼 깨끗해집니다:

List < ProdutoInfo > resultado = categorias . stream () .< ProdutoInfo > mapMulti ( cat -> cat . produtosCaros ( consumer , 50 )) . collect ( Collectors . toList ());

또는 메서드 참조(Method reference)를 사용하여 (훨씬 더 아름답게):

.< ProdutoInfo > mapMulti ( cat -> cat . produtosCaros ( consumer , 50 )) // 또는, 한계값이 고정되어 있다면: .< ProdutoInfo > mapMulti ( cat -> cat . produtosCaros ( consumer , 50 ))

(실제로 메서드 참조를 사용하려면 적응이 필요하겠지만, 핵심은 mapMulti()가 당신의 명령형 메서드를 호출하는 람다(Lambda)를 수용한다는 점입니다.)

mapMulti()를 언제 사용하는가 – 요약

  • 1:0 또는 1:N 관계를 가지며, 원래 요소당 생성되는 요소의 수가 적을 때.
  • 중간 스트림(Intermediate streams) 생성을 피하고 싶을 때 (대규모 루프에서 더 나은 성능).
  • flatMap + filter + map을 사용하는 것보다 for 루프와 if 문을 사용하는 것이 코드를 더 명확하게 만들 때.
  • 이미 Consumer를 받는 메서드가 있고 이를 스트림에 직접 통합하고 싶을 때.

mapMulti()flatMap()을 대체하는 것은 아닙니다. 각각의 용도가 있습니다. 하지만 시나리오가 적합할 때 mapMulti()를 사용하는 것은 마치 일자 드라이버를 십자 드라이버로 바꾸는 것과 같습니다. 같은 작업을 수행하지만 훨씬 더 스타일리시하게 처리할 수 있습니다.

마음에 드셨나요? 코드에 직접 테스트해 보고 스트림이 더 깔끔해졌는지 알려주세요. 다음에 만나요. 군더더기는 줄이고 결과는 더 많이 내는 코딩을 해봅시다! 🚀

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
1

댓글

0