Java Stream API 정리
Java 학습 7일차.
이번에는 실무 Java 코드에서 자주 사용하는 Stream API를 학습했다.
Stream은 단순히 데이터를 반복 처리하는 문법이 아니라, 여러 데이터 처리 작업을 하나의 파이프라인으로 연결해서 표현할 수 있도록 해준다.
오늘은 filter, map, flatMap, sorted, distinct, limit, groupingBy, toMap, reduce 등을 학습하고 상품 데이터를 대상으로 실습해 보았다.
1. Stream이란?
Stream은 Collection 등의 데이터를 처리하기 위한 API다.
데이터를 직접 저장하는 자료구조가 아니라, 데이터를 원하는 형태로 가공하기 위한 처리 흐름이라고 이해하면 쉽다.
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * 10)
.toList();
결과:
[20, 40]
처리 흐름을 보면 다음과 같다.
Collection
↓
Stream 생성
↓
filter
↓
map
↓
toList
↓
결과
Stream의 장점은 여러 데이터 처리 작업을 연결해서 표현할 수 있다는 것이다.
2. Collection과 Stream의 차이
Collection과 Stream은 비슷해 보이지만 역할이 다르다.
| Collection | Stream |
| 데이터를 저장하고 관리 | 데이터를 처리 |
| 자료구조 | 데이터 처리 파이프라인 |
| 여러 번 사용 가능 | 한 번 소비하면 재사용할 수 없음 |
| 데이터 자체를 보관 | 데이터를 가공하는 데 초점 |
예를 들어:
List<String> names = List.of("Kim", "Lee", "Park");
names는 데이터를 가지고 있는 Collection이다.
반면:
names.stream()
은 names의 데이터를 처리하기 위한 Stream을 생성한다.
즉,
Collection은 데이터를 담고 있고, Stream은 데이터를 처리한다.
라고 이해하면 된다.
3. Stream 기본 구조
Stream은 보통 다음과 같은 형태로 사용한다.
collection.stream()
.중간연산()
.중간연산()
.최종연산();
예를 들어 상품 목록을 처리하면:
products.stream()
.filter(Product::isActive)
.filter(product -> product.getPrice() >= 10000)
.sorted(Comparator.comparing(Product::getPrice))
.toList();
하나의 파이프라인으로 여러 작업을 연결할 수 있다.
4. Intermediate Operation
Intermediate Operation은 중간 연산이다.
대표적인 메서드는 다음과 같다.
filter
map
flatMap
sorted
distinct
limit
특징은 Stream을 반환한다는 것이다.
예를 들어:
products.stream()
.filter(Product::isActive)
.map(Product::getName)
.sorted();
filter()도 Stream을 반환하고 map()도 Stream을 반환하기 때문에 계속 연결해서 사용할 수 있다.
5. Terminal Operation
Terminal Operation은 최종 연산이다.
대표적으로 다음과 같은 메서드가 있다.
toList
collect
count
max
min
reduce
forEach
예:
long count = products.stream()
.filter(Product::isActive)
.count();
count()가 실행되면 Stream의 처리가 종료된다.
정리하면:
Intermediate Operation
↓
Stream을 계속 연결
Terminal Operation
↓
Stream 처리 종료
6. Lazy Evaluation
Stream의 중요한 특징 중 하나는 지연 평가(Lazy Evaluation)다.
다음 코드만 실행하면:
products.stream()
.filter(Product::isActive)
.map(Product::getName);
실제로 최종 결과를 만드는 작업은 수행되지 않는다.
Terminal Operation이 필요하다.
products.stream()
.filter(Product::isActive)
.map(Product::getName)
.toList();
이때 실제 처리가 수행된다.
즉:
Intermediate Operation
↓
처리 파이프라인 구성
Terminal Operation
↓
실제 처리 시작
7. filter
filter()는 조건에 맞는 데이터만 남긴다.
List<Product> activeProducts = products.stream()
.filter(Product::isActive)
.toList();
판매 중인 상품만 가져오는 코드다.
조건을 직접 작성할 수도 있다.
products.stream()
.filter(product -> product.getPrice() >= 10000)
.toList();
여러 조건을 연결할 수도 있다.
products.stream()
.filter(Product::isActive)
.filter(product -> product.getPrice() >= 10000)
.toList();
8. map
map()은 데이터를 다른 형태로 변환할 때 사용한다.
예를 들어 Product 객체에서 상품명만 추출할 수 있다.
List<String> productNames = products.stream()
.map(Product::getName)
.toList();
변환 관계는 다음과 같다.
Product
↓ map
String
예를 들어:
Product("키보드")
Product("마우스")
Product("모니터")
가 다음과 같이 변환된다.
"키보드"
"마우스"
"모니터"
9. flatMap
flatMap()은 중첩된 구조를 평탄화(flatten)할 때 사용한다.
예를 들어:
List<List<String>> names = List.of(
List.of("Kim", "Lee"),
List.of("Park", "Choi")
);
현재 구조는 다음과 같다.
[
[Kim, Lee],
[Park, Choi]
]
map()을 사용하면 중첩된 구조가 그대로 남을 수 있다.
반면 flatMap()을 사용하면 하나의 Stream으로 평탄화할 수 있다.
List<String> result = names.stream()
.flatMap(list -> list.stream())
.toList();
결과:
[Kim, Lee, Park, Choi]
정리하면:
map
A → B
flatMap
A → 여러 개의 B
↓
하나의 Stream<B>
실무에서는 한 객체가 여러 개의 하위 데이터를 가지고 있을 때 자주 사용할 수 있다.
예를 들어 상품별 태그를 모두 하나의 목록으로 만들 수 있다.
products.stream()
.flatMap(product -> product.getTags().stream())
.toList();
10. sorted
sorted()는 데이터를 정렬한다.
가격 오름차순:
List<Product> sortedProducts = products.stream()
.sorted(Comparator.comparing(Product::getPrice))
.toList();
가격 내림차순:
List<Product> sortedProducts = products.stream()
.sorted(
Comparator.comparing(Product::getPrice)
.reversed()
)
.toList();
11. distinct
distinct()는 중복 데이터를 제거한다.
List<Integer> numbers = List.of(1, 2, 2, 3, 3, 3);
List<Integer> result = numbers.stream()
.distinct()
.toList();
결과:
[1, 2, 3]
객체를 대상으로 사용하는 경우에는 equals()와 hashCode()의 동작도 함께 이해할 필요가 있다.
12. limit
limit()은 앞에서부터 지정한 개수만 가져온다.
products.stream()
.limit(5)
.toList();
예를 들어:
상위 5개 상품
최근 10개 데이터
추천 상품 3개
같은 상황에서 활용할 수 있다.
13. count
count()는 조건에 맞는 데이터의 개수를 반환한다.
long activeCount = products.stream()
.filter(Product::isActive)
.count();
반환 타입은 long이다.
14. max / min
max()와 min()은 최댓값과 최솟값을 조회할 때 사용한다.
가장 비싼 상품:
Product maxPriceProduct = products.stream()
.max(Comparator.comparing(Product::getPrice))
.orElseThrow();
가장 저렴한 상품:
Product minPriceProduct = products.stream()
.min(Comparator.comparing(Product::getPrice))
.orElseThrow();
max()와 min()의 반환값은 Optional<Product>이기 때문에 최종적으로 값을 꺼내기 위해 orElse(), orElseThrow() 등을 사용할 수 있다.
15. collect
collect()는 Stream의 결과를 원하는 자료구조 형태로 모을 때 사용한다.
예전 Java 코드에서는 다음과 같은 형태를 많이 볼 수 있다.
List<String> names = products.stream()
.map(Product::getName)
.collect(Collectors.toList());
최근 Java에서는 다음과 같이 작성할 수도 있다.
List<String> names = products.stream()
.map(Product::getName)
.toList();
16. groupingBy
groupingBy()는 데이터를 특정 기준으로 그룹화할 때 사용한다.
상품을 카테고리별로 그룹화해 보자.
Map<String, List<Product>> productsByCategory = products.stream()
.collect(Collectors.groupingBy(Product::getCategory));
예를 들어:
컴퓨터 → [키보드, 마우스, 모니터]
의류 → [셔츠, 바지, 운동화]
가구 → [책상, 의자]
형태로 그룹화할 수 있다.
즉:
Key = category
Value = 해당 category의 Product List
이다.
17. 카테고리별 상품 수 계산
groupingBy()에 counting()을 함께 사용할 수도 있다.
Map<String, Long> countByCategory = products.stream()
.collect(
Collectors.groupingBy(
Product::getCategory,
Collectors.counting()
)
);
결과 예시:
컴퓨터 → 3
의류 → 3
가구 → 2
이런 식으로 그룹별 집계를 만들 수 있다.
18. toMap
상품 ID를 Key로 사용해 Map으로 변환할 수도 있다.
Map<Long, Product> productMap = products.stream()
.collect(
Collectors.toMap(
Product::getId,
Function.identity()
)
);
결과는 다음과 같은 형태다.
1 → Product
2 → Product
3 → Product
여기서:
Function.identity()
는 다음과 같은 의미다.
product -> product
즉 Key는 상품 ID, Value는 상품 객체다.
19. toMap의 중복 Key 주의사항
toMap()에서는 중복 Key가 발생하면 기본적으로 예외가 발생한다.
예를 들어:
1 → Product A
1 → Product B
처럼 같은 ID가 여러 번 존재한다면 문제가 된다.
이 경우 merge function을 지정할 수 있다.
Map<Long, Product> productMap = products.stream()
.collect(Collectors.toMap(
Product::getId,
Function.identity(),
(oldValue, newValue) -> newValue
));
중복 Key가 발생하면 기존 값과 새로운 값 중 어떤 값을 사용할지 결정할 수 있다.
20. reduce
reduce()는 여러 개의 값을 하나의 값으로 합칠 때 사용한다.
예를 들어 숫자의 합을 구할 수 있다.
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
결과:
15
개념적으로 보면:
0 + 1 = 1
1 + 2 = 3
3 + 3 = 6
6 + 4 = 10
10 + 5 = 15
상품 가격의 총합도 구할 수 있다.
int totalPrice = products.stream()
.map(Product::getPrice)
.reduce(0, Integer::sum);
다만 단순한 숫자 합계라면 다음과 같이 작성하는 편이 더 명확할 수 있다.
int totalPrice = products.stream()
.mapToInt(Product::getPrice)
.sum();
따라서 reduce()를 사용할 수 있다는 것보다 상황에 맞는 연산을 선택하는 것이 중요하다.
21. 상품 데이터로 실습
실습에 사용할 Product 클래스:
public class Product {
private Long id;
private String name;
private String category;
private int price;
private boolean active;
public Product(Long id, String name, String category, int price, boolean active) {
this.id = id;
this.name = name;
this.category = category;
this.price = price;
this.active = active;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getCategory() {
return category;
}
public int getPrice() {
return price;
}
public boolean isActive() {
return active;
}
}
테스트 데이터:
List<Product> products = List.of(
new Product(1L, "키보드", "컴퓨터", 50000, true),
new Product(2L, "마우스", "컴퓨터", 30000, true),
new Product(3L, "모니터", "컴퓨터", 200000, true),
new Product(4L, "셔츠", "의류", 40000, true),
new Product(5L, "바지", "의류", 60000, false),
new Product(6L, "운동화", "의류", 100000, true),
new Product(7L, "책상", "가구", 150000, true),
new Product(8L, "의자", "가구", 80000, false)
);
실습 1. 판매 중인 상품 필터링
List<Product> activeProducts = products.stream()
.filter(Product::isActive)
.toList();
실습 2. 상품명만 추출
List<String> productNames = products.stream()
.map(Product::getName)
.toList();
실습 3. 가격순 정렬
낮은 가격부터:
List<Product> sortedProducts = products.stream()
.sorted(Comparator.comparing(Product::getPrice))
.toList();
높은 가격부터:
List<Product> sortedProducts = products.stream()
.sorted(
Comparator.comparing(Product::getPrice)
.reversed()
)
.toList();
실습 4. 카테고리별 그룹화
Map<String, List<Product>> productsByCategory = products.stream()
.collect(Collectors.groupingBy(Product::getCategory));
실습 5. 카테고리별 상품 수 계산
Map<String, Long> countByCategory = products.stream()
.collect(
Collectors.groupingBy(
Product::getCategory,
Collectors.counting()
)
);
실습 6. 가장 비싼 상품 조회
Product maxPriceProduct = products.stream()
.max(Comparator.comparing(Product::getPrice))
.orElseThrow();
현재 데이터에서는:
모니터 → 200,000원
이 가장 비싸다.
실습 7. 상품 ID → 상품 Map 변환
Map<Long, Product> productMap = products.stream()
.collect(Collectors.toMap(
Product::getId,
Function.identity()
));
22. 여러 연산을 하나의 파이프라인으로 조합하기
Stream의 핵심은 여러 연산을 자연스럽게 연결하는 것이다.
예를 들어:
List<String> result = products.stream()
.filter(Product::isActive)
.filter(product -> product.getPrice() >= 30000)
.sorted(
Comparator.comparing(Product::getPrice)
.reversed()
)
.limit(3)
.map(Product::getName)
.toList();
처리 과정을 순서대로 보면:
1. 판매 중인 상품만 선택
↓
2. 가격 30,000원 이상
↓
3. 가격 높은 순으로 정렬
↓
4. 상위 3개
↓
5. 상품명만 추출
↓
6. List로 반환
현재 데이터에서는 결과가:
[모니터, 책상, 운동화]
이다.
상품 가격이:
모니터 → 200,000
책상 → 150,000
운동화 → 100,000
이기 때문에 가격 내림차순으로 정확히 위와 같이 출력된다.
23. Stream 재사용 문제
Stream은 한 번 Terminal Operation이 실행되면 다시 사용할 수 없다.
다음 코드는 문제가 발생한다.
Stream<Product> stream = products.stream();
stream.filter(Product::isActive)
.toList();
stream.filter(product -> product.getPrice() >= 50000)
.toList();
첫 번째 toList()가 실행되면서 Stream이 이미 소비되었기 때문이다.
Collection은 계속 사용할 수 있다.
products
하지만 Stream은 필요할 때 다시 생성해야 한다.
products.stream()
.filter(Product::isActive)
.toList();
products.stream()
.filter(product -> product.getPrice() >= 50000)
.toList();
따라서 Stream은 일회성 처리 파이프라인이라고 생각하면 이해하기 쉽다.
24. 오늘 배운 Stream 연산 정리
| 메서드 | 설명 |
| filter | 조건에 맞는 데이터만 선택 |
| map | 데이터를 다른 형태로 변환 |
| flatMap | 중첩된 데이터를 평탄화 |
| sorted | 정렬 |
| distinct | 중복 제거 |
| limit | 앞에서부터 N개 선택 |
| count | 데이터 개수 |
| max | 최댓값 |
| min | 최솟값 |
| collect | 결과를 Collection 등으로 수집 |
| groupingBy | 기준별 그룹화 |
| toMap | Map으로 변환 |
| reduce | 여러 값을 하나로 축약 |
25. 면접 체크
Stream이 무엇인가?
Stream은 Collection 등의 데이터를 저장하기 위한 자료구조가 아니라 데이터를 함수형 방식으로 처리하기 위한 API입니다. 여러 중간 연산을 연결해서 데이터 처리 파이프라인을 만들 수 있고 Terminal Operation을 통해 최종 결과를 얻습니다.
Collection과 Stream의 차이는?
Collection은 데이터를 저장하고 관리하는 자료구조이고, Stream은 데이터를 처리하기 위한 API입니다. Collection은 여러 번 사용할 수 있지만 Stream은 Terminal Operation 이후 소비되기 때문에 같은 Stream을 다시 사용할 수 없습니다.
map과 flatMap의 차이는?
map은 하나의 요소를 다른 형태의 하나의 요소로 변환할 때 사용하고, flatMap은 중첩된 구조를 하나의 평탄한 Stream으로 변환할 때 사용합니다.
map
Product → String
flatMap
Product → List<Tag>
↓
Tag
Intermediate Operation과 Terminal Operation의 차이는?
Intermediate Operation은 Stream을 반환하기 때문에 여러 연산을 연결할 수 있으며 지연 실행됩니다. Terminal Operation은 최종 결과를 반환하고 Stream의 처리를 종료합니다.
Intermediate Operation:
filter
map
flatMap
sorted
distinct
limit
Terminal Operation:
toList
collect
count
max
min
reduce
Stream을 여러 번 사용하는 경우의 문제는?
Stream은 Terminal Operation을 수행하면 소비되기 때문에 동일한 Stream을 다시 사용할 수 없습니다. 같은 Collection을 다시 처리해야 한다면 Collection에서 새로운 Stream을 생성해야 합니다.
마무리
이번 Day 7에서 중요한 것은 Stream 메서드를 단순히 암기하는 것이 아니다.
다음과 같은 코드를 봤을 때:
products.stream()
.filter(Product::isActive)
.filter(product -> product.getPrice() >= 10000)
.sorted(Comparator.comparing(Product::getPrice))
.toList();
코드의 동작을 자연스럽게 읽을 수 있어야 한다.
Stream 생성
→ 조건 필터링
→ 추가 조건 필터링
→ 가격순 정렬
→ List 반환
특히 실무에서는 filter → map → sorted → collect, groupingBy → counting, toMap 같은 패턴을 자주 접하게 되므로 익숙해지는 것이 중요하다.
Day 7에서는 “Stream 코드를 읽고 필요한 연산을 조합할 수 있는가?”를 기준으로 학습을 마무리했다.
'개발 > Java' 카테고리의 다른 글
| [Java] Day 9 - JVM Memory ⭐⭐⭐ (0) | 2026.09.15 |
|---|---|
| [Java] Day 8 - Exception (0) | 2026.09.14 |
| [Java] Day 6 - Lambda + Functional Interface (0) | 2026.09.11 |
| [Java] Day 5 - HashMap + equals/hashCode (0) | 2026.09.10 |
| [Java] Day 4 - Collection Framework (0) | 2026.09.09 |
댓글