Stream
오라클 공식 문서 : 순차 및 병렬적인 집계연산을 지원하는 연속된 요소
모던 자바 인 액션 : 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소
연속된 요소라는 말에서 컬렉션이랑 크게 다를 바 없어 보인다.
위키피디아 : A Sequence of Data elements made available over time이라고 표현
캠브릿지 사전 : 일반 명사로 A continuous flow of things or people을 의미
두 가지 포인트에 집중 : available over time과 continuous flow
스트림은 어떠한 요소들이 모인 하나의 고정된 집합이라고 생각하기 보다
flow, 즉 데이터 흐름이라는 것에 좀 초점
오라클 공식 문서에서는 스트림 패키지를 요소들의 스트림에 함수형 스타일의 연산을 지원하는 클래스들이라고 묘사
즉 이 스트림 자체는 어떠한 데이터의 흐름
자바의 스트림 API는 이 데이터를 어떻게 다룰 것인가를 논하는 일종의 파이프라인이라 생각
스트림 구성(생성, 가공, 소비)
1. 생성 : 리스트, 맵과 같은 컬렉션과 배열, 파일로 생성 가능
(컬렉션과 다르게 무한을 만들 수도 있고 별도의 서드파티 라이브러리를 활용할 수 있다.)
2. 가공(중간연산자) : 필터, 맵, 리밋 등 소스로부터 얻어낸 값들을 입맛대로 가공하는 역할
중간연산자의 결과로는 새로운 스트림을 반환하는데 이는 곧 lazy evaluation을 할 수 있도록 만들어 준다.
스트림에서 lazy evaluation은 최종 연산이 들어오기 전까지 중간 연산은 실제로 실행되지 않음을 의미
즉 새로운 스트림 인스턴스를 돌려주기만 할 뿐 어떠한 작업도 하지 않는다.
이러한 lazy evaluation이 있기 때문에 루프 퓨전과 쇼트 서킷이라는 테크닉을 활용 가능
루프 퓨전
아래 결과물과 같이 모든 요소가 각 단계를 한 번에 거치는 게 아니라 개별적인 요소가 하나씩 단계를 순차적으로 거쳐가는 모습이 반복문이 합쳐진 것 같다고 해서 루프 퓨전이라고 표현한다.
List<String> names = List.of("pobi", "jason", "neo", "gugu");
names.stream()
.filter(str -> {
System.out.println("first filter = " + str);
return str.length() > 3;
})
.filter(str -> {
System.out.println("second filter = " + str);
return !str.contains("gu");
})
.map(str -> {
System.out.println("map = " + str);
return str.toUpperCase();
})
.forEach(str -> {
System.out.println("forEach = " + str + "\n");
});
쇼트 서킷
일련의 논리연산을 진행할 때 모든 연산을 수행하지 않고 결과가 확실한 경우에 나머지 연산을 수행하지 않는 것을 의미
IntStream.range(0, 10000000)
.limit(5)
.map(i -> i + 100)
.forEach(i -> System.out.println("result = " + i));
0부터 1000만 까지의 범위에 대해서 스트림을 생성했는데 5개로 limit을 걸게 되면 5개 이후로는 연산을 할 필요가 없기 때문에 결과도 다섯 개만 출력된다.
중간연산자(가공)는 두 가지로 구분 : stateless, stateful
stateless
- 특정 행위를 수행할 때 다른 요소에 대해서 독립적으로 처리될 수 있음을 의미
- 맵의 경우 맵 연산을 수행하기 위해서는 내가 현재 바라보고 있는 값에만 신경을 쓰면 된다.
stateful
- 선행된 연산에 영향 받음
- sorted, distinct와 같은 중간연산자는 이 중간연산자가 실행되려면 자기 자신 이외의 값은 무엇이었는지 등에 대해서 특정 상태를 알고 신경을 써야 한다
3. 소비(최종연산자) : 결과를 생성하거나 사이드 이펙트를 만들기 위해서 사용
자주 사용하는 collect나 findAny, findFirst 등이 있다.
주의점!!
1. 최종연산자가 수행되고 나면 스트림 파이프라인은 소비된 것으로 간주
다시 사용하려면 새로운 스트림을 만들어야 한다.
void test2() {
List<String> list = List.of("pobi", "jason", "neo", "gugu");
Stream<String> stream = list.stream();
recycle(stream);
System.out.println("-- complete --");
recycle(stream);
}
public void recycle(Stream<String> stream) {
stream.forEach(str -> System.out.println("consuming = " + str));
}
recycle이라는 메서드를 두 번 호출하였지만, 두 번째 recycle은 이미 소비되었다는 예외를 던진다.
2. forEach
public static List<String> discouragedVersion(Stream<String> stream, Pattern pattern) {
List<String> results = new ArrayList<>();
stream.filter(s -> pattern.matcher(s).matches())
.forEach(results::add); // Unnecessary use of side-dffects!
return results;
}
public static List<String> discouragedVersionWithParallel(Stream<String> stream, Pattern pattern) {
List<String> results = new ArrayList<>();
stream.parallel()
.filter(s -> pattern.matcher(s).matches())
.forEach(results::add); // There are no guarantees of results to be always same!
return results;
}
public static List<String> encouragedVersion(Stream<String> stream, Pattern pattern) {
return stream.filter(s -> pattern.matcher(s).matches())
.collect(Collectors.toList()); // No side-effects!
}
forEach는 로그나 디버깅을 위한 출력으로만 사용할 것을 권장
이펙티브 자바에서는 forEach가 "덜 스트림스럽다"라고 이야기한다.
공식 문서를 살펴보면 그 이유를 알 수 있다.
첫 번째 메서드와 같이 forEach 내부에서 값을 할당 하는 식으로 사용할 경우 불필요한 사이드 이펙트를 만들어 내는 셈이 된다.
두 번째와 같이 병렬처리가 추가될 경우에는 올바른 결과값을 기대하기 어렵다.
세 번째와 같이 안정적인 collect라는 메서드를 활용할 수 있는데 굳이 리스크를 감수할 필요는 없다.
스트림을 사용했을 때 얻을 수 있는 장점
1. 가독성이 좋다
public static List<String> forLoopVersion(List<String> list) {
List<String> result = new ArrayList<>();
for (String str : list) {
if (str.length() < 10) {
str = str.toLowerCase();
if (!str.contains("abc")) {
result.add(str);
}
}
}
}
public static List<String> streamVersion(List<String> list) {
return list.stream()
.filter(str -> str.length() < 10)
.map(String::toLowerCase)
.filter(str -> !str.contains("abc"))
.collect(Collectors.toList());
}
forLoopVersion 코드는 해당 메서드가 무슨 일을 하고 있는지 코드를 한 줄 한 줄 따라가면서 인지를 해야한다.
스트림의 코드는 필터와 같은 메서드 네이밍이나 람다식이나 메서드 레퍼런스로 넘겨준 이 짧은 코드를 활용해서 어떻게 동작하는지 쉽게 이해할 수 있다.
2.코드의 변경이 쉽다. 즉 유연하다
문자열을 소문자로 매핑하기 이전에 필터링을 하고 싶다면 메서드의 순서만 바꾸면 된다.
.map(String::toLowerCase) <---> .filter(str -> !str.contains("abc"))
forLoopVersion코드는 복잡하기 때문에 어느 부분을 손 대야 할지, 손 댔을 때 정상적으로 동작할지 기대하기도 쉽지않다.
3. 병렬처리를 간단하게 해결할 수 있다.
public static List<String> streamVersionWithParallel(List<String> list) {
return list.parallelStream() // parallelStream() 추가
.filter(str -> str.length() < 10)
.map(String::toLowerCase)
.filter(str -> !str.contains("abc"))
.collect(Collectors.toList());
}
public static List<String> streamVersionWithParallel(List<String> list) {
return list.stream()
.parallel() //parallel() 추가
.filter(str -> str.length() < 10)
.map(String::toLowerCase)
.filter(str -> !str.contains("abc"))
.collect(Collectors.toList());
}
parallelStream 혹은 parallel 이라는 키워드를 붙여주는 것만으로 병렬처리를 지원한다.
이 스트림의 병렬처리는 내부적으로 포크조인 프레임워크를 활용한다.
이러한 병렬성은 앞에 언급했던 stateless, stateful 이외에도 순서를 고려해야 할지 말지에 대한 여부나 병렬처리에 대한 오버헤드 등 여러 가지 신경 써야 할 것들이 많다.
단점
1. 컴퓨팅 비용
스트림은 내부적으로 다양한 조건과 상황에 따라서 연산을 처리하기 때문에 생성 하는데 적지 않은 비용이 발생한다.
주식 매매와 같이 성능이 코어한 이슈라면 스트림의 사용을 한번 더 고려해봐야 한다.
2. 인지에 대한 비용
기존 반복문의 경우 직접 값을 꺼내와서 사용했고, 반대로 스트림을 사용할 경우 스트림 내부에서 연산이 일어난다.
이를 각각 외부 반복과 내부 반복이라고 표현한다.
내부 반복이라는 것 자체가 GOF 책에서 등장하는 디자인 패턴 중 하나이기도 하고 또 그 자체로 단점으로 보기는 어려운데 문제는 코드가 정상적으로 동작하지 않을 경우 내부에서 반복이 어떻게 동작하는지 명확하게 알고 있어야 한다.
void test() {
IntStream.iterate(0, i -> (i+1) % 2)
.distinct()
.limit(10)
.forEach(System.out::println);
System.out.println("complete");
}
void test() {
IntStream.iterate(0, i -> (i+1) % 2)
.limit(10)
.distinct()
.forEach(System.out::println);
System.out.println("complete");
}
위 두 코드는 limit과 distinct의 순서에서 차이가 있다.
위쪽 코드는 무한히 반복하고, 아래쪽 코드는 정상적으로 complete가 찍힌다.
위쪽 코드는 distinct 입장에서 반복해서 들어오는 0과 1이라는 두 값은 이미 발견한 적이 있는 값이므로 limit으로 넘겨주지 않는다.
하지만 아래쪽의 경우 limit으로 먼저 개수의 제한이 걸리기 때문에 다음 distinct로 넘어갈 수가 있다.
참고
'개발 관련 강의 정리 > 10분 테코톡' 카테고리의 다른 글
[10분 테코톡] 🎼라흐의 DTO vs VO 정리 (0) | 2023.07.12 |
---|---|
[10분 테코톡] 📍인비의 DTO vs VO 정리 (0) | 2023.07.11 |
[10분 테코톡] 💫두강의 Generics 정리 (0) | 2023.07.06 |
[10분 테코톡] 🌳 나봄의 CORS 정리 (0) | 2023.07.05 |
[10분 테코톡] 크리스, 로마의 stream vs for (0) | 2023.07.02 |
댓글