[TIL] 자바 스트림

냠냠빈·2024년 11월 25일

스트림이란??

자바에서는 파일이나 콘솔의 입출력을 직접 다루지 않고, 스트림(stream)이라는 흐름을 통해 다룹니다. 스트림이란 실제의 입력이나 출력이 표현된 데이터의 이상화된 흐름을 의미합니다. 즉, 스트림은 운영체제에 의해 생성되는 가상의 연결 고리를 의미하며, 중간 매개자 역할을 합니다. 자바의 스트림(Stream)은 데이터 처리 작업을 간단하고 효율적으로 수행할 수 있도록 설계된 API로, 특히 Java 8에서 도입된 이후로 데이터의 집합을 처리하는 데 있어 매우 유용한 도구로 자리 잡았습니다.

스트림 구조


Stream의 특징

  • 선언형 프로그래밍: 스트림을 사용하면 데이터 처리 로직을 선언적으로 표현할 수 있습니다.
    예: list.stream().filter(...).map(...)
  • 연속성: 스트림은 데이터 요소를 순차적으로 처리합니다. 이 과정은 스트림 파이프라인(Stream Pipeline)으로 불립니다.
  • 불변성: 스트림 자체는 원본 데이터를 변경하지 않습니다. 데이터를 처리한 결과는 새로운 스트림으로 반환됩니다.
  • 지연(lazy) 처리: 스트림의 연산은 필요할 때만 실행됩니다. (예: 최종 연산을 호출할 때)
  • 재사용 불가능: 한 번 사용한 스트림은 다시 사용할 수 없습니다.
  • 병렬 처리 지원: parallelStream()을 사용하면 손쉽게 병렬 처리를 구현할 수 있습니다.

스트림의 구조

스트림은 보통 아래와 같은 세 가지 주요 단계로 구성됩니다:

  • 데이터 소스 생성 (Source): 스트림은 데이터를 생성하는 단계입니다.
    예: list.stream(), Arrays.stream(array)
  • 중간 연산 (Intermediate Operations): 스트림 데이터를 변환하거나 필터링하는 작업입니다. 중간 연산은 lazy 방식으로 처리됩니다.
    예: filter(), map(), sorted(), distinct()
  • 최종 연산 (Terminal Operations): 스트림 작업을 마무리하고 결과를 생성합니다.
    최종 연산은 스트림 파이프라인을 실행합니다.
    예: forEach(), collect(), count(), reduce()

자주 사용하는 문법

스트림 API에서 자주 사용하는 메서드 몇 가지를 알아보겠습니다:

  • 스트림 생성

    List<String> list = Arrays.asList("A", "B", "C");
    Stream<String> stream = list.stream();
  • 중간 연산

    filter(Predicate)

    list.stream().filter(s -> s.startsWith("A"));
    • 특정 조건에 맞는 요소만 필터링.

    map(Function)

    list.stream().map(String::toLowerCase);
    • 각 요소를 변환.

    sorted()

    list.stream().sorted();
    • 정렬.

    distinct()

    list.stream().distinct();
    • 중복 제거.

    limit(n) & skip(n)

    list.stream().limit(2).skip(1);
    • 결과를 제한하거나 건너뜀.
  • 최종 연산

    collect(Collectors)

    List<String> result = list.stream()
                    .filter(s -> s.startsWith("A"))
                    .collect(Collectors.toList());   
    • 결과를 리스트로 수집.

    forEach(Consumer)

    list.stream().forEach(System.out::println);
    • 각 요소를 반복 처리.

    reduce(BinaryOperator)

    int sum = Arrays.stream(new int[]{1, 2, 3})
                    .reduce(0, Integer::sum);
    • 요소를 결합하여 하나의 결과 생성.

작성 시 주의할 점

  • 한 번 사용한 스트림은 재사용 불가능: 스트림은 일회성으로, 다시 처리하려면 새로운 스트림을 생성해야 합니다.

    Stream<String> stream = list.stream();
    stream.forEach(System.out::println); // 사용 후 재사용 불가
  • 지연 평가에 유의: 중간 연산최종 연산이 호출될 때까지 실행되지 않으므로, 필요 없는 연산을 방지해야 합니다.

  • 병렬 처리 시 주의: parallelStream()은 데이터 구조나 작업 종류에 따라 성능이 저하될 수 있습니다. 스레드 안전성을 확인해야 합니다.

  • 메모리 관리: 무한 스트림(Stream.generate 또는 Stream.iterate)을 사용할 때 메모리 누수에 주의하세요.

    Stream.generate(() -> "A").limit(100); // 제한 필수
  • 명확한 가독성: 스트림 체인이 너무 길어지면 가독성이 떨어질 수 있습니다. 적절히 나누거나 주석을 추가하세요.


실용적인 예제

  • 문자열 리스트에서 특정 조건의 데이터를 필터링하고 변환하는 예제

    List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
    
    List<String> filteredNames = names.stream()
                                      .filter(name -> name.startsWith("A"))
                                      .map(String::toUpperCase)
                                      .sorted()
                                      .collect(Collectors.toList());
    
    System.out.println(filteredNames); // [ALICE]
  • 숫자 리스트에서 합계 구하기

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    
    int sum = numbers.stream()
                     .filter(num -> num % 2 == 0) // 짝수만 필터링
                     .reduce(0, Integer::sum);
    
    System.out.println(sum); // 6
profile
다 먹어버릴거야!

0개의 댓글