Stream이란?
컬렉션이나 배열 등의 데이터를 함수형 스타일로 처리할 수 있게 해주는 데이터 파이프라인
- 반복문, 제어문 등 대신 Lambda(람다)를 사용해서 데이터 필터링, 변환, 집계 등을 간결하고 명확하게 표현할 수 있는 방법
Stream 탄생
"컬렉션(List)에 담긴 각 정수 요소 중 3보다 큰 숫자만 고르고, 해당 숫자에 10을 곱한 값을 새 리스트에 담으세요."
를 코드로 작성하면 아래와 같다.
List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> ret1 = new ArrayList<>();
for (Integer num : arrayList) {
if(num > 3) {
Integer multipliedNum = num * 10;
ret1.add(multipliedNum);
}
}
System.out.println("ret1 = " + ret1); // 조회: ret1 = [40, 50]
해당 코드를 작성하려면 반복문(for), 조건문(if), 변환(num*10), 수집(add)를 모두 작성해야 하는데, 코드가 길고 헷갈릴 수 있다.
- 향후 이러한 처리들을 병렬로 처리할 경우 더 고민해야하는 포인트(스레드, 동기화, 락 등) 가 많아질 것이다.
Java가 아닌 다른 언어(JS, Python 등) 에서는
filter, map, reduce 와 같은 함수를 사용하면 "무엇을 할지"만 작성하면 알아서 반환이 된다.
// JS 예시
const arrayList = [1, 2, 3, 4, 5];
// filter: num > 3인 것만, map: 10을 곱한다
const ret1 = arrayList.filter(num => num > 3).map(num => num * 10);
console.log(ret1); // [40, 50]
Java에서도 반복문, 조건문, 수집 코드 등을 모두 작성하지 않고 보다 간결하게 작성을 필요로 하였고,
Lambda 표현식이 도입된 이후 "(x -> x>3 )" 과 같이 작은 함수식을 바로 작성할 수 있게 됨에 따라
fillter(), map() 같은 메서드를 통해 "무엇을 할 지"를 선언한 후 collect(), forEach 와 같은 최종 연산(명령)을 호출하면 한번에 실행되도록 할 수 있게 한 것을 "Stream" 이라고 한다.
- 병렬 쳐리도 stream을 사용하면 parallelStream() 메서드를 통해 간편하게 처리할 수 있다.
더보기
// 기존 방식
ExecutorService exec = Executors.newFixedThreadPool(4); // 스레드/작업 청크 생성
List<Integer> result = Collections.synchronizedList(new ArrayList<>());
for (Integer num : arrayList) {
exec.submit(() -> {
if (num > 3) {
result.add(num * 10);
}
});
}
exec.shutdown();
exec.awaitTermination(1, TimeUnit.MINUTES);
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// stream을 사용하면 한 줄로 작성 가능
List<Integer> ret = arrayList
.parallelStream() // 병렬 처리 > 스레드 분배, 작업 분할, 동기화 등은 stream이 알아서 처리
.filter(num -> num > 3)
.map(num -> num * 10)
.collect(Collectors.toList());
Stream 특징
- 데이터 소스와 연산 분리: 컬렉션에서 꺼낸 데이터 흐름(stream)에 연산을 적용한다
- 파이프라인 구조: 여러 개의 중간 연산(처리)를 차례대로 연결한 후, 최종 연산(결과 생성)을 실행한다.
- 지연 처리: 중간 연산은 실제 결과가 필요해질 때까지 실행되지 않는다.
- 재사용 불가: 최종 연산이 실행된 Stream은 닫혀버리기 때문에 재사용이 불가능하다.
Stream 구조
1. 스트림 생성 (Source)
- Collection.stream()
- Collection.parallelStream(): 병렬 처리 필요 시
2. 중간 연산
- filter(Predicate<T>): 조건에 맞는 요소만 남긴다
- map(Function<T,R>): 각 요소를 다른 형태로 변환한다
- flatMap(Function<T,Stream<R>>): 1:N 매핑하여 평탄화
- distinct(), sorter(), limit(n), skip(n) 등
- 특징
- 여러 중간 연산을 계속 연결할 수 있다.
- 최종 연산 전까지 실제 데이터 처리를 미룬다 > 지연 실행 가능
3. 최종 연산
- forEach(Consumer<T>): 각 요소를 소비한다.
- collect(Collector): 스트림 결과를 List, Set, Map 등을 수집한다.
- reduce(BinaryOperator<T>): 전체 요소를 하나의 값으로 집계한다.
- count(), anyMatch(), allMatch(), findFirst() 등
- 특징
- 결과를 반환한다
- 호출 즉시 Stream이 한 번에 처리된다.
// 예시 1 (위의 예시를 stream으로 표현)
List<Integer> ret2 = arrayList.stream()
.filter(num -> num > 3)
.map(num -> num * 10)
.collect(Collectors.toList()); // 조회: [40, 50]
// 예시2
List<String> words = List.of("java", "stream", "lambda", "api");
// 1) filter + map + collect
List<String> result = words.stream()
.filter(w -> w.length() > 4) // 길이 5 이상
.map(String::toUpperCase) // 대문자 변환
.sorted() // 알파벳 순 정렬
.collect(Collectors.toList()); // List로 수집
System.out.println(result); // [LAMBDA, STREAM]
'Dev > Java' 카테고리의 다른 글
[Java] 반복문 (0) | 2025.04.30 |
---|---|
[Java] HashMap (0) | 2025.04.29 |
[Java] Lambda (0) | 2025.04.25 |
[Java] System.out.print의 진실 (feat. toString() Override) (0) | 2025.04.24 |
[Java] Generic (0) | 2025.04.23 |