모던자바인액션 - 4

이건희·2023년 7월 7일
3

모던자바인액션

목록 보기
4/9

4장에서는 우리가 많이 들어 보았지만 정확하게 알지 못하는 Java Stream(스트림)에 대해 간단히 소개한다.

스트림이란?

스트림은 다음과 같은 특징들을 가진다.

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

한번 예시를 보며 위 특징들을 이해해보자.

예제 - 칼로리 기준 정렬, 요리명 반환

List<Dish> lowCarloricDishes = new ArrayList<>();
for(Dish dish: menu) {
	if(dish.getCalories() < 400) {
    	lowCaloricDishes.add(dish);
    } //400 칼로리 이하의 메뉴를 추가
}

Collections.sort(low CaloricDishes, 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());
} //정렬된 리스트를 처리하며 요리 이름 선택

위의 코드는 Java 8 이후, 스트림을 사용해 다음과 같이 구현할 수 있다.

List<String> lowCaloricDishesName =
			menu.stream() //스트림 생성
            	.filter(d -> d.getCalories() < 400) //400Cal 이하 선택
                .sorted(comparing(Dish::getCalories)) //정렬
                .map(Dish::getName) //요리명 추출
                .collect(toList()); //모든 요리명 리스트 저장

stream()을 parallelStream()으로 바꾸면 이 코드를 멀티코어 아키텍처에서 병렬로 실행할 수 있다. 책에서는 이로 인해 얻는 이점을 후에 설명한다고 한다.

  • if, 루프 등 제어블록 사용 없이, '저칼로리의 요리만 선택하라'와 같이 선언형으로 코드를 구현할 수 있다.

  • 선언형 코드는 동작 파라미터화와 함께 변하는 요구사항에 쉽게 대응할 수 있다.

  • filter, sorted, map, collect 같은 여러 연산을 파이프라인으로 연결해도 가독성과 명확성이 유지된다.

  • filter 같은 연산은 자유롭게 어떤 상황에서든 사용 가능하고, 이들은 멀티코어 아키텍처를 최대한 투명하게 활용할 수 있도록 구현 되어 있어 스레드와 락을 걱정할 필요가 없다.

그래서 스트림이란 무엇일까? 한마디로 정의하면,

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

이 역시 말이 너무 어렵다. 예제로 알아보자.

예제

List<String> threeHighCaloricDishNames = 
                menu.stream() //메뉴에서 스트림을 얻는다.
                    .filter(dish -> dish.getCalories() > 300) 
                    .map(Dish::getName) //요리명 추출
                    .limit(3) //선착순 3개만 선택
                    .collect(toList()); //스트림을 리스트로 반환
  1. 요리 리스트를 포함하는 menu(소스)에서 스트림을 얻기
    • 데이터 소스는 요리 리스트(메뉴)이다 !
    • 데이터 소스는 연속된 요소를 스트림에 제공
  1. filter, map 등으로 이루어지는 데이터 처리 연산 적용
    • collect를 제외한 모든 연산은 서로 파이프라인 형성하도록 스트림 반환
  1. collect 연산으로 파이프라인을 처리해 결과 반환
    • collect는 스트림이 아닌 List 반환
    • collect 호출 전까지 menu에서 무엇도 선택되지 않으며 출력 결과도 없다.
    • collect 호출 전까지 메서드 호출이 저장되는 효과

예제와 같이 스트림은 스트림 연산끼리 연결한 파이프라인을 구성할 수 있도록 스트림 자신을 반환한다.


스트림과 컬렉션

기존 컬렉션과 스트림 모두 연속된 요소 형식의 값을 저장하는 인터페이스를 제공한다.

  • 연속된 : 순서와 상관없이 아무 값에 접근이 아닌, 순차적으로 값에 접근

그러면 차이점에는 무엇이 있을까?

1. 데이터 계산 시점

만약 DVD에 어떤 영화가 저장되어 있다고 하자. DVD에 전체 자료구조가 저장되어 있으므로 DVD도 컬렉션이다. DVD를 시청하기 위해선, 영상의 모든 바이트가 DVD에 저장되어 있어야 한다.

하지만 DVD가 아닌 인터넷 스트리밍 같은 비디오를 시청한다 하자. 인터넷 스트리밍은 전체를 한번에 받는게 아닌 시청하는 부분의 몇 프레임만 받는다. 그러면 다른 대부분의 값을 처리하지 않은 상태에서 미리 내려받은 프레임부터 재생할 수 있다.

위 예와 같이 DVD는 컬렉션이고 스트리밍은 스트림이다.

컬렉션은

  • 컬렉션은 모든 값을 메모리에 저장하는 자료구조
  • 즉, 모든 요소는 컬렉션에 추가하기 전에 계산되어야 한다.

스트림은

  • 요청할때만 요소를 계산하는 고정된 자료구조
  • 사용자가 요청하는 값만 스트림에서 추출

2. 데이터 반복 처리 방법 - 내부반복, 외부반복

컬렉션은 사용자가 직접 요소를 반복해야한다. 이를 외부 반복이라고 하는데, 외부에 반복하는 부분이 명시적으로 보인다. 예시로, for-each문, iterator가 외부 반복이다.

하지만 컬렉션은 반복을 알아서 처리하고 결과 스트림값을 어디간에 저장해주는 내부 반복을 사용한다. 따라서 함수에 어떤 작업을 수행할지만 지정해주면 된다.

이렇게 내부 반복을 하면 무엇이 좋을까?

  1. 작업을 투명하게 병렬로 처리하거나 더 최적화된 다양한 순서로 처리 가능
  2. 데이터 표현과 하드웨어를 활용한 병렬성 구현을 자동으로 선택
    • 외부 반복에서는 synchronized 등을 이용해 병렬성을 스스로 관리 해야함

스트림 연산

한번만 사용 가능

우선 스트림은 반복자와 마찬가지로 한 번만 탐색할 수 있다. 즉, 탐색된 스트림 요소는 소비된다.

아래 예시를 보자

List<String> title = Arrays.asList("java 8", "in", "Action");
Stream<String> s = title.stream();
s.forEach(System.out::println); //for-each 사용하여 스트림 소비
s.forEach(System.out::println); //오류 : 스트림이 이미 소비됨

세번째 라인에서 스트림이 이미 소비 되었으므로 네번째 줄은 오류를 발생시킨다. 이와 같이 스트림은 한번만 탐색할 수 있고, 다시 탐색하려면 초기 데이터 소스에서 새로운 스트림을 생성해야 한다.

중간 연산과 최종 연산

예시를 다시 보자.

List<String> threeHighCaloricDishNames = 
                menu.stream() 
                    .filter(dish -> dish.getCalories() > 300) 
                    .map(Dish::getName) 
                    .limit(3) 
                    .collect(toList()); 
  • filter, map, limit은 서로 연결되어 파이프라인 형성 - 중간 연산
  • collect로 파이프라인 실행 후 스트림 닫기 - 최종 연산

중간 연산

  • 연결할 수 있는 스트림 연산
  • 스트림을 반환
  • 모든 중간 연산을 합친 다음, 한번에 최종 연산으로 처리
  • 중간 연산으로 어떤 결과도 생성할 수 없다.

최종 연산

  • 스트림 파이프라인에서 결과 도출
  • 최종 연산에 의해 List, Integer, void 등 스트림 이외의 결과 반환
  • 스트림을 닫는 연산

이후 챕터들이 모두 스트림 같다. 4장에서는 간단하게 스트림이 어떤 것인지 알아보았고 이후 더 세부적으로 설명할 것인가보다.

profile
백엔드 개발자가 되겠어요

0개의 댓글