본문 바로가기
도서/모던 자바 인 액션

모던 자바 인 액션 - 5장 다양한 스트림 활용

by Ahngyuho 2023. 8. 2.

해당 포스팅은 모던 자바 인 액션이라는 책을 읽고 정리한 것입니다. 

 

이 장에서 배울 내용

 

  • 필터링, 슬라이싱, 매칭
  • 검색, 매칭, 리듀싱
  • 특정 범위의 숫자와 같은 숫자 스트림 사용하기
  • 다중 소스로부터 스트림 만들기
  • 무한 스트림

이번 장에서는 스트림 API가 지원하는 다양한 연산을 살펴봅니다.

필터링, 슬라이싱, 매핑, 검색, 매칭, 리듀싱 등 다양한 데이터 처리 질의를 한번 배워보겠습니다.

그리고 여러 종류의 데이터 소스를 스트림으로 만드는 방법과, 무한 스트림과 같은 특수한 스트림도 한번 배워보겠습니다.

 

5.1 필터링

Predicate를 이용해서 요소를 필터링하는 방법과 고유 요소만 필터링하는 방법이 존재합니다.

 

5.1.1 Predicate로 필터링

이렇듯 이 filter 메서드는 Predicate를 인자로 전달받아 조건에 부합하는 요소들을 Stream 형태로 반환합니다.

 

5.1.2 고유 요소 필터링

여기서 고유 요소란 유일하게 식별되는 요소를 뜻하고, 그 여부는 스트림에서 만든 객체의 hashCode,equals로 결정됩니다.

해당 코드는 짝수만을 뽑아내어 고유한 요소만 출력하는 코드입니다.

 

5.2 스트림 슬라이싱

스트림 슬라이싱을 이용해서 좀 더 효율적으로 스트림 작업을 실행해 봅시다.

 

5.2.1 프레디케이트를 이용한 슬라이싱

자바 9에서는 스트림의 요소를 효율적으로 선택할 수 있도록 takeWhile, dropWhile 두 가지 새로운 메서드를 지원합니다.

 

TAKEWHILE 활용

해당 리스트는 칼로리를 기준으로 오름차순 되어 있습니다.

 

칼로리가 320이상인 Dish를 뽑아내고 싶다면

FilterPredicate를 전달해서 뽑아낼 수 있습니다.

하지만 위 리스트는 이미 오름차순으로 정렬되어 있음이 명확합니다.

그래서 위와 같이 모든 요소를 탐색하는 것은 비효율적입니다.

 

이때 필요한 것이 바로 takeWhile 연산입니다.

takeWhile을 이용하면 무한 스트림을 포함한 모든 스트림에 프레디케이트를 적용해서 스트림을 슬라이싱 할 수 있습니다.

takeWhile320보다 크거나 같은 칼로리를 발견하는 순간 반복을 멈추고 스트림을 반환합니다.

 

 

DROPWHILE 활용

만약 320보다 큰 요소들만 뽑아내고 싶다면 DROPWHILE을 활용할 수 있습니다.

DROPWHILEPredicate의 결과가 처음으로 거짓이되는 부분부터 처음 요소까지 모두 버리고 나머지 요소들을 스트림과 함께 반환합니다.

 

5.2.2 스트림 축소

스트림은 주어진 n개 이하의 요소를 갖는 스트림을 반환하는 limin(n)이라는 연산을 제공합니다.

스트림이 정렬되어 있으면 최대 요소 n개를 반환할 수 있습니다.

위 코드는 Predicate 조건과 일치하는 최대 3개의 요소를 반환합니다.

정렬되지 않는 스트림(데이터 요소가 Set인 경우)에도 limit를 사용할 수 있습니다. 소스가 정렬되어 있지 않으면 결과도 정렬되지 않은채로 반환됩니다.

 

5.2.3 요소 건너뛰기

스트림은 처음 요소 n개를 제외한 스트림을 반환하는 skip(n) 메서드를 지원합니다.

N개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 스트림을 반환합니다.

5.3 매핑

특정 객체에서 특정 데이터를 선택하는 작업을 배워보겠습니다.

스트림 APImap flatMap 메서드는 특정 데이터를 선택하는 동작을 지원합니다.

 

5.3.1 스트림의 각 요소에 함수 적용하기

Map메서드는 전달된 함수를 데이터 소스 요소에 적용하고 그 결과가 새로운 요소로 매핑됩니다.

예를 들어 Dish 요소를 가지는 List 데이터 소스를 String 형태의 데이터 소스로 변환할 수 있습니다.

위의 코드가 map을 이용한 예시 코드입니다.

Map의 결과는 Stream<String> 입니다.

 

만약 문자열 데이터 소스에서 해당 문자열 요소의 문자수를 세어 그 리스트를 반환한다고 할 때

이런 식으로 문자열 요소에 문자열의 길이를 세주는 함수를 적용해서 새로운 형태의 데이터 소스를 만들 수 있습니다.

만약 Dish에서 음식 메뉴명의 길이를 반환한다고 하면 아래와 같이 코드를 작성할 수 있습니다.

이런식으로 map map을 연결하는 연산도 가능합니다!

 

5.3.2 스트림 평면화

문자열을 담고 있는 리스트에서 각 단어를 중복 없이 뽑는 코드를 작성해봅시다.

해당 코드는 원하는 결과를 얻기 부족해보입니다.

결과가 문자열 배열 2개를 가지는 List가 되었습니다.

 

[H,e,l,l,o] , [W,o,r,l,d] 이렇게 결과가 생성된 것입니다. -> map의 결과가 Stream<String[]>…

 

MapArrays.stream 활용

이번에는 map으로 단어를 개별 문자열 배열로 반환하고, 각 배열을 스트림으로 만들어 보겠습니다.

[H,e,l,l,o] , [W,o,r,l,d] 각각의 배열을 stream으로 만든 결과가 나왔습니다.

결국 문제가 제대로 해결되지 않았습니다.

지금 문제가 발생하는 이유는 데이터 소스의 요소, 그 안에 존재하는 데이터들을 각각 배열로 만들어야 했는데 그렇지 못했다는 것입니다.

우선 단어에 개별 문자를 배열로 만들고 이 배열들을 각각의 스트림으로 만들어야 합니다.

[H,e,l,l,o] [W,o,r,l,d] 를 하나의 문자열 스트림으로 만드는 방법이 있습니다!

flatMap 사용

flatMap을 통해 각 배열을 하나의 평면화된 스트림으로 만드는 코드입니다.

 

flatMap은 스트림의 각 값을 다른 스트림(Arrays.stream())으로 만든 다음 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행해 줍니다.

 

flatMap은 각 스트림에 존재하는 요소들을 통합해 하나의 통합된 스트림으로 만들어 준다고 생각하시면 될 거 같습니다.

 

[1,2,3] , [3,4] 이 두개의 리스트가 존재하고 모든 숫자의 쌍 리스트를 결과로 반환해봅시다.

결과가 아까처럼 제대로 나오지 않습니다. Map의 결과는 stream이기 때문이죠

해당 코드의 map 결과를 한번 자세하게 살펴보겠습니다.

빨간색: numbser1의 숫자마다 map이 적용되어 반환되므로 요소가 Stream<int[]>인 새로운 데이터 소스 반환

파란색: map의 결과로 새롭게 생성된 데이터 소스의 stream 반환

 

그래서 이럴 땐 Stream<int[]>인 스트림에서 int[]만을 요소로 가지는 데이터 소스를 생성하기 위해 map말고 flatMap을 사용합시다!

5.4 검색과 매칭

데이터 소스에서 해당되는 속성이 있는지 여부를 검색하는 스트림 API도 존재합니다.

 

5.4.1 Predicate가 적어도 한 요소와 일치하는지 확인

Predicate가 주어진 스트림에서 적어도 하나의 요소가 만족하는지 확인하는 메서드는 anyMatch입니다.

anyMathboolean을 반환하는 최종연산입니다.

 

5.4.2 Predicate가 모든 요소와 일치하는지 검사

  • 모든 메뉴가 칼로리를 1000을 넘기지 않는지 확인
  • 결과가 true면 데이터 소스의 모든 요소가 조건을 만족

 

NONEMATCH

noneMatchallMatch와 반대 연산을 수행합니다. nonMatch는 주어진 Predicate와 일치하는 요소가 없는지 여부를 검색합니다.

 

위에서 살펴본 anyMatch, allMatch, noneMatch는 쇼트서킷 기법을 활용합니다.

자바의 && , ||

쇼트서킷이란

항상 전체 스트림을 모두 확인할 필요가 없습니다. And 연산을 하고자 할 때 하나의 조건이라도 만족하지 않으면 false를 반환하고 끝내버리는 것처럼 표현식에서 하나라고 거짓이라면 나머지 표현식의 결과와 상관없이 전체 결과를 거짓이라고 반환하는 것입니다.

이러한 상황을 쇼트서킷이라고 합니다.

Limit(n) 또한 쇼트서킷 연산을 활용합니다.

 

5.4.3 요소 검색

findAny 메서드는 현재 스트림에서 임의의 요소를 반환합니다. findAny 메서드를 다른 스트림 연산과 연결해서 사용할 수 있습니다.

스트림의 파이프라인은 내부적으로는 단일 과정으로 실행될 수 있으므로 결과를 찾는 즉시 결과를 반환할 수 있습니다.

 

그런데 Optional이라는 처음보는 형태의 클래스가 보입니다.

Optional이란?

Optional<T>는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스입니다.

이전 예제에서 findAny는 아무 요소도 반환하지 않을 수 있습니다. 결과를 단순히 null로 두게 되면 에러가 발생될 수 있으므로 자바 8 라이브러리에는 Optional<T>이라는 것이 추가된 것입니다.

 

일단 Optional<T>가 다음의 기능을 제공하는 것만 알아두고 갑시다

  • isPresent()는 Optional이 값을 포함하면 true을 반환, 아니면 false 반환
  • ifPresent(Consumer<T> block)은 값이 있으면 주어진 블록 실행
  • Consumer<T>는 T형식의 인수를 받으면 void를 반환하는 람다 전달 가능
  • T get()은 값이 존재하면 값을 반환, 없으면 NoSuchElementException을 일으킴
  • T orElse(T other)는 값이 있으면 값을 반환, 없으면 기본값 반환

Optional에 값이 존재하면 전달한 람다 실행!

 

5.4.4 첫 번째 요소 찾기

주어진 데이터 소스가 어떤 논리적인 아이템 순서(예를 들어 정수 오름차순)가 존재할 때

이런 스트림에서 첫 번째  요소를 찾을 수 있는 스트림 API가 있습니다.

findFirst로 첫 번째 제곱값을 반환할 수 있습니다.

findFistfindAny는 언제 사용할까?

해당 메서드는 보통 병렬성 때문에 많이 사용됩니다. 병렬 실행에서는 첫 번째 요소를 찾기 어려우므로 많이 사용된다고 합니다. 순서에 상관이 없는 결과를 반환하는 병렬 스트림 사용시에는 보통 제약이 더 적은 findAny를 사용한다고 합니다.

 

5.5 리듀싱

메뉴의 모든 칼로리의 합계를 구하시오.’ ‘메뉴에서 칼로리가 가장 높은 요리는?’

이런 연산을 간편하게 수행할 수 있는 스트림 API가 존재합니다.

이번에는 스트림의 요소를 종합해서 결과를 반환하는 스트림 API에 대해서 알아보겠습니다.

 

5.5.1 요소의 합

우선 외부반복을 사용해서 리스트안에 존재하는 정수들의 합을 구하는 코드를 작성해보겠습니다.

이제 더한다는 동작을 ~하는 동작으로 추상화 수준을 올려보겠습니다! 중복 제거를 위하여

 

reduce에는 다른 람다를 전달해 줄 수 있습니다.

 

그리고 이런 식으로 동작을 파라미터화해서 더하기만 할 수 있던던 코드에서 원하는 동작을 전달해서 모든 요소에 적용하는 코드를 작성할 수 있게 되었습니다!

 

Reduce는 어떤 식으로 동작하는 걸까요?

처음 a에는 초기값이 들어가게 됩니다. 그리고 다음 요소인 b와 더합니다.

만약 다음 요소가 존재한다면 누적값을 a, 그리고 다음 요소를 b에 넣고 람다에 다시 전달합니다.

이 과정을 모든 요소를 소비할 때까지 반복하는 것입니다.

 

메서드 참조를 이용한 더 간단한 코드

 

Mapreduce를 이용해서 연산을 적용하는 예시를 작성해 봅시다.

 

이 연산 형태는 mapreduce를 연결하는 기법인 맵 리듀스 패턴이라고 하며, 쉽게 병렬화하는 특징이 있어서 구글이 웹 검색에 적용해 유명해졌다고 합니다.

 

5.7 숫자형 스트림

5.4절에서 reduce 메서드를 이용해 스트림 요소에 적용해 합을 구하는 예제를 살펴봤었습니다.

이런 식으로 모든 메뉴의 칼로리 합을 mapreduce를 통해 구할 수 있습니다.

사실 위 코드는 박싱 과정이 숨겨져 있습니다. Caloriesint -> Integer

현재 map의 결과는 Stream<Integer>이지만 Stream 인터페이스에는 sum과 같은 함수는 제공되지 않고 있습니다. 그 이유는 스트림안의 요소들이 기본형만 오는 것이 아닌 일반 객체도 올 수 있고, 해당 객체에는 sum과 같은 합계 연산이 없을 수 있기 때문입니다.

 

그래서 Stream은 기본형 특화 스트림이라는 것을 제공합니다.

 

5.7.1 기본형 특화 스트림

자바 8에서는 세 가지 기본형 특화 스트림을 제공합니다. 스트림 API는 박싱 비용을 피할 수 있도록

‘int 요소에 특화된 IntStream’ ‘

double 요소에 특화된 DoubleStream’

‘long 요소에 특화된 LongStream’을 제공합니다.

각 인터페이스에는 합계를 계산하는 sum, 최대값을 검색하는 max 같이 숫자와 관련되 리듀싱 연산을 제공합니다.

이런 특화 스트림은 오직 박싱 과정에서 발생하는 효율성과 관련되어 있으며 이것외에 스트림에 추가 기능을 제공하지는 않는다고 합니다.

 

숫자 스트림으로 매핑

스트림을 특화 스트림으로 변환할 때는 mapToInt, mapToDouble, mapToLong 세 가지 메서드를 가장 많이 사용합니다. 이 메서드들은 map과 같은 기능을 수행하지만 map과 달리 Stream<T> 대신 특화 스트림을 반환합니다.

이렇게 특화 스트림에는 합계처럼 숫자에 관련된 메서드를 제공합니다.

mapToInt의 반환형은 IntStream임을 기억합시다! (Stream<T> 아님)

그래서 이제는 Stream<T> 인터페이스에는 존재하지 않는 sum메서드를 사용할 수 있게 됐습니다.

만약 스트림이 비어 있으면 sum은 기본값 0을 반환합니다. 뿐만 아니라 max, min, average 등 다양한 유틸리티 메서드를 지원합니다.

 

객체 스트림으로 복원하기

 

객체 스트림으로 복원하기

특화 스트림에서 특화되지 않은 일반적인 스트림으로 변환해주는 API가 존재합니다.

 

이런 식으로 특화 스트림을 일반 스트림으로 변환할 수 있습니다.

 

기본값: OptionalInt

IntStream의 제공되는 연산 사용시 기본값 때문에 잘못된 결과가 도출될 수 있습니다.

최대값을 찾는 경우 0이라는 기본값 때문에 잘못된 결과가 나올 수 있습니다.

스트림에 요소가 없는 상황과 실제 최대값이 0인 상황을 구별할 수 있는 클래스가 존재합니다.

Optional의 기본형 특화 스트림 버전으로 OptionalInt, OptionalDouble, OptionalLong

그리고 최대값이 없는 상황에서 사용할 기본값을 명시해 줄 수 있습니다.

 

5.7.2 숫자 범위

IntStream, LongStream에는 특정 범위의 숫자를 반환하는 메서드인 rangerangeClosed를 제공합니다.

RangerangeClosed의 차이는 인자로 전달되는 시작값과 종료값의 포함 여부입니다.

Range이 경우 시작, 종료값이 포함되지 않고, rangeClosed는 모두 포함됩니다.

 

이런 식으로 활용할 수 있습니다.

 

5.7.3 숫자 스트림 활용: 피타고라스 수

기본형 특화 스트림을 좀 더 제대로 활용할 수 있게 피타고라스 수 스트림을 만들어보는 코드를 작성해 보겠습니다.

 

피타고라스 수

a*a + b*b = c * c 공식을 만족하는 a, b, c를 피타고라스 수라고 합니다.

 

세 수 표현하기

세 요소를 갖는 int 배열을 사용해 봅시다. New int []{3,4,5}

이제 인덱스로 배열의 각 요소에 접근할 수 있습니다.

 

좋은 필터링 조합

Ab 이 두개가 피타코라스 수의 일부가 될 수 있는지 확인할 수 있는 방법이 있습니다.

Math.sqrt(a*a + b*b) % 1 == 0;

우선 제곱근이 정수인지 확인하는 것입니다.

X가 부동소수점이라면 x % 1.0이라는 자바 코드로 소수점 이하의 부분을 얻을 수도 있습니다.

Filter(Math.sqrt(a*a + b*b) % 1 == 0)

필터링하는 코드를 작성해 봤습니다.

 

집합 생성

이제 map을 이용해서 마지막 세 번째 수를 찾아 반환해보도록 하겠습니다.

우선 이런식으로 작성할 수 있습니다.

rangeClosed1-100까지의 수 생성

여기서 b라는 값을 꺼내 a, b 조합 생성

rangeClosed의 결과는 IntStream이므로 boxed를 통해 일반 Stream 반환 가능

 

a값 생성

마지막으로 a값을 생성하는 코드를 추가해 봅시다.

 

이제 최종 코드가 완성 되었습니다!

 

개선점

현재 코드에서 제곱근을 구하는 부분이 중복되고 있습니다.

이렇게 만들어진 세개의 요소 중 마지막 수는 정수여야 하므로 그 정수를 뽑아내는 filter를 추가해주면 됩니다!

 

5.8 스트림 만들기

컬렉션에서 스트림을 뽑아내는 방법 말고 실제로 스트림을 만드는 방법도 알아보겠습니다.

 

5.8.1 값으로 스트림 만들기

Stream.of를 통해 스트림을 만드는 예제 코드를 작성해 봅시다.

5.8.2 null이 될 수 있는 객체로 스트림 만들기

자바 9에서 null이 될 수 있는 개체를 스트림으로 만들 수 있는 새로운 메서드가 추가되었습니다.

 

5.8.3 배열로 스트림 만들기

Arrays.stream의 결과는 IntStream입니다.

 

5.8.4 파일로 스트림 만들기

파일을 처리하는 등의 I/O 연산에 사용하는 자바의 NIO API에도 스트림을 사용할 수 있습니다.

java.nio.file.Files의 많은 정적 메서드가 스트림을 반환합니다.

Files.lines는 주어진 파일의 행 스트림을 문자열로 반환해줍니다. 지금까지 배운 스트림을 활용해서 파일 내에 고유한 단어의 수를 찾는 프로그램을 간단하게 만들 수 있습니다.

Files.line으로 파일의 각 행 요소를 반환하는 스트림을 얻을 수 있음

스트림의 소스가 I/O 자원이므로 이 메소드를 try/catch 블록으로 감쌌고, Stream 인터페이스는 AutoCloseable 인터페이스를 구현하므로 finally 블록 사용 X try 내의 자원은 자동으로 관리됨

Line.split를 사용해서 각 단어를 스트림으로 추출

flatMap으로 스트림 평면화

distinctcount로 고유 단어의 개수 계산!

 

5.8.5 함수로 무한 스트림 만들기

스트림 API는 함수에서 스트림을 만들 수 있는 정적 메서드인 Stream.iterateStream.generate를 제공합니다. 이 두개의 메서드를 이용해서 무한 스트림 즉 크기가 고정되지 않은 스트림을 만들 수 있게 됐습니다.

Iterate, generate는 요청할 때마다 주어진 함수를 이용해서 값 생성

보통은 limit를 이용해 원하는 만큼 추출

 

Iterate 메서드

초깃값과 람다(UnaryOperation<T> 인수 사용)를 인수로 넘겨 받아 무한한 스트림 생성

Iterate는 요청할 때마다 끝이 없는 값을 생성할 수 있고 이를 무한 스트림 혹은 언 바운드(unbounded stream) 이라고 표현

Limit 메서드를 이용해서 스트림의 크기를 명시

forEach라는 최종 연산을 통해 스트림을 소비하고 개별 요소 출력

 

자바 9에서는 Predicate를 지원해줍니다.

위 코드는 Predicate를 사용해 종료 시점을 전달한 코드

아래 코드는 filter를 사용해 종료 시점을 전달한 코드입니다.

 

Filter를 사용한 코드는 안타깝게도 작업을 중단하는 시점을 알 수 있는 메서드는 아니므로 원하는 결과는 나오지 않습니다.

 

그래서 takeWhile을 이용하는 방법도 있습니다.

 

Generate 메서드

Iterate와 비슷하게 generate도 요구할 때 값을 계산하는 무한 스트림을 만들 수 있습니다.

GenerateSupplier<T>를 인수로 받음

  • Iterate와 달리 generate는 생산된 각 값을 연속적으로 계산하지 않음

예제를 보겠습니다.

Math.random은 임의의 새로운 값을 생성하는 정적 메서드

Limit를 이용한 크기 명시 limit가 없는 스트림은 언 바운드 스트림!

 

 

Generate는 병렬 코드에 사용?

Iterate가 있는데 왜 generate가 있을까요? 그건 병렬 코드 작성에서 생길 수 있는 문제 때문인 거 같습니다. Supplier는 발행자라고 표현되며 이 발행자는 상태를 저장하지 않습니다.

상태를 저장하지 않는다는 말은 계산에 필요한 데이터를 저장해 두지 않는다는 말입니다.

아직은 잘 모르지만 7장에서는 스트림을 병렬 처리하는 방법에 자세하게 배우니

상태를 저장하는 발행자가 스트림 병렬 처리에 끼치는 부작용에 대해서 알아봅시다.

스트림을 병렬로 처리하기 위해서 불변 상태 기법을 고수해야 하므로, 가변 객체와 불변 객체에 대한 개념도 다시 한번 상기하시면 좋을 것 같습니다.

 

정리

  • 스트림 API를 이용하면 복잡한 데이터 처리 질의를 표현 가능
  • filter, distinct, takeWhile(자바 9), dropWhile(자바 9), skip, limit 메서드로 스트림을 필터링, 슬라이싱 가능
  • 소스가 정렬되어 있다면 takeWhile, dropWhile 메서드를 사용해 쇼트서킷 개념 적용 가능
  • map, flatMap 메서드로 스트림의 요소 추출 혹은 변환 가능
  • findFirst, findAny 메서드로 스트림의 요소 검색
  • allMatch, noneMatch, anyMatch 메서드를 이용해 주어진 Predicate와 일치하는 요소 검색 가능
  • 스트렘에 위의 메서드를 적용해 쇼트서킷, 결과를 즉시 반환하는 개념 적용 가능
  • reduce 메서드로 스트림의 모든 요소를 반복 조합하며 값 반환 가능
  • filter,map 등은 상태를 저장하지 않는 상태 없는 연산(stateless operation)
  • Reduce 같은 연산은 계산하는데 필요한 상태를 각각 저장… sorted, distinct 등의 메서드는 새로운 스트림을 반환하기에 앞서 스트림의 모든 요소를 버퍼에 저장, 이런 메서드는 상태 있는 연산(stateful operation)이라고 불림
  • IntSream, DoubleStream, LongStream은 기본형 특화 스트림 각각의 기본형에 특화된 연산 제공
  • 스트림에 적용되는 데이터 소스는 컬렉션, 파일, 배열, iterate, generate 같은 것도 될 수 있음
  • 무한한 개수의 요소를 가진 스트림을 무한 스트림, 언 바운드 스트림이라고 불림