이번 글은 단순히
"Java 9에서 이런 메서드가 추가됐습니다."
를 나열하는 글이 아니다.
실무에서
stream()
.filter()
.map()
.collect(...)
만 사용하는 개발자가
Java 21 수준의 Stream을 사용할 수 있도록 흐름 중심으로 설명한다.
Stream 개선사항(Java 9~21)
Java 9 이후 Stream API는 "더 적은 코드로 더 안전하고 더 읽기 쉬운 데이터 처리"를 목표로 계속 발전해 왔다.
Java 8의 Stream 한계
Java 8에서는
List<Integer> numbers =
List.of(1, 2, 3, 4, 5, 6);
앞에서 조건이 깨질 때까지 가져오고 싶다면
List<Integer> result =
numbers.stream()
.filter(n -> n < 4)
.collect(Collectors.toList());
처럼 작성했다.
하지만 이것은
1
2
3
4
5
6
모든 데이터를 끝까지 검사한다.
사실 우리가 원하는 것은
1
2
3
↓
4 발견
↓
즉시 종료
이다.
takeWhile() (Java 9)
조건이 참인 동안만 가져오고 처음 거짓이 나오면 즉시 종료한다.
List<Integer> result =
numbers.stream()
.takeWhile(n -> n < 4)
.toList();
실행
1 → true
2 → true
3 → true
4 → false
↓
종료
결과
[1, 2, 3]
filter()와 차이
filter
numbers.stream()
.filter(n -> n < 4)
실행
1 확인
2 확인
3 확인
4 확인
5 확인
6 확인
끝까지 검사한다.
takeWhile
numbers.stream()
.takeWhile(n -> n < 4)
실행
1 확인
2 확인
3 확인
4 확인
↓
즉시 종료
dropWhile() (Java 9)
반대 기능이다.
List<Integer> result =
numbers.stream()
.dropWhile(n -> n < 4)
.toList();
실행
1 제거
2 제거
3 제거
4 발견
↓
이후 모두 유지
결과
[4, 5, 6]
ofNullable() (Java 9)
기존
String name = getName();
Stream<String> stream =
name == null
? Stream.empty()
: Stream.of(name);
Java 9
Stream<String> stream =
Stream.ofNullable(name);
동작
null
↓
빈 Stream
------------
Kim
↓
Stream("Kim")
실무에서 Optional과 함께 많이 사용된다.
iterate() 개선 (Java 9)
Java 8
Stream.iterate(
1,
n -> n + 1
)
.limit(5);
limit을 반드시 사용해야 했다.
Java 9
Stream.iterate(
1,
n -> n <= 5,
n -> n + 1
);
실행
1
2
3
4
5
6 → false
↓
종료
limit 없이도 종료 조건을 지정할 수 있다.
toList() (Java 16)
기존
List<String> result =
users.stream()
.map(User::getName)
.collect(Collectors.toList());
Java 16
List<String> result =
users.stream()
.map(User::getName)
.toList();
훨씬 간결하다.
그런데 정말 같은 것일까?
많은 사람들이
toList()
=
Collectors.toList()
라고 생각한다.
사실은 다르다.
Collectors.toList()
List<String> list =
stream.collect(Collectors.toList());
list.add("Kim");
가능하다.
보통 Mutable List(ArrayList)가 생성된다.
Stream.toList()
List<String> list =
stream.toList();
list.add("Kim");
결과
UnsupportedOperationException
Immutable List를 반환한다.
실무에서 중요한 이유
List<UserDto> users =
repository.findAll()
.stream()
.map(UserDto::from)
.toList();
이 결과는
조회 결과
↓
수정하지 않음
↓
Immutable
↓
안전
이라는 의도를 표현할 수 있다.
mapMulti() (Java 16)
조금 어려운 기능이다.
기존
users.stream()
.flatMap(user ->
user.getOrders()
.stream()
);
flatMap은
매번
List
↓
Stream 생성
↓
합치기
를 수행한다.
mapMulti
users.stream()
.<Order>mapMulti(
(user, consumer) -> {
for (Order order : user.getOrders()) {
consumer.accept(order);
}
}
);
중간 Stream을 생성하지 않는다.
즉,
flatMap
↓
Stream 생성
↓
객체 증가
----------------
mapMulti
↓
consumer에 직접 전달
↓
객체 감소
대량 데이터에서는 더 효율적이다.
Optional.stream() (Java 9)
Optional도 Stream으로 바꿀 수 있다.
기존
if (optional.isPresent()) {
stream.add(optional.get());
}
Java 9
optional.stream();
동작
Optional.empty()
↓
빈 Stream
------------
Optional(User)
↓
Stream(User)
실무 예제
여러 Optional을 처리할 때
users.stream()
.map(User::getAddress)
.flatMap(Optional::stream)
.toList();
굉장히 많이 사용하는 패턴이다.
Stream.iterate + takeWhile
조합도 가능하다.
Stream.iterate(
1,
n -> n + 1
)
.takeWhile(n -> n <= 5)
.toList();
실행
1
2
3
4
5
6
↓
takeWhile 종료
Java 21에서 Stream은 달라졌을까?
새로운 API가 크게 추가된 것은 아니다.
대신
Virtual Thread
Structured Concurrency
Pattern Matching
Record
등과 함께 사용할 수 있도록
전체적인 생태계가 발전했다.
실무에서 가장 많이 사용하는 최신 API
매우 자주 사용
stream.toList();
Stream.ofNullable(value);
takeWhile(...)
가끔 사용
dropWhile(...)
Optional.stream()
특정 성능 최적화
mapMulti()
Java 8 코드와 비교
Java 8
users.stream()
.collect(Collectors.toList());
Java 21
users.stream()
.toList();
Java 8
value == null
?
Stream.empty()
:
Stream.of(value);
Java 21
Stream.ofNullable(value);
Java 8
filter(...)
Java 21
takeWhile(...)
dropWhile(...)
면접 포인트
Java 9 이후 Stream에서 가장 많이 사용하는 기능은?
- takeWhile()
- dropWhile()
- Stream.ofNullable()
- Optional.stream()
Stream.toList()와 Collectors.toList() 차이는?
| Immutable List | 일반적으로 Mutable List |
| Java 16 | Java 8 |
takeWhile()와 filter() 차이는?
- filter() : 끝까지 검사한다.
- takeWhile() : 조건이 처음 거짓이 되는 순간 즉시 종료한다.
mapMulti()는 왜 추가되었는가?
flatMap()이 중간 Stream 객체를 생성하는 비용을 줄이기 위해, Consumer에 직접 데이터를 전달하는 방식으로 성능을 개선하기 위해 추가되었다.
최신 Java 문법 챕터 마지막 글
다음은 Virtual Thread(Java 21) 이다.
이 글에서는
- 왜 Thread가 비싼 자원인지
- Platform Thread와 Virtual Thread의 차이
- JVM 내부에서 어떻게 동작하는지
- Spring Boot 3.2에서 Virtual Thread를 사용하는 방법
- @Async, ExecutorService, ThreadPool과의 관계
- 언제 사용해야 하고 언제 사용하면 안 되는지
까지 최신 Java에서 가장 중요한 주제를 깊이 있게 설명하겠다.
'language > java' 카테고리의 다른 글
| Reflection과 Spring DI (0) | 2026.06.17 |
|---|---|
| Virtual Thread(Java 21) (0) | 2026.06.17 |
| Optional 심화 (0) | 2026.06.17 |
| sealed class(Java 17) (0) | 2026.06.17 |
| record(Java 16) (0) | 2026.06.17 |
댓글