언어/JAVA

[JAVA] Java Stream API 완벽 가이드 - Part 1: 소개와 기초

shaprimanDev 2025. 1. 18. 14:00
반응형

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 주요 특징

  1. 선언형 프로그래밍: 데이터를 어떻게 처리할지 선언하는 방식
  2. 지연 평가: 최종 연산이 호출될 때까지 중간 연산이 실행되지 않음
  3. 재사용 불가: 스트림은 한 번만 사용 가능
  4. 내부 반복: 반복 처리를 개발자가 아닌 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의 고급 활용과 실전에서 자주 사용되는 패턴들에 대해 자세히 다루도록 하겠습니다.

반응형