Java Stream API를 배우는 방법 [+5 Resources]
자바 스트림은 데이터 요소들의 연속적인 흐름으로, 순차적 또는 병렬적으로 작업을 처리할 수 있게 해줍니다.
스트림은 여러 단계의 중간 연산을 거친 후, 최종적으로 결과를 반환하는 종단 연산을 수행합니다.
스트림이란 무엇인가?
자바 8에 도입된 스트림 API를 통해 스트림을 효과적으로 관리할 수 있습니다.
제조 공정을 예로 들어보겠습니다. 상품들은 제조, 분류, 포장 단계를 거쳐 출하됩니다. 여기서 자바에서는 상품이 객체 또는 객체 컬렉션에 해당하고, 작업은 제조, 분류, 포장이며, 전체 파이프라인이 스트림입니다.
스트림은 다음과 같은 요소로 구성됩니다:
- 초기 입력 데이터
- 중간 연산
- 종단 연산
- 최종 결과
자바 스트림의 몇 가지 특징을 살펴보겠습니다.
- 스트림은 메모리에 데이터를 저장하는 구조가 아닙니다. 대신 배열, 객체, 특정 메서드로 처리되는 객체 컬렉션의 순서입니다.
- 스트림은 선언적 방식으로 작동합니다. 즉, 무엇을 해야 하는지는 명시하지만, 어떻게 해야 하는지는 지정하지 않습니다.
- 스트림은 한 번만 사용할 수 있으며, 사용 후에는 소멸됩니다.
- 스트림 연산은 원본 데이터 구조를 변경하지 않습니다. 새로운 구조를 생성하여 반환합니다.
- 스트림 파이프라인의 마지막 메서드에서 최종 결과를 반환합니다.
스트림 API와 컬렉션 처리 비교
컬렉션은 데이터를 저장하고 관리하는 메모리 내 데이터 구조입니다. 컬렉션은 데이터를 저장하기 위해 Set, Map, List와 같은 다양한 구조를 제공합니다. 반면 스트림은 데이터를 파이프라인을 통해 처리하고 전달하는 데 초점을 맞춥니다.
ArrayList 컬렉션의 예시를 보겠습니다.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(0, 3);
System.out.println(list);
}
}
Output:
[3]
위 예시에서 볼 수 있듯이 ArrayList 컬렉션을 생성하고 데이터를 추가한 후, 다양한 메서드를 사용하여 해당 데이터를 조작할 수 있습니다.
스트림을 사용하면 기존 데이터 구조를 기반으로 연산을 수행하고 변환된 새로운 값을 얻을 수 있습니다. 다음은 ArrayList 컬렉션을 생성하고 스트림을 사용하여 필터링하는 예시입니다.
import java.util.ArrayList;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList();
for (int i = 0; i < 20; i++) {
list.add(i+1);
}
System.out.println(list);
Stream<Integer> filtered = list.stream().filter(num -> num > 10);
filtered.forEach(num -> System.out.println(num + " "));
}
}
#Output
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
11
12
13
14
15
16
17
18
19
20
위의 예시에서 스트림은 기존 목록을 사용하여 생성되며, 10보다 큰 값을 필터링하기 위해 목록을 순회합니다. 스트림은 데이터를 저장하지 않고 목록이 순회되고 결과가 출력됩니다. 스트림 자체를 출력하려고 하면 값 대신 스트림 객체에 대한 참조를 얻게 됩니다.
자바 스트림 API 활용
자바 스트림 API는 요소 컬렉션 또는 시퀀스를 입력으로 받아, 일련의 연산을 적용하여 최종 결과를 생성합니다. 스트림은 요소들이 순차적으로 거치면서 변환되는 파이프라인과 같습니다.
다음과 같은 다양한 소스에서 스트림을 생성할 수 있습니다.
- List 또는 Set과 같은 컬렉션
- 배열
- 버퍼를 사용한 파일 및 파일 경로
스트림에서 수행되는 작업에는 두 가지 유형이 있습니다.
- 중간 연산
- 종단 연산
중간 연산과 종단 연산
각 중간 연산은 지정된 메서드를 사용하여 입력을 변환하고 새로운 스트림을 내부적으로 반환합니다. 실제 데이터 처리는 일어나지 않으며, 다음 스트림으로 전달됩니다. 스트림이 순회되어 원하는 결과를 얻는 것은 오직 종단 연산에서만 발생합니다.
예를 들어, 10개의 숫자 목록에서 필터링한 후 특정 연산을 적용하려고 한다고 가정해 봅시다. 목록의 모든 요소가 즉시 순회되어 필터링된 결과가 나오고, 다른 연산에 매핑되는 것이 아니라, 개별 요소들이 확인되고 조건에 부합하면 매핑됩니다. 각 요소는 새로운 스트림을 생성합니다.
맵 연산은 전체 목록이 아니라 필터를 만족하는 개별 항목에 대해 수행됩니다. 종단 연산 단계에서 순회되고 하나의 결과로 결합됩니다.
종단 연산이 실행된 후에는 스트림이 소모되어 더 이상 사용할 수 없게 됩니다. 동일한 작업을 다시 수행하려면 새로운 스트림을 생성해야 합니다.
출처: The Bored Dev
스트림 작동 방식에 대한 기본적인 이해를 바탕으로 자바 스트림 구현의 세부 사항을 알아보겠습니다.
#1. 빈 스트림
스트림 API의 empty 메서드를 사용하여 빈 스트림을 만들 수 있습니다.
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream emptyStream = Stream.empty();
System.out.println(emptyStream.count());
}
}
Output:
0
이 스트림의 요소 수를 출력하면 요소가 없는 빈 스트림이므로 출력으로 0을 얻습니다. 빈 스트림은 null 포인터 예외를 피하는 데 유용합니다.
#2. 컬렉션에서 스트림 생성
List 및 Set과 같은 컬렉션은 stream() 메서드를 제공하여 컬렉션에서 스트림을 만들 수 있게 합니다. 이렇게 생성된 스트림을 순회하여 최종 결과를 얻을 수 있습니다.
ArrayList<Integer> list = new ArrayList();
for (int i = 0; i < 20; i++) {
list.add(i+1);
}
System.out.println(list);
Stream<Integer> filtered = list.stream().filter(num -> num > 10);
filtered.forEach(num -> System.out.println(num + " "));
#Output
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
11
12
13
14
15
16
17
18
19
20
#3. 배열에서 스트림 생성
Arrays.stream() 메서드는 배열로부터 스트림을 만드는 데 사용됩니다.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
String[] stringArray = new String[]{"this", "is", "koreantech.org"};
Arrays.stream(stringArray).forEach(item -> System.out.print(item + " "));
}
}
#Output
this is koreantech.org
스트림을 만들 요소의 시작 및 끝 인덱스를 지정할 수도 있습니다. 시작 인덱스는 포함하고, 끝 인덱스는 제외됩니다.
String[] stringArray = new String[]{"this", "is", "koreantech.org"};
Arrays.stream(stringArray, 1, 3).forEach(item -> System.out.print(item + " "));
Output:
is koreantech.org
#4. 스트림을 사용하여 최소 및 최대 숫자 찾기
컬렉션 또는 배열에서 최대 및 최소값을 찾는 것은 자바의 Comparator를 사용하여 수행할 수 있습니다. min() 및 max() 메서드는 Comparator를 인자로 받아 Optional 객체를 반환합니다.
Optional 객체는 null이 아닌 값을 포함하거나 포함하지 않을 수 있는 컨테이너 객체입니다. null이 아닌 값을 포함하는 경우 get() 메서드를 호출하면 값이 반환됩니다.
import java.util.Arrays;
import java.util.Optional;
public class MinMax {
public static void main(String[] args) {
Integer[] numbers = new Integer[]{21, 82, 41, 9, 62, 3, 11};
Optional<Integer> maxValue = Arrays.stream(numbers).max(Integer::compare);
System.out.println(maxValue.get());
Optional<Integer> minValue = Arrays.stream(numbers).min(Integer::compare);
System.out.println(minValue.get());
}
}
#Output
82
3
학습 자료
이제 자바 스트림에 대한 기본적인 이해를 얻었으므로, 자바 8에 대한 지식을 향상시킬 수 있는 5가지 자료를 소개합니다.
#1. 자바 8 인 액션
이 책은 스트림, 람다, 함수형 프로그래밍을 포함하여 자바 8의 새로운 기능들을 상세히 설명하는 가이드입니다. 퀴즈와 지식 확인 질문이 있어 학습한 내용을 복습하는 데 도움이 됩니다.
아마존에서 페이퍼백 및 오디오북 형식으로 구입할 수 있습니다.
#2. 자바 8 람다: 대중을 위한 함수형 프로그래밍
이 책은 자바 SE 개발자들이 람다 표현식 추가가 자바 언어에 미치는 영향을 이해할 수 있도록 특별히 설계되었습니다. 유연한 설명, 코드 연습, 예시를 통해 자바 8 람다식을 익힐 수 있습니다.
아마존에서 페이퍼백과 Kindle 에디션으로 제공됩니다.
#3. 정말 참을성 없는 자바 SE 8
숙련된 자바 SE 개발자라면 이 책을 통해 자바 SE 8의 개선 사항, 스트림 API, 람다 표현식 추가, 자바 동시 프로그래밍 개선 및 많은 사람들이 이해하지 못하는 자바 7 기능에 대해 배울 수 있습니다.
아마존에서 페이퍼백 형식으로만 제공됩니다.
#4. 람다 및 스트림을 사용한 자바 함수형 프로그래밍 배우기

Udemy의 이 강의는 자바 8과 9의 함수형 프로그래밍의 기본 사항을 다룹니다. 람다 식, 메서드 참조, 스트림, 함수형 인터페이스를 중심으로 학습합니다.
함수형 프로그래밍과 관련된 다양한 자바 퍼즐 및 실습도 포함되어 있습니다.
#5. 자바 클래스 라이브러리

Coursera에서 제공하는 이 강의는 자바 클래스 라이브러리의 핵심 사항을 다룹니다. 자바 Generics를 사용하여 타입 안전 코드를 작성하는 방법, 4000개 이상의 클래스로 구성된 라이브러리 이해, 파일 처리 및 런타임 오류 처리 방법을 배울 수 있습니다. 다만 이 강의를 수강하기 위한 몇 가지 선수 조건이 있습니다.
- 자바 소개
- 자바를 사용한 객체 지향 프로그래밍 소개
- 자바의 객체 지향 계층
마지막 말
자바 스트림 API와 자바 8에 람다 함수가 도입되면서 병렬 처리, 함수형 인터페이스, 코드 간결성과 같은 여러 측면에서 자바가 크게 향상되었습니다.
그러나 스트림에는 몇 가지 제한 사항이 있습니다. 가장 큰 제한 사항은 한 번만 사용할 수 있다는 것입니다. 자바 개발자라면 위에서 언급한 자료들이 이러한 주제들을 더 깊이 있게 이해하는 데 도움이 될 것이므로 꼭 확인해 보시기 바랍니다.
자바의 예외 처리 방법에 대해서도 알아볼 수 있습니다.