woowacourse-study / 2022-modern-java-in-action

우아한테크코스 4기 모던 자바 인 액션 스터디
10 stars 4 forks source link

스트림 연산에 있어 상태가 있거나 없다는 의미가 무엇인가? #40

Open bugoverdose opened 2 years ago

bugoverdose commented 2 years ago

문제

스트림 연산은 중간연산과 최종연산으로만 분류되는 것이 아니다. 상태와 관련하여 스트림 연산을 분류하여 보시오.

선정 배경

관련 챕터

bugoverdose commented 2 years ago

스트림 연산은 크게 중간연산(intermediate operation)최종연산(terminal operation)으로 분류되며, 스트림 파이프라인을 형성하기 위해 조합된다. 그리고 스트림 파이프라인은 소스(source; 컬랙션, 배열, 생성 함수, I/O 채널), 복수(0개 이상)의 중간연산, 하나의 최종연산으로 구성된다.

이 때 중간연산상태가 없는 연산(stateless operation)상태가 있는 연산(stateful operation)으로 구분된다.

상태가 없는 연산(stateless operation)

상태가 없는 연산(filter, map 등)의 경우, 현재 순회 중인 데이터 요소를 처리하는 동안 이전에 처리된 데이터 요소에 대한 상태를 유지하지 않는다. 즉, 각 요소에 대한 처리는 다른 요소와 독립되어 수행될 수 있으며 성능상의 이점이 크다. 상태가 없는 중간 연산만으로 구성된 파이프라인의 경우 순차 처리든, 병렬 처리든 각 데이터에 단 한번만 접근하면 되며 데이터 버퍼링도 최소화된다.

상태가 있는 연산(stateful operation)

상태가 있는 연산의 경우 새로 순회 중인 요소들을 처리할 때 이전에 순회된 요소들에 대한 상태를 활용해야 할 수 있다.

상태가 있는 연산 중에는 결과를 만들어내기 위해 데이터 소스 전체를 처리해야 하는 경우도 있다. 예를 들어 sorted()를 통한 스트림 정렬의 경우 스트림 내의 모든 요소를 필요로 한다. 이 때문에 병렬 처리 환경에서 상태가 있는 중간 연산을 포함하는 파이프라인은 같은 데이터 요소에 대해 여러 번 접근해야 하거나(require multiple passes on the data) 주요 데이터를 별도로 관리(buffer significant data)해야 할 수도 있다.

이때 상태가 있는 연산도 관리되어야 하는 상태의 크기에 따라 추가적으로 분류될 수 있다.

한정된 상태가 있는 연산(stateful but bounded)

우선 스트림을 구성하는 전체 데이터 요소의 개수와 무관하게, 수행되기 위해 관리되어야 하는 상태의 크기가 한정되는 연산이 존재한다. 예를 들어, limit, skip의 경우, 현재까지 순회된 데이터 요소의 개수 정보만을 상태로 관리하며 그 값을 1씩 증가(increment)해가면 된다. 또한 reduce, sum, max의 경우에도 각 데이터 요소에 대한 여태까지의 최종적인 작업 결과만을 상태로 관리하며 다음 데이터 요소에 대해 활용하면 된다.

즉, 한정된 상태가 있는 연산이란 스트림 내에 5개의 데이터가 있든, 1000개의 데이터가 있든, 데이터 요소의 개수와 무관하게 언제나 동일한 개수의 상태만을 필요로 하는 중간연산을 의미한다.

한정되지 않은 상태가 있는 연산(stateful and unbounded operation)

문제가 되는 것은 바로 sorted, distinct와 같이 스트림 내의 데이터 요소에 따라 관리되어야 하는 상태의 크기가 변하는 연산들이다. 스트림 내의 모든 데이터가 있어야 비로소 sorted를 통해 정렬 작업을 수행할 수 있으며, distinct를 통해 현재 데이터가 이전에 순회된 데이터와 중복되는지 확인하기 위해서는 여태까지 순회된 데이터의 목록이 관리되어야 한다. 즉 여태까지 처리된 모든 요소가 버퍼에 추가되어있어야 한다는 것이다. 그리고 이는 무한스트림과 같이 스트림 데이터의 크기가 무한인 상황에서 문제를 일으킬 수 있다. (세상에서 가장 큰 소수를 찾고 출력하기 위해 sorted를 수행하는 경우를 생각하면 쉬울 것이다.)

bugoverdose commented 2 years ago

sorted 실습

private final List<Data> dataSource = Arrays.asList(
        new Data(1, 300),
        new Data(2, 500),
        new Data(3, 100));
void statelessOnly() {
    dataSource.stream()
            .peek(data -> System.out.println(data.getId()))
            .peek(System.out::println)
            .peek(data -> System.out.println(data.getId()))
            .peek(System.out::println)
            .forEach(System.out::println);
            // 1
            // Data{id=1, number=300}
            // 1
            // Data{id=1, number=300}
            // Data{id=1, number=300}

            // 2
            // Data{id=2, number=500}
            // 2
            // Data{id=2, number=500}
            // Data{id=2, number=500}

            // 3
            // Data{id=3, number=100}
            // 3
            // Data{id=3, number=100}
            // Data{id=3, number=100}
}
void sortWithState() {
    List<Data> sortedData = dataSource.stream()
            .peek(data -> System.out.println(data.getId()))
            .peek(System.out::println)
            // 1
            // Data{id=1, number=300}
            // 2
            // Data{id=2, number=500}
            // 3
            // Data{id=3, number=100}
            .sorted(Comparator.comparing(Data::getNumber)) // 모든 전체 데이터에 대해 정렬 필요
            .peek(data -> System.out.println(data.getId()))
            .peek(System.out::println)
            // 3
            // Data{id=3, number=100}
            // 1
            // Data{id=1, number=300}
            // 2
            // Data{id=2, number=500}
            .collect(toList());

    System.out.println(sortedData);
    // [Data{id=3, number=100}, Data{id=1, number=300}, Data{id=2, number=500}]
}
class Data {

    private final int id;
    private final int number;

    public Data(int id, int number) {
        this.id = id;
        this.number = number;
    }

    public int getId() {
        return id;
    }

    public int getNumber() {
        return number;
    }

    @Override
    public String toString() {
        return "Data{" + "id=" + id + ", number=" + number + '}';
    }
}