반응형
1. Stream API란?
1.1 Stream API의 개념
Stream API는 Java 8에서 도입된 기능으로, 데이터의 흐름을 추상화하여 컬렉션 데이터를 선언적으로 처리할 수 있게 해주는 API입니다. '흐름'이라는 단어가 의미하듯이, Stream은 데이터 소스로부터 데이터를 읽어서 파이프라인 형태로 처리하는 것을 가능하게 합니다.
1.2 등장 배경
- 함수형 프로그래밍의 필요성: Java 8에서 람다와 함께 도입되면서 함수형 프로그래밍 패러다임을 지원
- 가독성 있는 코드: 복잡한 데이터 처리를 더 간결하고 이해하기 쉽게 표현
- 병렬 처리의 용이성: 멀티코어 환경에서 병렬 처리를 쉽게 구현할 수 있도록 지원
1.3 기존 방식과의 차이점
기존의 반복문 방식:
List<Order> orders = getOrders(); // 주문 목록 조회
List<Order> highValueOrders = new ArrayList<>();
// 50만원 이상의 주문을 필터링
for (Order order : orders) {
if (order.getAmount().compareTo(new BigDecimal("500000")) >= 0) {
highValueOrders.add(order);
}
}
// 주문 금액 합계 계산
BigDecimal total = BigDecimal.ZERO;
for (Order order : highValueOrders) {
total = total.add(order.getAmount());
}
Stream API를 사용한 방식:
BigDecimal total = orders.stream()
.filter(order -> order.getAmount().compareTo(new BigDecimal("500000")) >= 0)
.map(Order::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
2. Stream API의 기본 구조
2.1 스트림 생성 방법
컬렉션으로부터 생성
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
배열로부터 생성
String[] arr = {"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
숫자 범위로부터 생성
IntStream intStream = IntStream.range(1, 5); // 1,2,3,4
IntStream closedRange = IntStream.rangeClosed(1, 5); // 1,2,3,4,5
직접 값을 지정하여 생성
Stream<String> stream = Stream.of("a", "b", "c");
2.2 중간 연산과 최종 연산
Stream API의 연산은 중간 연산(Intermediate Operations)과 최종 연산(Terminal Operations)으로 구분됩니다.
중간 연산 (Intermediate Operations)
- 새로운 스트림을 반환
- 여러 번 적용 가능
- 지연 평가(Lazy Evaluation) 수행
주요 중간 연산:
// filter: 조건에 맞는 요소 필터링
Stream<T> filter(Predicate<? super T> predicate)
// map: 요소를 변환
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
// sorted: 요소 정렬
Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)
// distinct: 중복 제거
Stream<T> distinct()
// limit: 요소 개수 제한
Stream<T> limit(long maxSize)
// skip: 처음 n개 요소 제외
Stream<T> skip(long n)
실제 사용 예제:
List<Employee> employees = getEmployees();
List<String> seniorEmployeeNames = employees.stream()
.filter(emp -> emp.getYearsOfService() > 5) // 근속년수 5년 초과
.sorted(Comparator.comparing(Employee::getSalary)) // 급여 순 정렬
.map(Employee::getName) // 이름만 추출
.distinct() // 중복 제거
.collect(Collectors.toList()); // List로 수집
최종 연산 (Terminal Operations)
- 스트림을 소비하고 결과를 반환
- 한 번만 적용 가능
- 실제 연산 수행을 트리거
주요 최종 연산:
// forEach: 각 요소에 대해 작업 수행
void forEach(Consumer<? super T> action)
// collect: 결과를 컬렉션으로 수집
<R, A> R collect(Collector<? super T, A, R> collector)
// reduce: 요소들을 하나의 결과로 줄임
Optional<T> reduce(BinaryOperator<T> accumulator)
// count: 요소 개수 반환
long count()
// anyMatch/allMatch/noneMatch: 조건 검사
boolean anyMatch(Predicate<? super T> predicate)
실제 사용 예제:
// 부서별 직원 급여 평균 계산
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
// 전체 급여 합계 계산
BigDecimal totalSalary = employees.stream()
.map(Employee::getSalary)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 고액연봉자 존재 여부 확인
boolean hasHighPaidEmployee = employees.stream()
.anyMatch(emp -> emp.getSalary().compareTo(new BigDecimal("100000000")) > 0);
3. Stream API의 특징과 주의사항
3.1 주요 특징
- 선언형 프로그래밍: 데이터를 어떻게 처리할지 선언하는 방식
- 지연 평가: 최종 연산이 호출될 때까지 중간 연산이 실행되지 않음
- 재사용 불가: 스트림은 한 번만 사용 가능
- 내부 반복: 반복 처리를 개발자가 아닌 API가 처리
3.2 주의사항
스트림 재사용 불가
Stream<String> stream = list.stream();
stream.forEach(System.out::println); // 정상 동작
stream.forEach(System.out::println); // IllegalStateException 발생
병렬 스트림 사용 시 주의
// 병렬 처리가 항상 더 빠른 것은 아님
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 순차 처리
long sequentialTime = measureTime(() ->
numbers.stream().map(this::heavyOperation).count()
);
// 병렬 처리
long parallelTime = measureTime(() ->
numbers.parallelStream().map(this::heavyOperation).count()
);
3.3 실전 활용 예제
급여 관리 시스템의 예제를 통해 Stream API의 실제 활용을 살펴보겠습니다:
public class SalaryProcessor {
public Map<String, SalaryStats> analyzeSalaries(List<Employee> employees) {
return employees.stream()
// 퇴사자 제외
.filter(emp -> !emp.isResigned())
// 부서별 그룹화
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.collectingAndThen(
Collectors.toList(),
this::calculateDepartmentStats
)
));
}
private SalaryStats calculateDepartmentStats(List<Employee> deptEmployees) {
DoubleSummaryStatistics stats = deptEmployees.stream()
.mapToDouble(emp -> emp.getSalary().doubleValue())
.summaryStatistics();
return new SalaryStats(
BigDecimal.valueOf(stats.getAverage()),
BigDecimal.valueOf(stats.getMin()),
BigDecimal.valueOf(stats.getMax()),
stats.getCount()
);
}
}
마무리
이상으로 Stream API의 기본 개념과 구조, 그리고 주요 사용법에 대해 알아보았습니다. 다음 포스트에서는 Collectors의 고급 활용과 실전에서 자주 사용되는 패턴들에 대해 자세히 다루도록 하겠습니다.
반응형