Book/모던 자바 인 액션

[모던 자바 인 액션] Chapter 04. 스트림 소개

ghan2 2024. 10. 1. 20:50
이 장의 내용
💡 스트림이란 무엇인가?
💡 컬렉션과 스트림
💡 내부 반복과 외부 반복
💡 중간 연산과 최종 연산

 

4.1 스트림이란 무엇인가?

스트림은 자바 8 API에 새로 추가된 기능이다. 스트림을 이용하면 선언형(즉, 질의로 표현 가능하다)으로 컬렉션 데이터를 처리할 수 있다. 또한 스트림을 이용하면 멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리할 수 있다. 

  • 기존 코드
List<Dish> lowCaloricDishes = new ArrayList<>();

for(Dish dish : menu) {
	if(dish.getCalories() < 400) {
    	lowCaloricDishes.add(dish);
    }
}

Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
	public int compare(Dish dish1, Dish dish2) {
    	return Integer.compare(dish1.getCalories(), dish2.getCalories());
    }
});

List<String> lowCaloricDishesName = new ArrayList<>();

for(Dish dish : lowCaloricDishes) {
	lowCaloricDishesName.add(dish.getName());
}

 

  • 최신 코드(자바 8)
List<String> lowCaloricDishesName 
						= menu.stream()
			  				  .filter(d -> d.getCalories() < 400)
                              .sorted(comparing(Dish::getCalories))
                              .map(Dish::getName)
                              .collect(toList));
                              
// stream()을 parallelStream()으로 바꾸면 이 코드에서 멀티코어 아키텍처에서 병렬로 실행할 수 있다. 
// 그러나 이 내용은 7장에서 자세히 설명한다. 
List<String> lowCaloricDishesName 
						= menu.parallelStream()
			  				  .filter(d -> d.getCalories() < 400)
                              .sorted(comparing(Dish::getCalories))
                              .map(Dish::getName)
                              .collect(toList));

 

4.2 스트림 시작하기

스트림이란 '데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소'라고 정의할 수 있다. 

또한 스트림에는 다음과 같은 두 가지 중요 특징이 있다. 

  • 파이프라이닝(Pipelining): 대부분의 스트림 연산은 스트림 연산끼리 연결해서 커다란 파이프라인을 구성할 수 있도록 스트림 자신을 반환한다. --> 게으름(laziness), 쇼트서킷(short-circuiting)같은 최적화 가능 (5장에서 다룸)
  • 내부 반복: 반복자를 이용해서 명시적으로 반복하는 컬렉션과 달리 스트림은 내부 반복을 지원한다. 

 

4.3 스트림과 컬렉션

4.3.1 딱 한 번만 탐색할 수 있다. 

반복자(iterator)와 마찬가지로 스트림도 한 번만 탐색할 수 있다. 즉, 한 번 탐색한 후에 다시 요소를 탐색하려면 초기 데이터 소스에서 새로운 스트림을 만들어야 한다. 

 

4.3.2 외부 반복과 내부 반복

외부 반복: 컬렉션 인터페이스를 사용해서 사용자가 직접 요소를 반복

내부 반복: 스트림 라이브러리는 반복을 알아서 처리하고 결과 스트림값을 어딘가에 저장해주는 반복

 

내부 반복은 외부 반복과 어떤 점이 다르며 어떤 이득을 줄까?

내부 반복을 이용하면 작업을 투명하게 병렬로 처리하거나 더 최적화된 다양한 순서로 처리할 수 있지만 기존 자바의 컬렉션 외부 반복으로는 이와 같은 최적화를 달성하기 어렵다. 내부 반복의 이점을 누리려면 (filter나 map같이) 반복을 숨겨주는 연산 리스트가 미리 정의되어 있어야 한다. 

 

내부 반복의 이점 뿐 아니라 스트림을 제공하는 더 다양한 이유가 있다. 스트림 라이브러리의 내부 반복은 데이터 표현과 하드웨어를 활용한 병렬성 구현을 자동으로 선택한다. (반면 for-each)를 이용하는 외부반복에서는 병렬성을 스스로 관리해야 한다.)

 

4.4 스트림 연산

4.4.1 중간 연산

연결할 수 있는 스트림 연산을 중간 연산이라고 한다. 중간 연산의 중요한 특징은 단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도 수행하지 않는다는 것, 즉 게으르다는 것이다. 중간 연산을 합친 다음에 합쳐진 중간 연산을 최종 연산으로 한 번에 처리하기 때문이다. -> 게으른 연산, 여러 스트림을 한 줄씩 처리해서 필요한 연산만 한다는 말 같다.

 

4.4.2 최종 연산

최종 연산은 스트림 파이프라인에서 결과를 도출한다. 보통 List, Integer, void 등 스트림 이외의 결과가 반환된다. 

 

4.4.3 스트림 이용하기

스트림 이용 과정은 다음과 같이 세 가지로 요약할 수 있다. 

1. 질의를 수행할 (컬렉션 같은) 데이터 소스

2. 스트림 파이프라인을 구성할 중간 연산 연결

3. 스트림 파이프라인을 실행하고 결과를 만들 최종 연산

 

4.6 마치며

  • 스트림은 소스에서 추출된 연속 요소로, 데이터 처리 연산을 지원한다.
  • 스트림은 내부 반복을 지원한다. 내부 반복은 filter, map, sorted 등의 연산으로 반복을 추상화한다. 
  • 스트림에는 중간 연산과 최종 연산이 있다. 
  • 중간 연산은 filter와 map처럼 스트림을 반환하면서 다른 연산과 연결되는 연산이다. 중간 연산을 이용해서 파이프라인을 구성할 수 있지만 중간 연산으로는 어떤 결과도 생성할 수 없다. 
  • forEach나 count처럼 스트림 파이프라인을 처리해서 스트림이 아닌 결과를 반환하는 연산을 최종 연산이라고 한다. 
  • 스트림의 요소는 요청할 때 게으르게 계산된다.