해당 서적을 참고하여 개인 공부용으로 정리한 글입니다.
기본적인 Java 지식이 있으시다면 이해하기 수월한 정도입니다 :)
또한, 해당 책은 2018년 Java 11 기준으로 작성되어 있습니다.
따라서 이후 버전에서 변경된 내용은 수정하여 작성했습니다. (아는 부분만 말입니다..)
1. 스트림 특징
스트림을 이용하면 SQL 처럼 선언형으로 컬렉션을 처리할 수 있다.
또한, 멀티스레드를 구현하지 않아도 병렬성을 획득할 수 있다.
아래는 여러 요리 중 칼로리가 400미만인 요리명을 칼로리순으로 저장하는 코드다.
public class Chap04 {
public static void main(String[] args) {
Dish[] dishes = {
new Dish("스테이크", 700, false, Dish.Type.MEAT),
new Dish("삼겹살", 800, false, Dish.Type.MEAT),
new Dish("치킨", 1000, false, Dish.Type.MEAT),
new Dish("연어", 450, false, Dish.Type.FISH),
new Dish("밥", 300, true, Dish.Type.OTHER),
new Dish("과일", 150, true, Dish.Type.OTHER),
new Dish("피자", 550, true, Dish.Type.OTHER),
new Dish("새우", 300, true, Dish.Type.FISH)
};
List<String> lowCaloricDishesName =
Arrays.stream(dishes)
.parallel()
.filter(dish -> dish.getCalorie() < 400)
.sorted(Comparator.comparing(Dish::getCalorie))
.map(Dish::getName)
.collect(Collectors.toList());
lowCaloricDishesName.forEach(System.out::println);
}
}
class Dish {
private final String name;
private final int calorie;
private final boolean vegetarian;
private final Type type;
public Dish(String name, int calorie, boolean vegetarian, Type type) {
this.name = name;
this.calorie = calorie;
this.vegetarian = vegetarian;
this.type = type;
}
public String getName() {
return name;
}
public int getCalorie() {
return calorie;
}
public boolean isVegetarian() {
return vegetarian;
}
public Type getType() {
return type;
}
public enum Type {MEAT, FISH, OTHER}
}
이를 통해 다음과 같은 특징이 있다는 것을 알 수 있다.
- 선언형 코드를 구현할 수 있다. 어떻게 동작을 수행할지 구현하는 것이 아니라 '~~해라' 형태로 동작을 지정해준다.
- 동작 파라미터화 함께 사용하면 요구사항에 유연하게 대처할 수 있다.
- filter, sorted, map 같은 여러 빌딩 블록 연산을 연결해 복잡한 데이터 처리 파이프라인을 만들 수 있다.
- 가독성과 명확성이 유지된다.
- 스레딩 모델에 제한되지 않고 병렬화할 수 있다.
병렬 처리를 했을 때 스레드가 얼마나 사용되고, 얼마나 성능이 향상되는지는 Ch.7 에서 다룬다.
2. 스트림 시작하기
스트림 : 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소(Sequence of Elements)
컬렉션은 자료구조이기 때문에 데이터를 중점으로 시간 및 공간 복잡도와 관련된 요소 저장 및 접근 연산이 주를 이룬다.
스트림은 표현 계산식이 중점이며, 제공 소스로부터 데이터를 소비하며 순차적 또는 병렬적으로 실행한다.
대부분의 스트림 연산(중간 처리 메서드)은 다음 스트림 연산과 연결되기 위해 스트림을 반환한다.
즉, 스트림과 스트림을 연결해 파이프라인을 구성한다.
또한, Iterator 등을 이용해 명시적으로 반복하는 컬렉션과 달리 내부에서 반복되어 수행한다.
칼로리 상위 3개의 요리명을 구하려면 다음과 같이 파이프라인을 구성한다.
collect 나 forEach 같은 최종 처리 메서드로 결과를 획득한다.
private static void topThreeCaloricDishNames(List<Dish> menu) {
List<String> result = menu.stream()
.filter(dish -> dish.getCalorie() > 400)
.sorted(Comparator.comparing(Dish::getCalorie))
.map(Dish::getName)
.limit(3) // 최초 3개만
.collect(Collectors.toList());
result.forEach(System.out::println);
}
3. 스트림과 컬렉션
컬렉션과 스트림의 가장 큰 차이는 '언제 계산하는가' 이다.
컬렉션의 모든 요소는 컬렉션에 추가하기 전에 계산되어야 한다. 또한, 모든 값이 메모리에 저장된다.
반면 스트림은 이론적으로 요청할 때만 요소를 계산한다.
즉, 생산자와 소비자 관계를 형성하며 사용자가 요청한 값만 스트림에서 추출한다.
DVD와 스트리밍의 차이와 동일하다.
한번 탐색된 스트림의 요소는 소비된다.
따라서 다시 탐색하려면 초기 데이터 소스에서 새로운 스트림을 만들어야한다.
또한, 컬렉션과 달리 스트림은 내부 반복을 지원한다.
외부 반복은 병렬성을 스스로 관리해야 한다. 우리가 직접 명시적으로 선언해주기 때문이다.
내부 반복을 이용하면 반복 과정을 우리가 신경 쓰지 않아도 된다.
단, 반복을 숨겨주는 연산이 미리 정의되어야 한다. 이러한 연산들은 대부분 람다식을 인수로 받는다.
4. 스트림 연산
위에서도 언급했듯이 스트림 연산은 중간 연산(filter, map, sorted, limit 등)과 최종 연산(collect, forEach 등)으로 나뉜다.
중간 연산의 가장 큰 특징은 최종 연산을 스트림 파이프라인에 실행하기 전까지는 아무런 연산을 하지 않는다.(lazy)
최종 연산이 들어와야 중간 연산들을 합쳐 한번에 처리하기 때문이다.
limit은 Short Curcuit 기법(Ch.5)이 적용되며, filter와 map은 다른 연산이지만 병합되는 Loop Fusion 기법이 적용된다.
마치 빌더 패턴(Builder Pattern)과 비슷하다.
5. 결론
- 스트림을 통해 선언형 연산을 수행할 수 있다.
- 내부 반복을 통해 반복과정을 신경 쓸 필요가 없다. 병렬처리도 같이 관리된다.
- 사용자가 요청한 값만 사용한다.
- 요소를 소비하기 때문에 한번 소비된 스트림을 다시 사용할 수 없다.
- 빌더 패턴처럼 중간 연산들을 설정해놓고 최종 연산에서 한번에 수행한다. (lazy)
'Language > Java' 카테고리의 다른 글
[모던 자바 인 액션] Ch.6 - 데이터 수집 (0) | 2022.06.22 |
---|---|
[모던 자바 인 액션] Ch.5 - 스트림 활용 (0) | 2022.06.16 |
[모던 자바 인 액션] Ch.3 - 람다 표현식 (0) | 2022.06.13 |
[모던 자바 인 액션] Ch.2 - 동작 파라미터화 (0) | 2022.06.08 |
[모던 자바 인 액션] Ch.1 - Java의 변화 (0) | 2022.06.07 |
댓글