언어(Language)/Java

[Java] 자바 스트림(Stream) 개념 정리 및 활용

잇트루 2022. 9. 26. 00:00
반응형

스트림이란? (Stream)

스트림(Stream)은 다양한 데이터 소스(배열, 컬렉션)를 표준화하여 다루는 방식으로 통합된 방식으로 데이터 핸들링이 가능하다.

배열, 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자이다. 스트림을 사용하면 List, Set, Map 등 다양한 데이터 소스로부터 스트림을 만들 수 있고, 이를 표준화된 방법으로 다룰 수 있다.

즉, 스트림은 데이터 소스를 다루는 풍부한 메서드를 제공한다.

 

스트림의 특징

1. 선언형으로 데이터 소스를 처리한다

스트림을 이용하면, 선언형으로 데이터 소스를 처리할 수 있다.

 

선언형 프로그래밍이란 “어떻게” 수행할지 보다 “무엇을” 수행할 지에 관심을 두는 프로그래밍 패러다임이다. 명령형 프로그래밍 방식에서는 절차적으로 무엇을 어떻게 수행해야 하는 지를 다룬다.

 

선언형 프로그래밍 방식은 코드를 작성하면 내부 동작의 원리를 모르더라도 코드가 무슨 일을 하는지는 이해할 수 있다. “어떻게” 수행하는지에 대한 영역은 추상화되어 있다.

 

다음은 List 컬렉션을 이용하여 1부터 10까지의 수 중 짝수의 합을 구하는 예제이다.

자바7 이전까지는 List 컬렉션에서 요소를 순차적으로 처리하기 위해 다음과 같이 사용했다.

import java.util.List;

public class ListEx {
    public static void main(String[] args) {
        List<Integer> lst = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        int result = 0;

        for (int i : lst) {
            if (i % 2 == 0) {
                result += i;
            }
        }

        System.out.println(result);
    }
}

 

위 코드를 Stream을 사용하여 작성하면 다음과 같다.

import java.util.List;

public class ListEx {
    public static void main(String[] args) {
        List<Integer> lst = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        int result = lst.stream()
                .filter(i -> (i % 2 == 0))
                .mapToInt(i -> i)
                .sum();

        System.out.println(result);
    }
}

 

2. 람다식으로 요소 처리 코드를 제공한다.

Stream이 제공하는 대부분의 요소 처리 메서드는 함수형 인터페이스 매개 타입을 가진다.

따라서 람다식 또는 메서드 레퍼런스를 이용해서 요소 처리 내용을 매개값으로 전달할 수 있다.

 

다음은 스트림을 이용하여 컬렉션에 저장된 데이터를 사용하여 이름과 나이를 출력하는 예제이다.

public class StreamEx {
    private String name;
    private int age;

    public StreamEx(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

    public class StreamLambdaEx {
    public static void main(String[] args) {
        List<StreamEx> list = Arrays.asList(
                new StreamEx("홍길동", 20),
                new StreamEx("임꺽정", 30)
        );

        Stream<StreamEx> stream = list.stream();
        stream.forEach(i -> {
            String name = i.getName();
            int age = i.getAge();
            System.out.println(name + " " + age);
        });
    }
}

 

3. 내부 반복자(Internal Iterator)를 사용함으로 병렬 처리가 쉽다.

외부 반복자(External iterator)는 개발자가 코드로 직접 컬렉션의 요소를 반복하여 데이터를 가져오는 패턴이다. 즉, 인덱스를 사용하는 for문과 iterator 사용하는 while문 등의 경우 외부 반복자를 이용하는 것이다.

 

내부 반복자(internal Iterator)는 컬렉션 내부에서 요소들을 반복시키고 개발자는 요소당 처리해야 할 코드만 제공하는 코드 패턴이다.

내부 반복자를 사용하면, 내부에서 어떻게 반복시킬 것인지는 컬렉션에게 맡기고, 요소 처리 코드에만 집중할 수 있는 장점이 있다.

 

따라서 내부 반복자는 반복 순서를 변경하거나, 멀티 코어 CPU를 최대한 활용하여 요소들을 분배시킬 수 있다. 즉, 병렬 작업을 할 수 있는 것이다.

병렬 스트림을 사용하기 위해서는 parallel() 메서드를 사용한다.

 

스트림을 사용하여 1부터 10까지 출력하는 예제이다.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class ParallelEx {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Stream<Integer> stream = list.stream();

        stream.forEach(i -> {
            System.out.println(i);
        });
    }
}
// 출력
1
2
3
4
5
6
7
8
9
10

 

위 코드를 parallel() 메서드를 활용하여 병렬 스트림으로 구현하면 다음과 같다.

또한, 병렬 처리는 출력 순서가 실행할 때마다 다르게 나타난다.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class ParallelEx {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Stream<Integer> stream = list.stream();

        stream.parallel().forEach(i -> {
            System.out.println(i);
        });
    }
}
// 출력
7
6
9
3
10
2
8
4
1
5

 

만약, 병렬로 동작하는 스트림 객체를 반환하고자 한다면, Collection.parallelStream() 메서드를 활용한다. 이는 병렬 스트림을 생성하여 parallel() 메서드를 사용하지 않더라도 스트림 객체 자체가 병렬로 동작하도록 할 수 있다.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class ParallelEx {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Stream<Integer> stream = list.parallelStream();

        stream.forEach(i -> {
            System.out.println(i);
        });
    }
}

 

4. 중간 연산과 최종 연산을 수행할 수 있다.

스트림은 컬렉션의 요소에 대해 중간 연산과 최종 연산을 수행할 수 있다.

중간 연산에서는 매핑, 필터링, 정렬 등을 수행하며, 최종 연산에서는 반복, 카운팅, 평균, 총합 등의 집계를 수행한다.

이러한 연산 구성을 파이프라인 구성이라 하며 (.)을 이용하여 구성할 수 있다.

import java.util.List;

public class ListEx {
    public static void main(String[] args) {
        List<Integer> lst = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        int result = lst.stream()
                .filter(i -> (i % 2 == 0)) // 중간 연산
                .mapToInt(i -> i) // 중간 연산
                .sum(); // 최종 연산

        System.out.println(result);
    }
}

 

스트림 사용 시 주의할 점

스트림은 데이터 소스로부터 읽기만 한다. (변경하지 않음)

스트림은 일회용으로 한 번 사용하면 닫힌다. 따라서 새로운 스트림을 만들어야 한다.

반응형