1. Stream.concat: 두 스트림 결합
프로그래밍에서는 종종 이런 상황이 생깁니다. 두 개(또는 그 이상)의 데이터 스트림이 있고, 이를 하나로 합치고 싶습니다. 예를 들어 서로 다른 그룹의 학생 리스트 두 개가 있는데 — 모두를 한꺼번에 처리해야 하는 경우입니다. “옛 방식”(Stream API 이전)이라면 addAll로 리스트 두 개를 합쳤을 겁니다. 하지만 우리가 Stream<T>로 작업한다면, 이를 지연(lazy)되고 표현력 있게 처리하고 싶습니다.
문법과 동작 원리
가장 기본적인 방법은 정적 메서드 Stream.concat을 사용하는 것입니다:
Stream<T> Stream.concat(Stream<? extends T> a, Stream<? extends T> b)
무슨 일이 일어나나:
- 먼저 첫 번째 스트림의 요소들을 받고, 그다음 두 번째 스트림을 받습니다.
- 결과는 새로운 스트림이며, 첫 번째의 모든 요소를 내보낸 뒤 두 번째의 모든 요소를 내보냅니다.
- 결합은 지연됩니다. 최종 연산(예: forEach 또는 collect)으로 결과 스트림을 순회하기 전까지는 아무 일도 일어나지 않습니다.
예시: 두 이름 리스트 결합
import java.util.List;
import java.util.stream.Stream;
List<String> groupA = List.of("Anya", "Boris", "Vika");
List<String> groupB = List.of("Grisha", "Dasha");
Stream<String> allStudents = Stream.concat(groupA.stream(), groupB.stream());
allStudents.forEach(System.out::println);
결과:
Anya
Boris
Vika
Grisha
Dasha
중요: 결합 후 결과 스트림은 1회성입니다. 다른 모든 Stream처럼 재사용할 수 없습니다.
Stream.concat의 특징
- 한 번에 두 스트림만. 세 개 이상이면 concat을 체이닝하거나 다른 접근법(아래 참조)을 사용하세요.
- 순서가 보존됩니다. 항상 첫 번째 스트림의 요소들이 먼저, 그다음 두 번째 스트림의 요소들이 나옵니다.
- 지연 결합. 첫 번째 스트림이 무한 스트림이면, 두 번째로는 절대 진행되지 않습니다.
- 전형적인 버그: 동일한 스트림을 자기 자신과 결합하려는 시도(Stream.concat(stream, stream))는 동일 스트림의 재사용에 해당하며 — 이는 허용되지 않습니다.
2. 두 개 초과 스트림 결합: flatMap과 Stream.of
스트림이 두 개보다 많을 때는 Stream.of + flatMap 조합으로 모으는 것이 더 편리합니다.
Stream.of + flatMap 사용
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
List<String> groupA = List.of("Anya", "Boris");
List<String> groupB = List.of("Vika");
List<String> groupC = List.of("Grisha", "Dasha");
Stream<String> allStudents = Stream.of(groupA, groupB, groupC)
.flatMap(Collection::stream);
allStudents.forEach(System.out::println);
여기서 일어나는 일:
- Stream.of(groupA, groupB, groupC)는 세 개 컬렉션으로 이루어진 스트림을 생성합니다.
- flatMap(Collection::stream)은 각 컬렉션을 하나의 문자열 스트림으로 펼칩니다.
- 결과적으로 모든 학생을 담은 하나의 큰 스트림이 됩니다.
장점: 원하는 만큼 많은 스트림을 결합할 수 있습니다.
또 다른 방법: 스트림 리스트 결합
import java.util.List;
import java.util.stream.Stream;
List<Stream<String>> streams = List.of(
Stream.of("a", "b"),
Stream.of("c"),
Stream.of("d", "e")
);
Stream<String> merged = streams.stream()
.flatMap(s -> s);
merged.forEach(System.out::println);
결과:
a
b
c
d
e
3. Collectors.joining: 구분자로 문자열 이어 붙이기
가끔은 스트림을 단순히 합치는 것이 아니라 모든 요소를 하나의 문자열로 이어 붙여야 할 때가 있습니다 — 예를 들어 이름을 콤마로 구분해 출력하려는 경우입니다. 이때 사용하는 수집기가 Collectors.joining입니다.
문법
String result = stream.collect(Collectors.joining(", "));
- 인수 없이: 구분자 없이 문자열을 그대로 이어 붙입니다.
- 구분자와 함께: 요소 사이에 지정한 문자열을 넣습니다.
- 접두사와 접미사 함께: Collectors.joining(delimiter, prefix, suffix)
예시: 학생 리스트를 한 줄로
import java.util.List;
import java.util.stream.Collectors;
List<String> students = List.of("Anya", "Boris", "Vika");
String line = students.stream()
.collect(Collectors.joining(", "));
System.out.println("학생 목록: " + line);
결과:
학생 목록: Anya, Boris, Vika
접두사와 접미사 예시
String line = students.stream()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(line);
결과:
[Anya, Boris, Vika]
왜 필요한가요?
- CSV 문자열 생성.
- 리스트와 보고서를 보기 좋게 출력.
- 데이터를 한 줄로 전달(예: URL이나 로그).
4. concat과 flatMap 비교: 언제 무엇을 사용할까?
- Stream.concat — 스트림이 정확히 두 개이고, 순서 보존이 중요할 때.
- Stream.of + flatMap — 스트림이 많거나 컬렉션에 스트림들이 담겨 있을 때.
- Collectors.joining — 요소 타입이 String이고 결과를 하나의 문자열로 얻고 싶을 때.
비교 표
| 메서드 | 사용할 때 | 코드 예 |
|---|---|---|
|
두 개의 스트림 | |
|
여러 스트림, 스트림 컬렉션 | |
|
요소를 하나의 문자열로 합치기 | |
5. 학습용 애플리케이션에서의 실전 예
예시 1. 두 학부의 학생을 결합
import java.util.List;
import java.util.stream.Stream;
List<String> itStudents = List.of("Anya", "Boris");
List<String> mathStudents = List.of("Vika", "Grisha");
Stream<String> all = Stream.concat(itStudents.stream(), mathStudents.stream());
all.forEach(System.out::println);
예시 2. 모든 그룹의 학생을 결합해 한 줄로 출력
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
List<List<String>> allGroups = List.of(
List.of("Anya", "Boris"),
List.of("Vika"),
List.of("Grisha", "Dasha")
);
String allNames = allGroups.stream()
.flatMap(Collection::stream)
.collect(Collectors.joining("; "));
System.out.println("모든 학생: " + allNames);
결과:
모든 학생: Anya; Boris; Vika; Grisha; Dasha
예시 3. 숫자 스트림 결합
import java.util.stream.Stream;
Stream<Integer> s1 = Stream.of(1, 2, 3);
Stream<Integer> s2 = Stream.of(4, 5);
Stream<Integer> merged = Stream.concat(s1, s2);
merged.forEach(System.out::print); // 12345
6. 중요한 뉘앙스와 주의점
concat은 두 개의 스트림에만 동작
세 개의 스트림을 결합하려면 중첩된 표기가 됩니다 — 동작은 하지만 지저분해 보일 수 있습니다. Stream.of + flatMap을 사용하는 것이 더 좋습니다:
Stream<Integer> s1 = Stream.of(1);
Stream<Integer> s2 = Stream.of(2);
Stream<Integer> s3 = Stream.of(3);
Stream<Integer> merged = Stream.concat(Stream.concat(s1, s2), s3);
스트림은 1회성
최종 연산 이후 스트림은 다시 사용할 수 없습니다. 재사용을 시도하면 IllegalStateException이 발생합니다.
concat으로 만든 스트림은 지연 실행
첫 번째 스트림이 무한 스트림(예: Stream.generate(...)로 생성)인 경우, 두 번째 스트림으로는 절대 진행되지 않습니다.
요소 순서
순서는 항상 보존됩니다. 먼저 첫 번째 스트림, 그다음 두 번째 스트림입니다.
Collectors.joining은 Stream<String>에서만 작동
문자열이 아닌 경우 먼저 문자열로 변환하세요. 예를 들어 map(Object::toString)을 사용할 수 있습니다:
import java.util.stream.Collectors;
import java.util.stream.Stream;
Stream<Integer> numbers = Stream.of(1, 2, 3);
String line = numbers
.map(Object::toString)
.collect(Collectors.joining(", "));
System.out.println(line); // 1, 2, 3
“수동” 방식과 비교
Stream API 이전:
import java.util.ArrayList;
import java.util.List;
List<String> merged = new ArrayList<>(listA);
merged.addAll(listB);
Stream API 사용:
import java.util.stream.Collectors;
List<String> merged = Stream.concat(listA.stream(), listB.stream())
.collect(Collectors.toList());
스트림의 장점: 결합 전후에 filter, map 등의 중간 연산을 쉽게 연결할 수 있고, 다양한 데이터 소스를 대상으로 동일한 방식으로 작업할 수 있습니다.
7. 스트림 결합 시 흔한 실수
오류 №1: 스트림을 재사용함. 스트림은 한 번만 사용할 수 있습니다. 동일한 스트림을 자기 자신과 결합할 수 없습니다.
import java.util.stream.Stream;
Stream<String> s = Stream.of("a", "b");
Stream<String> merged = Stream.concat(s, s); // 오류! s가 재사용됩니다
오류 №2: 무한 스트림과 유한 스트림을 결합하려 함. 첫 번째 스트림이 무한 스트림이면, 두 번째 스트림은 결코 시작되지 않습니다.
import java.util.stream.Stream;
Stream<Integer> infinite = Stream.generate(() -> 1);
Stream<Integer> finite = Stream.of(2, 3);
Stream<Integer> merged = Stream.concat(infinite, finite);
// merged.limit(10).forEach(System.out::println); // finite는 결과에 포함되지 않습니다
오류 №3: 문자열이 아닌 타입에 joining 사용. 수집기 Collectors.joining은 문자열 스트림만 처리합니다. 숫자 등의 경우 먼저 문자열로 변환해야 합니다:
import java.util.List;
import java.util.stream.Collectors;
List<Integer> numbers = List.of(1, 2, 3);
// numbers.stream().collect(Collectors.joining(", ")); // 컴파일 오류!
String joined = numbers.stream()
.map(Object::toString)
.collect(Collectors.joining(", "));
오류 №4: 순서 깨짐. 순서가 중요하다면 concat 또는 순서가 보장되는 flatMap을 사용하세요. unordered() 같은 메서드는 결정적인 출력 순서를 깨뜨릴 수 있습니다.
GO TO FULL VERSION