Java Stream API Basic 1-1


들어가며

이전 버전과 비교하여 자바8에서는 상당히 많은 내용들이 추가되었다. 그 중 가장 도드라지게 많이 사용되는 것이 Stream API일 것이다.

이번 장에서는 Stream API를 활용하기에 앞서 간단하게 개념을 살펴보고, 기존에 많이 사용하던 Collection과 비교하며 어떤 차이점을 갖고있는지 살펴보자.


Stream (숲)

선언형 데이터 처리

Stream을 사용하면 데이터를 선언형으로 처리할 수 있다. 여기서 선언형이란 의미가 와닿지 않는다면 데이터를 ‘How(어떻게)’ 처리하는지를 기술하는게 아니라 What(무엇)을 하는지를 기술하는 것이라고 이해하면 쉽다.

이를테면

위와 같은 코드는 “숫자 요소들 중에서 10 이상인 요소들만 걸러내라“와 같이 What(무엇)을 하는지를 표현한다.

반면

위와 같은 코드는 “숫자 요소들을 순회하며 10 이상인 요소인지 검사하고, 만족한다면 결과 리스트에 해당 요소를 추가하고, 만족하지 않는다면 아무것도 하지 않는다.“와 같이 Filtering(what)을 위해서 어떻게(How)를 해야하는지를 자세히 기술한다.

선언형으로 처리되는 코드는 What을 표현하기 때문에 보다 간결하고 직관적이다. 다시말해 높은 가독성을 가진 코드를 작성할 수 있다.

손쉬운 파이프라이닝

Stream의 각 중간연산(filter, map 등)은 서로 연결 및 조립하여 복잡한 데이터 처리를 위한 파이프라인을 만들 수 있다.


Stream이란 (나무-1)

자바8에서 부터 Collection에는 Steam을 반환하는 .stream() 오퍼레이션이 추가되었다.

Stream이란 ‘데이터 처리 연산’을 지원하도록 ‘소스’에서 추출된 ‘연속된 요소(Sequence of elements)’ 로 정의할 수 있다.

아래에서 연속된 요소, 소스, 데이터 처리 연산이 구체적으로 무엇을 의미하는지 하나씩 살펴보자.

연속된 요소

Collection과 마찬가지로 Stream특정 타입으로 이루어진 연속된 값 집합의 인터페이스를 제공한다.

다만 Collection의 경우 목적이 데이터를 저장하는 자료구조이기 때문에 요소의 저장 및 삭제(add/put, remove 등)접근(get 등)이 주를 이룬다.

반면 Stream의 경우 목적이 데이터 처리를 위한 연산이기 때문에 filter, sorted, map처럼 계산식(연산)이 주를 이룬다.

소스

StreamCollection, Array, I/O 자원 등의 데이터 제공 소스로부터 데이터를 ‘소비(Consume)’한다. 만일 정렬된 컬렉션으로부터(소스) 스트림을 생성하면 정렬이 그대로 유지된다.

데이터 처리 연산

Stream은 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 데이터베이스와 비슷한 연산을 지원한다. 이를테면 filter, map, reduce, find, match, sort 등으로 데이터 처리가 가능하다. 스트림 연산은 순차적 또는 병렬적으로 실행할 수 있다.


Stream의 두 가지 중요한 특징(나무-2)

파이프라이닝(Pipelining)

대부분의 스트림 연산은 스트림 연산끼리 연결해서 커다란 파이프라인을 구성할 수 있도록 스트림 자신을 반환한다. 그 덕분에 게으른 연산쇼트서킷같은 최적화도 얻을 수 있다.

내부 반복

반복자(Iterator 등)을 이용하여 명시적으로 반복하는 Collection과는 달리 Stream은 내부 반복을 지원한다.


예시를 통한 이해

Stream 연산을 살펴보면

  1. menu.stream()을 통해서 데이터 소스menu로 부터 Stream을 얻는다.
  2. filter, map, limit에 이르기 까지 데이터 처리 연산을 조립하여 파이프라인을 구성한다.
  3. 최종 연산인 collect을 통해 파이프라인 처리를 진행하여 결과를 반환한다. 여기서 중요한 점은, ‘collect’가 호출되기 전 까지는 기존의 filter, map, limit의 연산이 수행되지 않는다는 점이다. 즉, ‘게으른 연산’으로 처리된다.

Stream과 Collection(나무-3)

Collection데이터 저장이 목적인데 반해, Stream데이터 연산이 목적이다. 그래서 Stream요청이 있을 때 연산을 수행하는 게으른 특징 을 갖는다.

  • Collection은 비디오 실행(연산/계산)을 위해 사전에 데이터가 모두 저장되어 있어야 하는 ‘DVD’ 에 비유 할 수 있다.

  • Stream은 비디오 실행(연산/계산)을 위해 필요할 때 실행에 필요한 데이터가 제공되는 ‘인터넷 스트리밍’ 에 비유할 수 있다.

위와 같은 특징을 보다 잘 이해하기 위해 아래의 항목들을 머릿속에 개념화 시켜보자.

언제(when) 처리하느냐

아래의 예시는 사실 비즈니스 로직을 통해 서로 다르게 구현이 가능하지만, 어디까지나 CollectionStream의 차이를 설명하기 위한 극단적인 예시임을 이해해주기 바란다.

숫자로 이루어진 연속된 데이터에 대해서 총 두 개의 연산을 수행한다고 가정하자.

  1. 첫 번째 연산 : 소수 데이터 추출
  2. 두 번째 연산 : 홀수 번째 데이터 추출
  • Collection의 경우는 연산을 진행하기 위해서는 모든 값을 저장해야 한다. 따라서 첫 번째 연산인 소수만 추출을 위해서 우선 모든 소수를 저장을 진행해야 한다. 그리고 해당 연산이 종료된 후 모든 소수가 저장된 Collection을 대상으로 홀수 번째 요소들을 필터링해야 한다.

위 코드를 살펴보면 첫 번째 연산(first)을 수행함으로써 모든 소수를 저장한다. 즉, 두 번째 연산(second)를 수행하기 위해 사전에 모든 데이터가 저장되어 있어야 한다.

만일 세 번째, 네 번째 등 다른 연산이 추가된다면 이후 연산을 위해 각 연산 시에 모든 데이터들을 저장해야 한다.

이것이 바로 Collection이 ‘연산 진행을 위해 모든 데이터들을 사전에(when) 처리(저장)’해야 함을 의미한다.

  • Stream의 경우 이론적으로 요청할 때만 요소를 계산 한다. 다시말해 연산을 위해 미리 데이터들을 저장할 필요가 없다. 이는 게으른 연산을 살펴보면 쉽게 이해가 가능하다.

위 코드는 mainsimple로 구성되어 있는데, 동일한 코드를 간소화 시킨 것이다.

우선 첫 번째 연산(filter)두 번째 연산(filter)을 수행하여도 각 단계에서 연산이 수행되지 않는다. firstStream, secondStream에는 연산의 결과가 저장되어있지 않다.

마지막 최종 연산인 collect를 수행했을 때 비로소 첫 번째 연산과 두 번째 연산이 수행된다. 여기서 주의해야 할 점이 있다. 각 연산이 수행된다는 의미가 첫 번째 연산이 모두 수행된 다음 두 번째 연산이 수행된다는 의미가 아니다!

만일 이처럼 연산된다면 Collection과 다를 바 없어진다. Stream요청할 때 요소를 계산한다고 하였다. 이는 내부적으로 각 연산이 파이프라이닝 되어 각 요소들을 요청하여 소비(연산)한다.

처음엔 조금 아리송 할지 몰라도 몇 가지 키워드를 머릿속에 넣어놓고 상기해보면 어느순간 이해 될 것이다.

  • Stream은 데이터의 흐름이다.
  • Stream데이터의 흐름파이프라이닝하여 처리한다.
  • Stream은 각 연산들이 생성자소비자관계를 형성한다.
  • Stream은 소비자가 값을 요청할 때만 계산한다.

컬렉션과 스트림의 철학적 접근

스트림시간적으로 흩어진 값의 집합으로 간주 할 수 있다. 반면 컬렉션공간적으로 흩어진 값의 집합 으로 볼 수 있다.


참고 및 출처

  • 모던 자바 인 액션