본문 바로가기

Language/Java

Java - Stream 이란? (Stream과 Collection, Stream 각 연산)

1. Stream 이란

Stream에 대해 알아보도록 하겠습니다. 이 글은, 모던 자바 인 액션이라는 책을 기반으로 작성되었습니다.

 

 

 

1. Stream 이란

데이터 처리연산을 지원하도록 소스에서 추출된 연속된 요소입니다.

 

1) 연속된 요소

Collection과 마찬가지로, Stream은 특정 요소 형식으로 이루어진 연속된 값 집합 인터페이스를 제공합니다.

Collection의 경우에는 시간과 공간의 복잡성과 관련된 요소의 저장 및 접근 연산이 주를 이루고, Stream의 경우에는 filter, sorted, map과 같은 데이터를 처리하기 위한 계산식이 주를 이룹니다.

 

2) 소스

Stream은 Collection, 배열, I/O 자원 등의 데이터 제공 소스로 부터 데이터를 소비합니다.

 

3) 데이터 처리연산

Stream은 filter, map, reduce, find, match, sort등의 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 데이터베이스와 비슷한 연산을 지원합니다.

 

 

 

 

 

2. Stream과 Collection

2.1 계산 시점의 차이

StreamCollection의 가장 큰 차이는, 데이터 계산을 하는 시점입니다.

Collection의 경우 현재 자료구조(LinkedList, ArrayList)가 포함하는 모든 데이터를 메모리에 저장하는 자료구조입니다. 즉, Collection에 포함될 각 요소들은 포함이 되기전에 꼭 계산이 완료되어야합니다.

 

반면에 Stream의 경우에는 요청할 때만 요소를 계산하는 고정된 자료구조(Stream에 요소를 추가 또는 삭제 불가)입니다.

 

 

2.2 외부반복과 내부반복

위의 예제는 외부반복의 예제입니다. Collection인터페이스를 사용하기 위해서는 사용자가 직접 for-each를 통해 반복문을 만들어 연산 처리를 작성해야합니다.

 

반면 Stream의 경우에는 반복을 알아서 처리하고, 결과 Stream값을 어딘가에 저장해주는 내부반복을 사용합니다. 내부반복을 사용한다면 아래와 같은 이점이 있습니다.

1) 반복자를 사용할 필요가 없다.

- 내부에서 자동으로 반복을 처리해주기 때문에 사용자가 직접 반복문을 처리할 필요가 없습니다.

2) 병렬성 처리의 이점

- 외부반복을 사용하는 경우, 병렬처리를 위해서는 스레드간의 공유자원에 대해 동기화(한순간에 한 스레드만 공유자원에 접근하는 코드를 실행하도록)를 처리해주어야 하지만, 내부반복을 사용한다면 이를 관리할 필요가 없습니다.

 

 

 

 

 

3. Stream 사용

위의 데이터는 앞으로의 Stream 연산 예제에서 사용될 데이터입니다.

 

 

3.1 필터링

filter

filter연산은 프리디케이트(boolean을 반환하는 함수)를 인수로 받아 true를 반환하는 모든 요소를 포함하는 스트림을 반환합니다.

 

위 예제는 각 요소(i)가 3보다 큰 경우를 찾아 스트림으로 반환하는 예제입니다.

 

 

 

distinct

distict의 경우 stream안의 중복된 요소를 제거하는 연산입니다. 중복된 1, 2를 고유하게 만들어 줍니다.

 

또한 toSet()을 이용하더라도 중복된 요소를 제거할 수 있습니다.

 

 

 

takeWhile (JDK 1.9이상)

takeWhile은 stream의 요소가 filter의 비교 대상을 기준으로 순서대로 정렬되어 있는 경우, 인수로 전달받은 프리디케이트가 처음으로 false를 반환하기 전까지의 요소들을 포함하는 stream을 반환합니다.

 

filter는 모든 요소들을 순회하며 조건을 검사하는 반면, takeWhile은 특정 조건에 일치하는 동안만 요소를 순회하기 때문에, stream안의 요소가 매우 많은 경우, 그 효율성을 무시할 수 없습니다.

 

 

 

dropWhile (JDK 1.9이상)

dropWhile은 인수로 전달받은 프리디케이트가 true를 반환하는 모든 요소를 버리고, 나머지 요소들을 포함하는 stream을 반환합니다.

 

 

 

3.2 매핑

map

map연산은 Stream을 새로운 Stream으로 변환하는 연산을 합니다. 위 예제의 경우 원래의 Integer StreamString Stream으로 변환하는 예제입니다.

 

 

 

3.3 검색, 매칭

anyMatch

anyMatch는 인수로 전달받은 프리디케이트가 true를 반환하는 요소가 하나라도 존재한다면, true값을 반환합니다.

 

 

noneMatch

noneMatch는 인수로 전달받은 프리디케이트가 true를 반환하는 요소가 존재하지 않는 경우 true를 반환합니다. (allMatch의 경우 noneMatch와 정반대의 연산을 진행합니다.)

 

 

findFirst

findFirst의 경우 스트림에서 첫번째 요소를 찾아 반환합니다. 하지만 이 연산의 경우 Optional을 반환하는데, 이유는 잘 생각해보면 간단합니다. Stream은 여러가지 연산을 거칠 수 있으며 연산의 결과로 Stream에 한개의 요소도 포함이 되어있지 않을 수도 있습니다. 따라서 Optional을 통해 null관련 에러를 피할 수 있습니다.

 

 

 

3.4 리듀싱

리듀싱은 모든 스트림의 요소를 처리하여 하나의 값으로 도출합니다. 즉, 스트림의 요소가 하나의 값으로 줄어들 때 까지 람다는 각요소를 반복하여 연산 및 조합을 합니다.

 

위의 예제는 초기값 0에, 두 요소 a, b를 더해 결과를 반환하는 예제입니다.

 

 

반복연산 대신 reduce를 사용하는 이유

반복연산을 통해 위와같은 작업을 처리하는 경우, 병렬적으로 처리하기 위해서는 동기화 작업이 필요합니다. 동기화를 하더라도 결국 병렬화로 얻는 이득이 스레드간의 소모적 경쟁 때문에 상쇄되어버립니다. 반면 reduce를 사용하면 병렬작업을 편리하게 이용할 수 있습니다.

 

하지만 reduce를 이용하여 병렬작업을 처리하는 경우, 두가지 제한 사항을 만족해야 합니다.

1) 람다에서 사용되는 변수값이 외부의 코드를 통해 바뀌면 안됍니다.

2) 결과값이 연산 순서에 영향을 받으면 안됍니다.