공식문서 읽기 본부의 수장이 되고자 하는 우리의 공대키메라!
이번 시간에는 스트림에 대한 공식 문서를 읽어보고 정리할 것이다.
그럼 후비고~
1. Stream이란?
A sequence of elements supporting sequential and parallel aggregate operations.
순차적 및 병렬 집계 작업을 지원하는 요소들의 시퀀스
그러니까, 순차적 및 병렬 집계 작업을 지원하는 요소 시퀀스라는데...
와닿지가 않는다.
위에 사전에서 봤듯이 일반적인 의미는 뭔가 흐름을 의미한다.
컴퓨터 사이언스에서는 다음과 같이 설명한다
In computer science, a stream is a sequence of potentially unlimited data elements made available over time.
컴퓨터 과학에서 스트림은 시간이 지남에 따라 무제한으로 제공되는 데이터 요소들의 연속입니다.
결국 자바에서의 스트림은 뭔가 데이터 요소들의 연속, 즉 데이터 파이프 라인을 만들어서 데이터를 다루도록 도와주는 도구라고 볼 수 있다.
여기서 또 데이터 파이프라인은 데이터의 수집부터 처리, 변환, 저장에 이르기까지 일련의 단계를 거치는 과정을 의미합니다.
(꼬리질문 사전 차단)
2. 맛보기
다음 예제는 Stream과 IntStream을 사용하는 집계 연산이다.
int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
.mapToInt(w -> w.getWeight())
.sum();
공식문서에서는 Collection 의 인스턴스로 선언되어 있는 widgets를 가지고 예시를 든다.
Collection.stream() 을 통해 stream을 생성하고 red widget만 filter처리하고, 그것을 int value의 하나의 stream으로 변환한다.
Stream은 이것뿐만 아니라 IntStream, DoubleStream같은 원시적인 전문화(primitive specialization)가 있고 이들은 모두 “스트림”으로 불리며 다음과 같다.
계산을 수행하기 위해 stream 작업은 stream pipeline으로 구성된다.
stream pipeline은 소스 혹은 더 많은 중간 작업 그리고 종료 작업 으로 이루어져 있다.
위의 이 구절을 다시 곱씹어 보자.
stream pipeline은 소스 혹은 더 많은 중간 작업 그리고 종료 작업 으로 이루어져 있다.
여기서 stream pipeline이 stream들을 처리하기 위해 하는 일련의 작업들을 말함을 알 수 있다.
Collections과 streams는 표면적인 유사성을 가지고 있지만 다른 목적을 가지고 있다.
Collections는 주로 요소들의 효율적인 관리와 접근에 관심이 있고,
이와 반대로 streams는 요소들에 직접 접근하는 수단을 제공하지는 않지만 소스를 선언적으로 묘사하거나 해당 소스에서 집계 작업을 하는 계산 작업에 관심이 있다.
하지만 만약 주어진 stream 작업들이 요구한 기능을 제공하지 않는다면, BaseStream.iterator() 나 BaseStream.spliterator() 작업들이 제어된 탐색을 수행하는데 사용된다.
여기서 마지막 문장이 이해가 잘 안되는데, 요구한 기능이 뭔가 생각을 해보니... 집계 작업을 하는 계산 작업이라고 보인다.
위에 언급된 BaseStream.iterator() 나 BaseStream.spliterator()는 순회를 하는데 사용되는 것으로, 집계 작업을 하는건 아니니까 말이다.
위에 widgets 예시 처럼 stream pipelin은 스트림 소스에 질의(query)로 보일 수 있다.
3. stream의 특징
stream 의 특징으로는 5개가 있다.
No storage, Funtional in nature, Laziness-seeking, probably unbounded, consumable!
이 5가지에 대해 알아보도록 하겠다.
저장하지 않는다!(No storage)
stream은 요소들을 저장하는 데이터 구조가 아니다.
대신, 계산 작업의 파이프라인을 통해 array, ganerator function, I/O channel, data structure처럼 소스로부터 원소들을 나른다(convey)
여기서 convey 한다는 영어단어가 쓰였는데, 운반하다, 실어나르다 라는 말이다.
정말 pipeline에 어울리는 단어가 아닌가!
본질적으로 함수형이다!(Funtional in nature)
함수형이라는 말이 도대체 뭘까? 이는 Stream API가 함수형 프로그래밍 패러다임을 따르고 있다는 말이다.
그러면 함수형 패러다임은 또 뭐야?
궁금해서 찾아보았다. 다음을 열어보면 된다.
1. 순수함수(Pure functions)
- 동일한 입력에 대해 항상 동일한 출력을 반환.
- 함수 외부의 상태를 변경하지 않음 (부작용 없음)
- 전역 변수나 I/O 작업에 의존하지 않음
2. 불변성 (Immutability)
- 데이터가 생성된 후 변경되지 않음
- 변경이 필요하면 새로운 데이터 객체를 생성
- 객체의 상태가 변하지 않으므로 예측 가능하고 안전함
3. 일급 함수 (First-class Functions)
- 함수를 변수에 할당할 수 있음
- 함수를 다른 함수의 인자로 전달할 수 있음
- 함수를 함수의 결과로 반환할 수 있음
4. 고차 함수 (Higher-order Functions)
- 함수를 인자로 받는 함수
- 함수를 결과로 반환하는 함수
5. 재귀 (Recursion)
- 루프 대신 재귀를 통한 반복 처리
- 함수가 자기 자신을 호출하여 문제 해결
6. 지연 평가 (Lazy Evaluation)
- 결과가 필요할 때까지 계산을 미룸
- 불필요한 연산 방지와 무한 데이터 구조 처리 가능
하여간 이러한 패러다임을 stream이 따르고 있다.
stream에서 작업이 결과를 만들지만 데이터 원본을 수정하지 않는다.
예를 들어, collection에서 얻은 Stream을filtering 하면 필터링한 원본 데이터 없이 새로운 stream을 생성한다.
원본 데이터의 collection의 원소들을 제거하는게 아니라 즉, 원래 데이터는 그대로 있다는 말이고 그냥 새로운 Stream이 생성이 된다. (이 부분은 순수함수와 불변성을 만족한다.)
게으름을 추구한다!(Laziness-seeking)
필터링, 매핑, 중복 제거(filtering, mapping, dupliate removal) 같은 많은 stream 작업들이 성능 최적화의 기회를 주면서 lazily하게 구체화될 수 있다.
예를 들어, 세 개의 연속된 모음을 가진 첫 번째 String을 찾는 작업은 모든 입력 문자열을 검사할 필요가 없다.(왜?)
stream 작업은 중간연산과 최종연산으로 나뉜다. 중간 연산은 항상 지연방식(lazy)하다.
여기서 모든 입력 문자열을 검사할 필요가 없다는 말이 뭔가 이해가 안돼서 코드를 쳐보았다.
List<String> words = Arrays.asList("hello", "beautiful", "equation", "aviation", "automobile");
String result = words.stream()
.filter(s -> {
System.out.println("검사 중: " + s);
return hasThreeConsecutiveVowels(s);
})
.findFirst() // 첫 번째 일치하는 요소만 찾음
.orElse("없음");
이게 모든 입력 문자열을 정말로 검사할 필요가 없는게, 중간에 검색하다가 하나만 찾더라도 그 찾은 값을 반환한 후 stream은 종료한다.
결국 지연 평가는 연산을 지연하고 이를 통해 필요한 만큼만 처리함으로 효율적으로 작동하는것이다.
이걸 좀 있어보이게 포장하면?? 성능 최적화(performance optimization)에 좋다.
무한할 수 있다!(Possibly unbounded)
경계가 없다는 것이다.
collection은 유한한 크기를 가지고 있는 반면 stream은 그렇지 않다!
limit 이나 findFirst같은 Short-circuiting 작업들이 유한한 stream의 게산을 제한된 시간에 마치도록 한다.
여기서 Short-circuiting 은 조건이 만족되면 즉시 멈추고 결과를 반환하는 것을 말한다!
소비할 수 있다!(Consumable)
stream의 데이터들은 stream의 한 생명 주기동안 오직 한 번 방문한다.
Iterator처럼, 재방문을 위해선느 새로운 stream이 원본 데이터로부터 같은 원소로 생성되야한다.
4. 스트림 작업과 파이프라인(Stream operations and pipelines)
스트림 작업은 중간작업과 최종 작업으로 나뉜며 스트림 파이프라인을 만들기 위해 결합한다.
중간 작업 (Intermediate operation)
중간 작업은 새로운 stream을 반환하며 항상 Lazy하다.
여기서 재미있는 점이 있는데, filter()같은 중간 연산을 쓴다면 이건 실제로 아직 필터를 실행한건 아닌데, 대신에 새로운 stream을 반환한다.
이 새 스트림은 순회할 때 주어진 조건에 맞는 초기 스트림의 요소들을 포함한다.
파이프라인 소스의 순회는 파이프라인의 종단 연산이 실행될 때까지 시작되지 않는다.
여기서 지연실행의 의미를 더 잘 알 수 있는데, 실제 데이터 처리는 종단 연산(terminal operation)이 호출될 때 시작되며,
그전에는 어떤 데이터도 실제로 처리되지 않는다.
실제 데이터(원본 컬렉션)를 하나씩 살펴보는 작업은 최종 연산(collect, forEach 등)을 호출할 때까지 전혀 시작되지 않는다는 말이다!
최종 작업(Terminal operation)
Stream.forEach 혹은 IntStream.sum같은 최종 작업들은 결과 혹은 부작용을 생산하는 stream을 순회할수도 있다.
최종 작업이 수행된 후 stream pipeline은 소비되었다고 보고 더이상 사용할 수 없다.
같은 데이터를 다시 순회하고 싶으면 새로운 stream을 얻기 위해 data source 를 return해야한다.
위에 특징에서 consumable이라고 한 점 기억하는가? 소비되고 더이상 사용할 수 없는 점은 함수형 패러다임에서 불변성을 강제하는 특징이기도 하다.
중간연산은 stateless 연산과 stateful 연산으로 나뉜다.
상태없는 연산은 각 요소를 독립적으로 처리하며 filter, map, flapMap, peek등이 있다.
이는 단일 패스로 효율적 처리가 가능하다.
예시 코드를 보겠다.
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0) // 상태 없는 연산
.map(n -> n * 2) // 상태 없는 연산
.collect(Collectors.toList());
말 그대로 각 요소를 독립적으로 처리하고 있다. 하나의 패스로 데이터를 처리할 수 있다.
각 코드를 보면 filter 는 filter 코드만 작업하고, map은 map코드만 작업 중이다.
즉, 각 요소가 다른 요소와 독립적으로 처리가 되는 중이다.
상태있는 연산은 다른 요소의 정보가 현재 요소 처리에 영향을 주며 distinct, sorted, limit, skip등이 있다.
이는 여러번 데이터 패스 필요할 수 있으며 중간 결과를 버퍼링해야 할 수 도 있따. 또한, 병렬 처리 시 성능 저하가 발생할 수 있다.
너무 내용이 긴 관계로 한번 끊어서 다음 시간에 이어서 글을 더 읽어보도록 하겠다.
출처
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html
https://www.youtube.com/watch?v=rbm87IFpwvQ&t=12s
https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html
사실 stream에 대해 묻는 면접관이 있었다.
질문이 이랬다.
stream이 뭐죠? 자바8에서 많이 쓰이는데... 이거 쓰면 좋나요?
뭐가 어떻게 좋은거죠?
사실 너무 당연하게 써왔던지라 당황하고 말았다.
그냥 너무 숨쉬듯이 매일 치던 명령어인데 설명하라니 허억 당황하고 말았다.
그러면서 뒤돌아봤는데,
그동안 어쩌면 최소한의 이해 만으로 빠르게 코드만 따라 치는 경향이 있던 것 같다.
오늘도 화이팅!
'programming language > Java' 카테고리의 다른 글
[Java] Stream(스트림) - 2탄 (0) | 2025.05.08 |
---|---|
[Java] 람다 표현식(Lambda Expreesion) - 2탄 (0) | 2025.05.01 |
[Java] 익명 클래스(Anonymous Class) (0) | 2025.04.23 |
[Java] : Local Class알아보기 (4) | 2025.04.19 |
[Java] : Nested Class 와 Inner class (0) | 2025.04.18 |