스트림 API 시작
StreamStartMain.java
package stream.start;
import java.util.List;
import java.util.stream.Stream;
public class StreamStartMain {
public static void main(String[] args) {
List<String> names = List.of("Apple", "Banana", "Berry", "Tomato");
// "B"로 시작하는 이름 필터 후 대문자로 바꿔 리스트 수집
Stream<String> stream = names.stream(); // List의 stream() 메서드로 자바가 제공하는 스트림 생성
List<String> result = stream.filter(name -> name.startsWith("B"))
.map(s -> s.toUpperCase())
.toList();
System.out.println("외부 반복으로 출력");
for(String s : result) {
System.out.println(s);
}
System.out.println("내부 반복으로 출력");
names.stream().filter(name -> name.startsWith("B"))
.map(s -> s.toUpperCase())
.forEach(System.out::println);
System.out.println("메서드 참조");
names.stream().filter(name -> name.startsWith("B"))
.map(String::toUpperCase)
.forEach(System.out::println);
}
}
> 중간 연산(filter, map)은 데이터를 걸러내거나 형태를 변환하며 최종연산(toList(), forEach)를 통해 최종 결과를 모으거나 실행함
> 스트림의 내부 반복을 통해 "어떻게 반복할지"보다 "어떻게 변환되어야 하는지"에 집중할 수 있음 = 선언형 프로그래밍
> 메서드 참조로 람다식이 더 간결해지며 가독성이 높아짐
스트림 API란?
스트림(Stream)
: 자바 8부터 추가된 기능으로 데이터의 흐름을 추상화해 다루는 도구
: 컬렉션 또는 배열 등 요소들을 연산 파이프라인을 통해 연속적인 형태로 처리할 수 있게 해줌
* 파이프라인
스트림이 여러 단계를 거쳐 변환되고 처리되는 모습이 마치 물이 여러 파이프를 타고 이동하며 정수 시설이나 필터를 거치는 과정과 유사
스트림의 특징
1. 데이터 원본을 변경하지 않음
> 스트림에서 제공하는 연산들은 원본 컬렉션을 변경하지 않고 결과만 새로 생성
2. 일회성
> 한 번 사용된 스트림은 다시 사용할 수 없음
3. 파이프라인 구성
> 중간 연산들이 이어지다가 최종 연산을 만나면 연산이 수행되고 종료됨
4. 지연 연산
> 중간 연산은 필요할 때까지 실제 동작하지 않고, 최종 연산이 실행될 때 한 번에 처리
5. 병렬 처리
> 스트림으로부터 병렬 스트림을 쉽게 만들 수 있어 멀티코어 환경에서 병렬 연산을 비교적 단순한 코드로 작성 가능
1. 데이터 원본을 변경하지 않음
package stream.basic;
import java.util.List;
public class ImmutableMain {
public static void main(String[] args) {
List<Integer> originList = List.of(1,2,3,4);
List<Integer> filteredList = originList.stream()
.filter(n -> n%2==0)
.toList();
System.out.println("filteredList = " + filteredList); // 2, 4 출력
System.out.println("originList = " + originList); // 1, 2, 3, 4 출력
}
}
2. 일회성 : 중복 실행 안됨
package stream.basic;
import java.util.stream.Stream;
public class DuplicateExecutionMain {
public static void main(String[] args) {
Stream<Integer> stream = Stream.of(1,2,3);
stream.forEach(s -> System.out.println(s)); // 최초 실행
stream.forEach(System.out::println); // 중복 실행 -> 에러 발생
}
}
실행 결과

-> 스트림 중복 실행 시 "스트림이 이미 작동했거나 닫혔습니다" 예외 발생
-> 한 번 사용된 스트림은 다시 사용할 수 없으며 필요하다면 새 스트림을 생성해야 한다.
// 대안 : 대상 리스트를 스트림으로 새로 생성해 사용
List<Integer> list = List.of(1,2,3);
Stream.of(list).forEach(System.out::println);
3. 파이프라인 구성
기존 직접만들었던 Stream과 자바에서 제공하는 Stream을 사용해 비교해보자
package stream.basic;
import lambda.lambda5.mystream.MyStreamV2;
import java.util.List;
public class LazyEvalMain1 {
public static void main(String[] args) {
List<Integer> data = List.of(1,2,3,4,5,6);
ex1(data);
ex2(data);
}
private static void ex1(List<Integer> data) {
System.out.println("== Stream API 시작 ==");
List<Integer> result = MyStreamV2.of(data)
.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);
System.out.println("== Stream API 종료 ==");
}
private static void ex2(List<Integer> data) {
System.out.println("== Stream API 시작 ==");
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);
System.out.println("== Stream API 종료 ==");
}
}
1) 일괄처리 : ex1()
> 직접 만든 Stream 사용

-> 공정을 단계별로 쪼개 데이터 전체를 한 번에 처리하고, 결과를 저장해두었다가 다음 공정을 한 번에 수행
-> 마치 "모든 쿠키 반죽 -> 반죽 완료 -> 전부 굽기 -> 전부 굽기 완료 -> 전부 포장"의 흐름과 유사
-> filter가 모든 요소를 순서대로 실행한 뒤 통과한 요소에 대해 map이 한 번에 실행됨
2) 파이프라인 처리 : ex2()
> 자바 Stream 사용

-> 한 요소가 한 공정을 마치면, 즉시 다음 공정으로 넘어가는 구조
-> 자동차 공장에서 조립 라인에 제품이 흐르는 모습과 유사
-> data에 1일 들어오고 filter에서 false 반환 시 바로 다음 요소로 skip
정리
1. 두 방식 모두 짝수를 골라 10을 곱해주는 결과는 같지만 실행 과정에서 차이가 발생
2. 자바 스트림은 중간 단계에서 데이터를 모아 한 번에 처리하지 않고, 한 요소가 중간 연산을 통과하면 곧바로 다음 중간 연산으로 이어지는 파이프라인 형태를 가짐
4. 지연 연산
위의 함수애 toList() 최종 연산을 제외하고 실행해보자
package stream.basic;
import lambda.lambda5.mystream.MyStreamV2;
import java.util.List;
public class LazyEvalMain2 {
public static void main(String[] args) {
List<Integer> data = List.of(1,2,3,4,5,6);
ex1(data);
ex2(data);
}
private static void ex1(List<Integer> data) {
System.out.println("== Stream API 시작 ==");
MyStreamV2.of(data)
.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 종료 ==");
}
private static void ex2(List<Integer> data) {
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;
});
System.out.println("== Stream API 종료 ==");
}
}
1) 즉시 연산 : ex1()
> 직접 만든 Stream 사용

-> 최종 연산(toList, forEach 등)을 호출하지 않았는데도 filter, map이 바로바로 실행되어 모든 로그가 찍힘
-> 중간 연산이 호출될 때마다 바로 연산을 수행하고 그 결과를 내부 List 등에 저장해두는 방식을 취함
2) 지연 연산 : ex2()
> 자바 Stream 사용

-> 최종 연산이 호출되지 않으면 아무 일도 하지 않음
-> 중간 연산들은 "이런 일을 할 것이다"라는 파이프라인 설정을 해놓기만 하고, 정작 실제 연산은 최종 연산이 호출되기 전까지 전혀 진행되지 않음
-> 스트림 API는 게으름.. 정말 꼭 필요할 때만 연산을 수행하도록 최대한 미루고 미룸 = 지연 연산
지연 연산과 최적화
계산한 연산 중 첫 번째 항목 하나만 찾는다고 가정해보자
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;
})
.findFirst().get();
-> 자바 스트림은 첫 번째 최종 연산을 구할 수 있는 findFirst()를 제공 = 단축 평가(short-circuit)
-> 직접 만든 MyStream은 모든 요소에 대한 연산을 끝까지 수행한 후 결과 목록 중 첫 번째 요소를 반환하므로 필터 6번 + 맵 3번의 총 9번의 연산을 수행하게 된다.
-> 자바의 Stream은 조건을 만족하는 요소를 찾은 순간 연산을 멈추고 결과를 반환하므로 필터 2번 + 맵 1번의 총 3번의 연산을 수행하게 된다.
지연 연산 장점
1. 불필요한 연산 생략 : findFirst(), limit()와 같은 단축 연산 사용 시 결과를 찾은 시점에서 더 이상 나머지 요소를 처리할 필요가 없음
2. 메모리 사용 효율 : 중간 연산 결과를 매 단계마다 별도 자료구조에 저장하지 않고 최종 연산 때까지 필요할 때만 가져와서 처리
3. 파이프라인 최적화 : 스트림은 요소를 하나씩 꺼내며 필터와 맵 등을 묶어서 실행하기에 내부적으로 효율적으로 동작함
=> 스트림 API의 핵심은 "어떤 연산을 할지" 파이프라인으로 정의해놓고, 최종 연산이 실행될 때 한 번에 처리한다는 것
'인프런 > 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍' 카테고리의 다른 글
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 9. 스트림 API 3 - 컬렉터 (0) | 2025.06.23 |
|---|---|
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 9. 스트림 API 2 - 기능 (0) | 2025.06.17 |
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 7. 메서드 참조 (0) | 2025.05.28 |
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 6. 람다 vs 익명 클래스 (0) | 2025.05.28 |
| [인프런] 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 / 5. 람다 활용 (0) | 2025.05.22 |