본문 바로가기
Language/Java

[Java 8] Stream - 1 (Stream 개요)

by ocwokocw 2021. 2. 11.

- 출처: https://www.oracle.com/technical-resources/articles/java/ma14-java-se-8-streams.html

- Stream 소개

컬렉션이 없다면 무슨일이 벌어질까? 거의 모든 자바 어플리케이션에서는 컬렉션을 만들고 처리한다. 이런 업무들은 프로그래밍의 근간이다. 데이터를 그룹화하며 처리한다.

 

예를 들어 메뉴를 구성하는 요리 컬렉션이 있는데 각 요리 칼로리의 총합을 구하거나, 칼로리가 적은 요리만을 골라 특별 식단을 구성하는것과 같은 기능을 원한다고 해보자. 이처럼 많은 비즈니스 로직에는 찾거나 그룹화하는 연산이 포함된다. Java 8 이전에는 위와같은 동작들이 필요할때마다 구현하여 사용하였을것이다. 간단한 SQL을 구문을 하나 살펴보자.

 

SELECT
    NAME
FROM
    DISHES
WHERE
    CALORIE < 400

 

위와 같은 SQL을 작성할 때 속성과 테이블이 바뀐다고 하면 이름을 변경할 뿐이지 또 하나의 SQL 구현을 새로 프로그래밍 하지는 않는다. 본질적으로 400 미만이라는 선언적인 구문은 똑같기 때문이다. 또 병렬 프로그래밍을 하기 위해서 많은 복잡한 문법들 멀티 스레드에서 다룰때 조심해야하는 부분들을 신경쓰면서 해왔지만 Java 8 에서는 새로운 문법을 제시한다. 이런것들을 가능하게 하기 위해 Java 8 에서는 Stream 이라는 새로운 기능을 제공한다.


- Stream 이란 무엇인가?

Stream 이란 간단히 말하면 "집계 연산들을 지원하는 Source 의 원소들의 Sequence" 이다. 영어를 해석하니 뭔가 말이 이상해졌긴 하지만 하나씩 단어를 살펴보자.

  • 원소들의 Sequence: Stream은 특정 원소 타입 값 Set 에 대한 인터페이스를 제공한다. 그러나 Stream 은 실제로 원소들을 저장하진 않는다. 그들은 필요 시점에 계산되는 개념이다.
  • Source: Stream은 컬렉션이나 배열, I/O 리소스들과 같은 데이터를 제공하는 Source 로 부터 소비한다.
  • 집계 연산들: Stream은 SQL 같은 연산들을 제공하며 Functional 프로그래밍 언어에서 지원하는 일반적인 연산들(filter, map, reduce, find, match, sorted 등)을 지원한다.

 

또 Stream은 컬렉션 연산과 차별되는 2가지 특징이 있다.

  • Pipelining: 많은 Stream 연산자들은 stream 자기 자신을 반환한다. (빌더 패턴에서 자기 자신을 반환하면 종결 메소드 호출까지 자기 자신의 메소드를 호출할 수 있는것을 떠올려 보자.) 이런 특징은 laziness 와 short-circuiting (과정이나 연산 중간에 확실한 답이 나와서 더이상의 진행이 무의미할때 결론을 미리 내는것과 같은 기법)을 가능하게 한다.
  • 내부 반복: 컬렉션은 반복이 명시적으로 드러나지만 Stream 연산은 내부적으로 반복을 수행한다.
List<Integer> transactionsIds = 
    transactions.stream()
                .filter(t -> t.getType() == Transaction.GROCERY)
                .sorted(comparing(Transaction::getValue).reversed())
                .map(Transaction::getId)
                .collect(toList());

 

위의 코드와 그림을 살펴보자.

 

우선 transactions 의 list 형에서 stream() 메소드를 호출하여 stream 형태를 얻었다. transatcions 의 list 가 데이터소스이며, stream에 원소들의 Sequence를 제공할것이다. 그다음 Stream 에서 filter, sorted, map과 같은 집계 연산들을 적용하였다.

 

집계 연산들은 collect 가 호출되기 전까지는 실제로 수행되지 않는다. collect 연산은 결과를 반환하기 위해 파이프라인을 처리하기 시작한다. stream 에서 사용가능한 다른 메소드들을 살펴보기 전에 잠시 Collection과 Stream에 대해서 생각해보자.


- Stream vs Collections

기존 collections과 streams 의 새로운 개념 모두 원소들의 sequence 에 대한 인터페이스를 제공한다. 간단히 차이점을 말하면 collections는 데이터에 관한 것이고 streams 는 계산에 관한것이다.

 

DVD에 저장되어있는 영화를 상상해보자. 이것은 컬렉션이다. 왜냐면 전체 데이터 구조를 포함하고 있기 때문이다. 이제 같은 비디오를 인터넷을 통해 stream 방식으로 본다고 가정해보자. 이제는 stream 이다. 스트리밍 비디오 플레이어는 유저가 보고있는 부분의 몇몇 프레임들을 미리 다운로드 받아야 한다. 그러면 stream 의 대부분의 값들이 계산되기 전에 stream 의 시작부분 부터는 시청을 할 수 있다.

 

상스러운 말로 collections 와 streams 의 차이점은 무엇인가 언제 계산되느냐라고 할 수 있다. 컬렉션은 메모리 내장 구조인데 모든 값들을 데이터 구조가 가지고 있으며 컬렉션의 모든 원소는 컬렉션에 더해지기 전에 계산이 완료되어있어야 한다. 반면 stream은 개념적으로 고정 데이터 구조이며 원소들은 요청시에 계산된다.

 

Collection 인터페이스를 사용할때는 사용자에 의해 반복자가 사용되어야 한다. (예를 들면 foreach로 불리는 향상된 for 문 같은 경우 - 외부 반복문)

 

List<String> transactionIds = new ArrayList<>(); 
for(Transaction t: transactions){
    transactionIds.add(t.getId()); 
}

 

대조적으로 Streams 라이브러리는 내부 반복을 사용한다. 반복을 내부적으로 알아서 하며 stream의 결과 값을 어디에 저장할 것인가를 고려한다. 우리는 단지 함수에 어떤 동작이 수행되어야 하는지만 기술하면 된다.

 

List<Integer> transactionIds = 
    transactions.stream()
                .map(Transaction::getId)
                .collect(toList());

'Language > Java' 카테고리의 다른 글

[Java 8] Stream - 3 (Numeric, Stream 생성)  (1) 2021.02.11
[Java 8] Stream - 2 (기본 연산)  (0) 2021.02.11
[Java 8] 람다(Lambda) - 3  (0) 2021.02.11
[Java 8] 람다(Lambda) - 2  (0) 2021.02.10
[Java 8] 람다(Lambda) - 1  (0) 2021.02.10

댓글