4장: 스트림
데이터셋에 여러 복잡한 연산이 필요하다면 스트림을 사용해보자.
스트림
데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소(Sequence of elements)
연속된 요소: 스트림은 컬렉션처럼 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스를 제공한다. 컬렉션에서는 시간과 공간의 복잡성과 관련된 요소의 저장 및 접근 연산을 제공하는 반면 스트림은 표현 계산식을 제공한다. 즉, 컬렉션의 주제는 데이터고 스트림의 주제는 계산이다.
소스: 스트림은 컬렉션, 배열 I/O 자원 등의 데이터 제공 소스로부터 데이터를 소비한다.
데이터 처리 연산: 스트림은 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 데이터베이스와 비슷한 연산(filter, map, reduce, find, match, sort 등)을 지원한다. 이 연산들은 순차적, 병렬로 실행할 수 있다.
아래 예제를 보면 menu는 데이터 소스이며 연속된 요소를 스트림에 제공한다. 이 스트림에 filter, map, limit, collect로 이어지는 데이터 처리 연산을 적용한다. 이 때 collect를 제외한 모든 연산은 파이프라인을 형성할 수 있도록 스트림을 반환한다. 마지막으로 collect 연산으로 파이프라인을 처리해 결과를 반환한다.
특징
선언형
선언형(데이터를 처리하는 임시 구현 코드 대신 질의로 표현)으로 컬렉션 데이터를 처리할 수 있다.
즉, 컬렉션을 직접 반복하지 않고 원하는 것을 질의만 하면 얻을 수 있도록 도와준다.
다음 SQL문과 같은 결과를 자바의 List<Dish>로 얻어야한다면, for문과 if문 등의 반복자, 누적자를 이용할 수 밖에 없었지만, 스트림을 사용하면 간단하게 질의만 하면 된다.
조립 가능
filter, sorted, map, collect 같은 고수준 빌딩 블록 연산을 연결해 복잡한 데이터 파이프라인을 만들 수 있다.
덕분에 유연성이 좋아진다.
병렬 처리
내부적으로 단일 스레드 모델에 사용할 수 있지만 멀티코어 아키텍처를 최대한 투명하게 활용할 수 있게 구현되어 특정 스레딩 모델에 제한되지 않고 자유롭게 어떤 상황에서든 이용할 수 있다.
따로 멀티스레드 코드를 구현하지 않아도 데이터 처리 과정을 병렬화하면서 스레드와 락을 알아서 처리해주어 성능 향상 효과를 볼 수도 있다.
파이프라이닝
대부분의 스트림 연산은 스트림 연산끼리 연결해 거대한 파이프라인을 구성할 수 있도록 스트림 자신을 반환한다. 그 덕분에 laziness, short-circuiting 같은 최적화가 가능하다. (5장 참고)
내부 반복
반복자를 직접 명시해 반복하는 컬렉션과 달리 내부 반복을 지원한다.
스트림과 컬렉션
자바의 컬렉션과 스트림 모두 순차적으로 값에 접근하는 연속된 요소 형식의 값을 저장하는 자료구조의 인터페이스를 제공한다.
DVD와 유튜브를 볼때의 차이처럼 데이터를 언제 계산하는지 차이가 있다.
컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장한다. 즉, 컬렉션의 모든 요소는 컬렉션에 추가하기 전에 계산되어야한다. 예를 들어 소수를 담는 컬렉션을 생성할 때에 모든 요소를 포함하려 할 것이므로 컬렉션 생성 작업이 영원히 끝나지 않을 것이다.
스트림은 이론적으로 요청할 때만 요소를 계산하는 고정된 자료구조로, 스트림 자체에 요소를 추가하거나 제거할 수 없다. 오직 사용자가 요청하는 값을 스트림에서 추출할 뿐이다.
탐색은 1회뿐
스트림은 반복자와 마찬가지로 단 한번만 탐색할 수 있다. 한 번 탐색한 요소를 다시 탐색하려면 초기 데이터 소스에서 새로운 스트림을 만들어야 한다. 이 때 초기 데이터가 I/O채널처럼 한번 사용되고 소멸되면 반복이 불가능하며, 컬렉션처럼 반복 사용할 수 있는 데이터 소스여야한다.
외부 반복과 내부 반복
for-each 문을 직접 사용자가 명시해줘야 하는 외부 반복 방법 대신 스트림에서는 내부적으로 알아서 반복하여 작업을 수행해준다.
스트림은 내부 반복을 통해 병렬성을 최적화하여 관리해준다.
스트림 연산
중간 연산
입력 인자를 받아 다른 스트림을 반환한다.
파이프라인처럼 여러 연산을 연결할 수 있다.
lazy한 특성으로 인해 최종 연산을 스트림 파이프라인에 실행하기 전에는 아무 연산도 수행하지 않는다.
쇼트 서킷 기법으로 최적화가 가능하다.
아래 주석과 같이 서로 다른 여러 연산이 한 과정으로 병합(루프 퓨전)되어 수행된다.
최종 연산
스트림 파이프라인에서 결과를 도출해 반환한다.
보통 최종 연산에 의해 List, Integer, void 등 스트림 이외의 결과가 반환된다.
스트림 순서
질의를 수행할 컬렉션 같은 데이터 소스를 준비한다.
스트림 파이프라인을 구성할 중간 연산들을 연결한다.
스트림 파이프라인을 실행하고 결과를 만들 최종 연산을 연결해 수행한다.
스트림은 빌더 패턴에서 여러 파라미터를 설정한 후(중간 연산) build() 메서드를 호출(최종 연산)해 객체를 생성하는 방식과 유사하다.
Last updated