Java에서 Stream은 데이터 처리 연산을 지원하는 강력한 API로, 컬렉션, 배열, I/O 소스 등의 요소를 선언적이고 효율적으로 다룰 수 있게 해줍니다. 함수형 프로그래밍 스타일을 도입하여 코드의 가독성과 유지보수성을 높이며, 병렬 처리도 간편하게 구현할 수 있습니다.
이 글에서는 Java Stream의 기본 개념, 주요 특징, 활용 방법을 쉽게 설명하고, 실제 예제를 통해 어떻게 효율적으로 사용할 수 있는지 알아보겠습니다.
목차
Toggle1. Stream의 기본 개념
✔ Stream이란?
Java 8부터 도입된 Stream은 컬렉션, 배열 등의 데이터 소스를 처리하기 위한 추상화된 연산 집합입니다. 기존의 반복문(for
, while
)을 대체할 수 있으며, 람다 표현식(Lambda Expressions)과 함께 사용해 코드를 간결하게 작성할 수 있습니다.
Stream은 크게 두 가지로 구분됩니다:
- 중간 연산(Intermediate Operations):
filter()
,map()
,sorted()
등 데이터를 변환하거나 필터링합니다. - 최종 연산(Terminal Operations):
forEach()
,collect()
,reduce()
등 결과를 도출하거나 저장합니다.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
.filter(name -> name.startsWith("A")) // 중간 연산
.forEach(System.out::println); // 최종 연산
✔ Stream vs. 컬렉션(Collection)
특징 | Stream | 컬렉션(Collection) |
---|---|---|
데이터 저장 | 저장하지 않음 | 데이터를 저장 |
연산 방식 | 지연 평가(Lazy Evaluation) | 즉시 평가(Eager Evaluation) |
재사용 가능 | 불가능 (1회용) | 가능 |
병렬 처리 | parallelStream() 으로 간단히 구현 |
직접 구현 필요 |
Stream은 데이터를 직접 저장하지 않고, 원본 데이터를 읽어 연산을 수행합니다. 또한, 지연 평가(Lazy Evaluation)를 사용해 필요한 시점에만 연산을 실행합니다.
2. Stream의 주요 특징
(1) 선언형 프로그래밍 지원
Stream은 “무엇을 할 것인가(What)”에 집중합니다. 반복문은 “어떻게 할 것인가(How)”를 명시해야 하지만, Stream은 원하는 결과를 선언하면 내부적으로 처리합니다.
// 기존 방식 (How)
for (String name : names) {
if (name.length() > 3) {
System.out.println(name);
}
}
// Stream 방식 (What)
names.stream()
.filter(name -> name.length() > 3)
.forEach(System.out::println);
(2) 병렬 처리(Parallel Processing) 간소화
parallelStream()
을 사용하면 멀티 스레드 환경에서 데이터를 분할 처리할 수 있습니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.parallelStream()
.mapToInt(Integer::intValue)
.sum();
System.out.println("합계: " + sum);
(3) 불변성(Immutable)과 1회용
Stream은 원본 데이터를 변경하지 않으며, 한 번 사용하면 재사용할 수 없습니다.
Stream<String> stream = names.stream();
stream.forEach(System.out::println);
// stream.forEach(...); → Error: 스트림이 이미 소비됨
3. Stream의 주요 연산
✔ 중간 연산 (Intermediate Operations)
filter(Predicate)
: 조건에 맞는 요소만 필터링map(Function)
: 요소를 변환 (예: 문자열 → 길이)sorted()
: 정렬distinct()
: 중복 제거
List<Integer> numbers = Arrays.asList(1, 2, 3, 2, 4);
numbers.stream()
.filter(n -> n % 2 == 0) // 짝수만 필터링
.map(n -> n * 2) // 2배 증가
.distinct() // 중복 제거
.forEach(System.out::println);
✔ 최종 연산 (Terminal Operations)
forEach(Consumer)
: 각 요소에 작업 수행collect(Collector)
: 결과를 컬렉션으로 변환reduce()
: 요소를 결합 (예: 총합 계산)count()
: 요소 개수 반환
List<String> result = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
System.out.println(result); // [Alice, Charlie]
4. Stream 활용 예제
✔ 조건에 맞는 데이터 필터링
List<Product> products = Arrays.asList(
new Product("Laptop", 1200),
new Product("Phone", 800),
new Product("Tablet", 500)
);
List<Product> expensiveProducts = products.stream()
.filter(p -> p.getPrice() > 1000)
.collect(Collectors.toList());
✔ 데이터 변환 및 정렬
List<String> cities = Arrays.asList("Seoul", "Busan", "Incheon");
List<String> sortedCities = cities.stream()
.sorted()
.map(String::toUpperCase)
.collect(Collectors.toList());
✔ 병렬 스트림으로 성능 향상
long count = products.parallelStream()
.filter(p -> p.getPrice() > 500)
.count();
5. Stream 사용 시 주의사항
- 재사용 불가: 한 번 사용한 Stream은 다시 사용할 수 없습니다.
- 지연 평가: 중간 연산은 최종 연산이 호출될 때 실행됩니다.
- 성능 고려: 간단한 연산은
for
문이 더 빠를 수 있습니다.
마치며
Java의 Stream API는 데이터 처리 방식을 혁신적으로 바꿨습니다. 람다 표현식과 결합해 코드를 간결하게 만들고, 병렬 처리를 쉽게 구현할 수 있어 현대적인 Java 개발에 필수적입니다.
Stream을 제대로 활용하면 가독성 높은 코드와 효율적인 데이터 처리를 동시에 얻을 수 있습니다. 이제 여러분의 프로젝트에서 Stream을 적용해보세요!
💡 더 알아보기
“어떤 데이터 처리 작업을 Stream으로 바꿔볼까요?” 💬 댓글로 여러분의 생각을 공유해주세요!