스트림 - 모던 자바인액션

HUSII·2023년 7월 17일
0

자바 공부

목록 보기
6/8

스트림

자바 8 API에 새로 추가된 기능

스트림을 이용하면 선언형으로 데이터를 처리할 수 있다.

선언형: 데이터를 처리하는 임시 구현 코드 대신 짏의형으로 처리하는 것

또한 스트림을 이용 하면 멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리할 수 있다.

스트림을 이용하지 않고 데이터를 처리하는 것과 스트림으로 데이터를 처리하는 것을 비교해보자


요청: 칼로리가 400보다 낮은 음식의 이름들의 리스트를 가져와라(내림차순으로 정렬)

스트림을 이용하지 않고 처리

List<Dish> lowCaloricDishes = new ArrayList<>();
for(Dish dish: menu) {
	if(dish.getCalories() < 400)
    	lowCaloricDishes.add(dish.getName());
}
Collections.sort(lowCaloricDishes, (Dish dish1, Dish dish2) -> {
	return Integer.compare(dish1.getCalories(), dish2.getCalories());
}
List<String> lowCaloricDishesName = new ArrayList<>();
for(Dish dish: lowCaloricDishes){
	lowCaloricDishesName.add(dish.getName());
}

(lowCaloricDishes는 컨테이너 역할만 하는 중간 변수이다)
스트림을 사용하지 않고 처리하는 코드를 보면, 가독성이 매우 떨어진다.

스트림으로 처리

List<String> lowCaloricDishesName =
	menu.stream()
    	.filter(dish -> dish.getCalories() < 400)
        .sorted(comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(toList());
	

스트림을 사용한 코드는 가독성이 매우 좋다
선언형으로 코드를 구현할 수 있다.

마치 SQL 질의문처럼
SELECT NAME FROM DISHES WHERE CALORIES < 400;

루프와 if 조건문 등의 제어 블록을 사용해서 어떻게 동작을 구현할지 지정할 필요가 없이 '저칼로리의 음식만 선택하라' 같은 동작의 수행을 지정할 수 있다.

filter(또는 sort, map, collec) 같은 연산은 고수준 빌딩 블록으로 이루어져 있으므로 특정 스레딩 모델에 제한되지 않고 어떤 상황에서든 자유롭게 사용할 수 있다.
(단일 스레드 모델부터 멀티코어 아키텍쳐 또한 최대한 투명하게 활용가능)


스트림의 정의

스트림이란 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소

연속된 요소: 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스를 제공

컬렉션의 주제는 데이터고, 스트림의 주제는 계산이다
컬렉션: ArrayList, LinkedList, HashMap 등 저장 방식 및 접근 연산 주로 제공
스트림: filter, map, sorted 처럼 계산식이 주

소스: 컬렉션 배열 I/O 자원 등의 제공 소스로부터 데이터를 소비한다.
(리스트로 스트림을 만들면 리스트의 순서가 그대로 유지된다)

데이터 처리 연산: 데이터베이스와 비슷한 연산을 지원한다
스트림 연산은 순차적으로 혹은 병렬로 실행할 수 있다


스트림의 특징

스트림은 두가지 중요 특징이 있다
파이프파이닝: 대부분의 스트림 연산은 스트림 연산끼리 연결해서 커다란 파이프라인을 구성할 수 있도록 스트림 자신을 반환한다. 그덕분에 lazy 연산, 쇼트서킷 같은 최적화도 얻을 수 있다.

내부 반복: 반복자를 이용해서 명시적으로 반복하는 컬렉션과 달리, 스트림은 내부 반복을 지원한다.

위의 두가지 중요 특징에 의해 3가지 장점을 얻는다.

선언형: 더 간결하고 가독성이 좋아진다
조립할 수 있음: 유연성이 좋아진다
병렬화: 성능이 좋아진다

스트림을 이용하면 컬렉션으로 처리못하는 무제한의 요소도 처리할 수 있다


컬렉션 vs 스트림 1. 구조

비유를 하면 컬렉션은 DVD이고, 스트림은 인터넷 스트리밍이다.
DVD는 영화 전체의 내용이 저장되어 있다.
반면 인터넷 스트리밍을 이용하여 영화를 시청할때는, 사용자가 시청하는 부분의 몇 프레임을 미리 내려받는다. 그러면 스트림의 다른 대부분의 값을 처리하지 않는 상태에서 미리 내려받은 프레임부터 재생할 수 있다.

데이터를 언제 계산하느냐가 컬렉션과 스트림의 가장 큰 차이다.
컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료구조
-> 모든 요소는 컬렉션에 추가하기 전에 계산되어야 한다.
스트림은 요청할 때만 요소를 계산하는 고정된 자료구조이다.
스트림은 사용자가 데이터를 요청할 때만 값을 계산한다.

스트림은 게으르지게 만들어지는 컬렉션과 같다


컬렉션 vs 스트림 2. 데이터 반복 처리 방법

컬렉션 인터페이스를 사용하려면 사용자가 직접 요소를 반복해야 한다.
이를 외부 반복이라고 한다.

명시적으로 컬렉션 항목을 하나씩 가져와서 처리한다

반면 스트림은 (반복을 알아서 처리하고 결과 스트림값을 어딘가에 저장해주는)내부 반복을 사용한다.

내부 반복을 이용하면 작업을 투명하게 병렬로 처리하거나 더 최적화된 다양한 순서로 처리할 수 있다.


스트림 연산

스트림 인터페이스의 연산을 크게 두가지로 구분할 수 있다.

1. 중간 연산
filter, map, limit 같은 연산들
중간 연산은 다른 스트림을 반환한다. -> 여러 중간 연산을 연결해서 질의를 만들 수 있다.
단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도 수행하지 않는다는 특징이 있다.
-> lazy 연산

filter 다음에 map이 바로 온다면, 한과정으로 병합된다. -> 루프 퓨전

2. 최종 연산
forEach, collect 같은 연산들
스트림 파이프라인에서 결과를 도출한다. -> 스트림 이외에 결과가 반환된다.


스트림의 이용 과정은 세가지이다.
1. 질의를 수행할 (컬렉션 같은) 데이터 소스
2. 스트림 파이프라인을 구성할 중간 연산 연결
3. 스트림 파이프라인을 실행하고 결과를 만들 최종 연산


스트림의 다양한 메서드들 - 1

filter Predicate 함수형 인터페이스(boolean을 반환하는 함수)를 인수로 받아서 이와 일치하는 모든 요소를 포함하는 스트림을 반환

distinct 고유 요소로 이루어진 스트림을 반환 (중복 제거)

고유 여부는 스트림에서 만든 객체의 hashCode, equals로 결정된다

takeWhile filter와 같이 Predicate를 인수로 받는다. 이 메서드는 Predicate가 true가 나왔을때 이전 요소를 반환한다.

아주 많은 요소를 포함하는 큰 스트림에서는 최적화가 이루어진다.
대신 정렬되었다는 가정이 있어야한다.

dropWhile takeWhile과 정반대의 작업을 수행한다. Predicate가 처음으로 거짓이 되는 지점에서 작업을 중단하고, 남은 모든 요소를 반환한다.

무한히 남은 요소를 가진 무한 스트림에서도 동작한다.

limit 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환한다.

스트림이 정렬되지 않아도 사용할 수 있다. -> 정렬되지 않은 상태로 반환됨

skip 처음 n개 요소를 제외한 스트림을 반환하는 메서드

n개 이하의 요소르 포함하는 스트림이 skip(n)을 호출하면 빈 스트림이 반환됨

map 함수를 인수로 받는 메서드. 인수로 제공된 함수는 각 요소에 적용되며, 함수를 적용한 결과가 새로운 요소로 매핑된다.

이 과정은 기존의 값을 '고친다'라는 개념보다는 '새로운 버전을 만든다'라는 개념에 가까우므로 변환에 가까운 매핑이라는 단어를 사용한다.

flatMap 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행
Stream<Stream<Integer\>>.flatMap -> Stream<Integer\>

배열은 컬렉션이 아니기때문에 stream()을 사용못한다.
대신 Arrays.stream을 이용하면 배열을 이용해서 스트림 생성이 가능하다

⚙ 기본형 래퍼클래스(Integer, Double)는 가능하지만, 기본형(int, double)은 불가능하다


스트림의 다양한 메서드들 - 2

anyMatch boolean을 반환하는 최종 연산 메서드. 스트림에서 적어도 한 요소가 일치하는지 확인한다.

allMatch boolean을 반환하는 최종 연산 메서드. 스트림에서 모든 요소가 일치하는지 확인한다.

noneMatch allMatch와 반대 연산 수행(일치하는 요소가 없는지 확인한다)

위 세 메서드는 스트림 쇼트서킷 기법, 즉 자바의 &&, ||와 같은 연산을 활용한다.
쇼트서킷: 전체 스트림을 처리하지 않았더라도 결과를 반환하는 프로세스

ex) if(a==0 && b==0)에서 a가 0이 아니라면, b의 값과 상관없이 조건문의 결과가 false이므로 b를 체크하기 전에 if문을 탈출한다.

findAny 현재 스트림에서 임의의 요소를 반환하는 최종 연산 메서드

findFirst 스트림의 첫번째 요소를 반환한다.
(일부 스트림에는 논리적인 아이템 순서가 정해져 있을 수 있다)

💡 findAny vs findFirst
병렬 실행에서 첫번째 요소를 찾는건 어렵다. 따라서 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 써야한다


스트림의 다양한 메서드들 - 3 리듀싱

리듀싱 연산: 모든 스트림 요소를 처리해서 값으로 도출하는 연산
(함수형 프로그래밍 언어 용어로는 폴드라고 부른다)

int sum = numbers.stream().reduce(0, (a,b) -> a+b);
초깃값: 0
두 요소를 조합해서 새로운 값을 만드는 예제

과정: 맨처음 a에 초깃값 0이 사용되었고, 스트림에서 numbers의 첫번째 요소를 소비해서 두번째 파라미터(b)로 사용했다. 두 파라미터를 더한 결과가 새로운 누적값이 되고 첫번째 파라미터(a)로 사용한다. 다음으로 numbers의 두번째 요소를 소비해서, 두번째 파라미터(b)로 사용한다.
위 과정을 계속 반복해서 최종 결과를 도출한다.

int sum = numbers.stream().reduce(0, Integer::sum);
위 코드처럼 깔끔하게 작성할 수 있다

reduce 메서드는 누적합 뿐만 아니라 최솟값, 최댓값을 찾을 때도 사용할 수 있다


스트림 연산: 상태 없음과 상태 있음

map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보낸다.
이 두 메서드들은 보통 상태가 없는, 즉 내부 상태를 갖지 않는 연산이다.(stateless operation)

반면 sorted나 distince 같은 연산은 이전 메서드들과는 다르게, 과거의 이력을 알고 있어야 한다. 이말은 모든 요소가 버퍼에 추가되어 있어야 한다. 이러한 연산은 내부상태를 갖는 연산이다.(stateful operation)

profile
공부하다가 생긴 궁금한 것들을 정리하는 공간

0개의 댓글