Stream이란?
Java 8부터 도입된 기능으로 데이터 흐름을 추상화해서 다루는 도구이다. 컬렉션 데이터를 함수형 프로그래밍 방식으로 처리할 수 있게 해준다.
Stream 연산
Stream 연산은 중간 연산과 최종 연산으로 나뉜다.
List<Integer> numbers = List.of(1,2,3,4)
int sum = numbers.stream()
.filter(n -> n % 2 == 0) // 중간 연산 -> 짝수 필터링 후 Stream 반환
.map(n -> n * 2) // 중간 연산 -> 두 배 변환 후 Stream 반환
.sum(); // 최종 연산 -> 합계 반환
중간 연산은 Stream을 반환하여 중간 연산을 통해 체이닝이 가능하다. 최종 연산은 Stream을 소비하여 최종 결과를 반환한다.
Stream의 특징
1. 기존 데이터 소스를 변경하지 않음
List<Integer> list = List.of(1, 2, 3, 4);
List<Integer> mapList = list.stream().map(number -> number * 2)
.toList();
//출력 -> 1,2,3,4
System.out.println(list);
//출력 -> 2,4,6,8
System.out.println(mapList);
Stream에서 제공하는 연산들은 원본 컬렉션을 변경하지 않고 결과만 새로 생성한다.
2. 일회성 소비
List<Integer> list = List.of(1, 2, 3, 4);
Stream<Integer> stream = list.stream();
List<Integer> result1 = stream.filter(number -> number % 2 != 0)
.toList();
// Stream 재사용 불가
List<Integer> result2 = stream.map(number -> number * 2)
.toList();
한 번 사용된 Stream은 다시 사용할 수 없다.
3. 파이프라인 처리 방식
List<Integer> result = data.stream()
.filter(i -> {
boolean isEven = i % 2 == 0;
System.out.println("filter() 실행: " + i + "(" + isEven + ")");
return isEven;
})
.map(i -> {
int mapped = i * 10;
System.out.println("map() 실행: " + i + " -> " + mapped);
return mapped;
})
.toList();
System.out.println("result = " + result);
위 코드의 실행 결과는 아래와 같다

실행 결과를 보면 요소가 하나씩 중간 연산을 거치는 것을 볼 수 있다.
Stream은 중간 연산에서 데이터를 모아서 한 방에 처리하는 일괄 처리 방식이 아닌, 각각의 요소가 개별적으로 처리되는 파이프라인 처리 방식이다.
지연 연산
Stream은 최종 연산이 호출되기 전까지 중간 연산들을 실행하지 않는다.
List<Integer> data = List.of(1, 2, 3, 4, 5, 6);
System.out.println("== Stream API 시작 ==");
//최종 연산 X
data.stream()
.filter(i -> {
boolean isEven = i % 2 == 0;
System.out.println("filter() 실행: " + i + "(" + isEven + ")");
return isEven;
})
.map(i -> {
int mapped = i * 10;
System.out.println("map() 실행: " + i + " -> " + mapped);
return mapped;
});
System.out.println("== Stream API 종료 ==");
위 코드는 Stream에 최종 연산을 적용하지 않았다.

로그의 시작과 끝만 출력이 되었고, 중간 연산에 로그는 출력되지 않았다. 중간 연산에 진입하지 않았다는 뜻이다.
List<Integer> data = List.of(1, 2, 3, 4, 5, 6);
System.out.println("== Stream API 시작 ==");
data.stream()
.filter(i -> {
boolean isEven = i % 2 == 0;
System.out.println("filter() 실행: " + i + "(" + isEven + ")");
return isEven;
})
.map(i -> {
int mapped = i * 10;
System.out.println("map() 실행: " + i + " -> " + mapped);
return mapped;
})
.toList();
System.out.println("== Stream API 종료 ==");
최종 연산이 추가하고 실행시켜보자

중간 연산의 로그가 출력되었다.
Stream은 반드시 수행해야 하는 최종 연산을 만날 때 까지 중간 연산들을 미룬다. 이렇게 꼭 필요할 때까지 연산을 최대한 미루는 것을 지연(Lazy) 연산이라고 한다.
지연 연산과 파이프라인 처리 방식으로 설계된 이유
1. 단축 평가를 통한 불필요한 연산 생략
단축 평가가 무엇인지 예시로 살펴보자.
List<Integer> data = List.of(1, 2, 3, 4, 5, 6);
System.out.println("== Stream API 시작 ==");
data.stream()
.filter(i -> {
boolean isEven = i % 2 == 0;
System.out.println("filter() 실행: " + i + "(" + isEven + ")");
return isEven;
})
.map(i -> {
int mapped = i * 10;
System.out.println("map() 실행: " + i + " -> " + mapped);
return mapped;
})
//조건을 만족하는 첫 번째 데이터 조회
.findFirst().get();
System.out.println("== Stream API 종료 ==");
findFirst() 메서드로 중간 연산을 거친 요소 중 가장 첫 번째 데이터를 조회해본다.

Stream은 파이프라인 처리 방식이기 때문에 한 요소씩 중간 연산을 거친다.
어느 한 요소가 중간 연산의 모든 조건을 만족해서 최종 연산에 도달하게 된다면, 해당 값은 즉시 반환된다.
최종 연산에 값이 도달하고 Stream이 모두 소비되었기에, 이후 요소들은 연산을 거칠 필요가 없다.
이처럼 연산 중에 건너뛰어도 되는 부분을 알아내고, 불필요한 연산을 피하는 것을 단축 평가라고 한다.
단축 평가는 파이프라인 처리 방식과 지연 연산 덕분에 가능한 최적화 방법 중 하나이다.
2. 메모리 사용 효율
각 요소가 그 때마다 처리되므로 중간 연산 결과를 별도의 자료구조에 저장하지 않아도 된다. 추가적인 메모리 사용은 없다.
정리
익숙한 Stream이지만, 내부적으로 어떻게 동작하는지는 잘 몰랐던 것 같다. 이렇게 효율적으로 동작하는지 몰랐으며, 잘 사용하면 상황에 따라 성능 향상을 얻을 수 있을 것 같다.
'Language > Java' 카테고리의 다른 글
| [Java] ReentrantLock을 통한 스레드 동기화 (0) | 2026.01.02 |
|---|---|
| [Java] 자바 스레드 생명 주기와 메모리 가시성 (0) | 2025.12.30 |
| [Java] Stream Collector, Downstream Collector (0) | 2025.12.26 |
| [Java] 메서드 참조 (0) | 2025.12.24 |
| [Java] 람다와 함수형 프로그래밍 (0) | 2025.12.23 |