4장

1. 스트림 API 알아보기

스트림을 이용하면 선언형으로 컬렉션 데이터를 병렬 처리할 수 있다. 다음은 자바 7로 구현한 기존 코드이다.

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

기존 코드는 lowCaloricDishes라는 '가비지 변수'를 사용했다. lowCaloricDishes는 컨테이너 역할만 하는 중간 변수다. 자바 8은 세부 구현을 전부 라이브버리 내부에서 처리한다. 다음은 자바 8로 구현한 코드이다.

import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;

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

위 코드에서 stream()을 parallelStream()으로 바꾸면 멀티코어 아키텍처에서 병렬로 실행할 수 있다.

import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;

List<String> lowCaloricDishesName = 
    menu.parallelStream() // 병렬 실행
        .filter(d -> d.getCalories() < 400)
        .sorted(comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(toList());

스트림은 다양한 이점이 있다. 먼저 선언형 코드와 동작 파라미터화를 활용해서 변하는 요구사항에 쉽게 대응할 수 있다. 또한 filter, sorted, map, collected 같은 빌딩 블록 연산을 연결해서 복잡하나 가독성과 명확성이 높은 데이터 처리 파이프라인을 만들 수 있다. filter 같은 고수준 빌딩 블록(high-level building block)은 특정 스레딩 모델에 제한되지 않고 자유롭게 사용할 수 있다. 스트림 API를 활용하면 데이터 처리 과정을 병렬화하면서도 스레드와 락을 걱정할 필요가 없다. 스트림 API 특징을 요약하면 다음과 같다.

  • 선언형: 더 간결하고 가독성이 좋아짐

  • 조립 가능: 유연성이 좋아짐

  • 병렬화: 성능이 좋아짐

앞으로 사용할 예제의 기본 클래스는 다음과 같다.

@AllArgsConstructor // 모든 필드 깂을 파라미터로 받는 생성자 생성
@Getter @Setter // getter, setter 생성
public class Dish {
    private final String name;
    private final boolean vegetarian;
    private final int calories;
    private final Type type;
}

2. 스트림의 정의

스트림(stream)은 데이터를 조작하고 처리하기 위해 소스에서 임시로 추출한 원소라고 할 수 있다.

  • 데이터 조작 및 처리(데이터 처리 연산): 스트림은 filter, map 등 표현 계산식으로 데이터를 조작한다. 스트림 연산은 순차 또는 병렬로 실행할 수 있다.

  • 소스: 스트림은 컬렉션, 배열 등 데이터 제공 소스로부터 데이터를 소비한다. 정렬된 컬렉션으로 스트림을 생성하면 정렬이 그대로 유지된다.

  • 원소(연속된 요소): 스트림은 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스를 제공한다.

스트림은 두 가지 중요 특징이 있다.

  • 파이프라이닝(Pipelining): 대부분의 스트림 연산은 스트림 자신을 반환한다. 따라서 스트림 연산을 연결해서 커다란 파이프라인을 만들 수 있다.

  • 내부 반복: 스트림은 내부 반복을 지원한다.

예제를 통해 확인해보자.

import static java.util.stream.Collectors.toList;

List<String> threeHighCaloricDishNames =
    menu.stream() // 데이터 소스 : 요리 리스트(메뉴)
        .filter(dish -> dish.getCalories() > 300) // 데이터 처리 연산
        .map(Dish::getName) // 데이터 처리 연산
        .limit(3) // 데이터 처리 연산
        .collect(toList()); // 데이터 처리 연산
System.out.println(threeHighCaloricDishNames); // [pork, beef, chicken] 출력

3. 스트림과 컬렉션

컬렉션
스트림

공통점

연속된 요소 형식의 값을 저장하는 자료구조의 인터페이스 제공 * 연속된(sequenced): 순차적으로 값에 접근

연산

시공간의 복잡성과 관련된 요소 저장/접근 연산 ex. ArrayList, LinkedList

표현 계산식 연산 ex. filter, sorted, map

주제

데이터

계산

반복

외부 반복

내부 반복

계산 시기

현재 자료구조에 포함된 모든 값을 메모리에 저장 모든 요소는 컬렉션에 추가/삭제하기 전 계산

이론적으로 사용자가 요청할 때만 요소를 계산

생성 주체

생산자 중심(supplier-driven)

요청 중심(demand-driven)

3.1. 딱 한 번만 탐색 가능

반복자와 마찬가지로 스트림도 한 번만 탐색할 수 있다. 탐색된 스트림의 요소는 소비된다. 한 번 탐색한 요소를 다시 탐색하려면 초기 데이터 소스에서 새로운 스트림을 만들어야 한다. 아래와 같은 코드는 이미 소비된 스트림에 접근하므로 java.lang.IllegalStateException을 일으킨다.

List<String> title = Arrays.asList("Java8", "In", "Action");
Stream<String> s = title.stream();
s.forEach(System.out::println); // title의 각 단어 출력
s.forEach(System.out::println); // java.lang.IllegalStateException 출력

3.2. 외부 반복과 내부 반복

  • 외부 반복(external iteration): 사용자가 직접 요소를 반복 처리

  • 내부 반복(internal iteration)은 라이브러리가 반복을 알아서 처리

외부 반복과 비교해서 내부 반복이 좋은 점은 무엇일까? 다음 예제를 보고 생각해보자.

// 반복자를 활용한 외부 반복
List<String> names = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()) {
    Dish dish = iterator.next();
    names.add(dish.getName());
}

// for-each 루프를 활용한 외부 반복
List<String> names = new ArrayList<>();
for (Dish dish : menu) {
    names.add(dish.getName());
}

// 스트림 : 내부 반복
List<String> names = menu.stream()
                        .map(Dish::getName)
                        .collect(toList());

컬렉션을 외부 반복으로 처리할 경우 사용자가 직접 반복을 처리하므로 최적화를 달성하기 어렵다. 내부 반복을 사용할 경우 데이터 표현과 하드웨어를 활용한 병렬성 구현을 자동으로 산택하고 최적화한다.

4. 스트림 연산

java.util.stream.Stream 인터페이스는 많은 연산을 정의한다. 스트림 인터페이스의 연산은 크게 두 가지로 구분할 수 있다.

  • 중간 연산(intermediate operation): 연결할 수 있는 스트림 연산.

  • 최종 연산(external operation): 스트림을 닫는 연산.

4.1. 중간 연산과 최종 연산

중간 연산은 스트림을 반환하므로 여러 중간 연산을 연결해서 질의를 만들 수 있다. 중간 연산은 단말 연산을 스트림 파이프라인에서 실행하기 전 아무 연산도 수행하지 않는다. 다시 말해서, 스트림 파이프라인을 구성할 뿐 스트림의 원소를 소비하지 않는다. 반면에 최종 연산은 스트림의 원소를 소비하며 스트림 파이프라인을 최적화해서 최종 결과를 도출한다. 다음은 중간 연산과 최종 연산을 확인할 수 있는 예제 코드다.

List<String> names =
    menu.stream()
        .filter(dish -> {
            System.out.println("filtering: " + dish.getName());
            return dish.getCalories() > 300;
        })
        .map(dish -> {
            System.out.println("filtering: " + dish.getName());
            return dish.getName();
        })
        .limit(3)
        .collect(toList());
System.out.println(names);

프로그램의 실행 결과는 다음과 같다.

filtering: pork
mapping: pork
filtering: beef
mapping: beef
filtering: chicken
mapping: chicken
[pork, beef, chicken]

먼저 중간 연산을 살펴보자. limit 연산과 쇼트 서킷이라 불리는 기법이 사용되어 요리가 처음 3개만 선택되었다. 또한 루프 퓨전(loop fusion) 기법으로 서로 다른 연산인 filter와 map이 한 과정으로 병합되었다. 최종 연산은 스트림 파이프라인에서 결과를 도출한다. collect(toList())는 스트림을 리듀스해서 리스트 형식의 컬렉션을 만든다.

4.2. 스트림 이용하기

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

  • 질의를 수행할 데이터 소스

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

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

스트림 파이프라인은 빌더 패턴(builder pattern)과 비슷하다. 빌더 패턴이 호출을 연결해서 설정을 만들고 build 메서드를 호출하듯이 스트림 파이프라인도 데이터 소스에 중간 연산을 연결하고 최종 연산을 호출한다.

형식
연산
반환 형식
연산의 인수
함수 디스크립터

중간 연산

filter

Stream<T>

Predicate<T>

T -> boolean

map

Stream<R>

Predicate<T, R>

T -> R

limit

Stream<T>

sorted

Stream<T>

Comparator<T>

(T,T) -> int

distinct

Stream<T>

최종 연산

forEach

void

스트림의 각 요소를 소비하면서 람다 적용

count

long(generic)

스트림의 요소 개수 반환

collect

스트림을 reduce해서 리스트, 맵, 정수 형식 컬렉션 생성

참고 자료

  • 모던 자바 인 액션 - 한빛미디어

Last updated