[Java] Java 기초 - Stream

Hyunjun Kim·2025년 4월 13일
0

Data_Engineering

목록 보기
26/153

15 스트림(Stream)

자바에서 스트림(Stream)은 함수형 프로그래밍(Functional Programming)을 가능하게 해주는 도구이다.
스트림을 사용하면 코드의 흐름을 함수처럼 연결해서 처리할 수 있으며, 간결하고 오류가 적은 코드를 작성할 수 있다.

기존에는 조건문이나 반복문처럼 명령을 순서대로 나열하는 방식, 즉 절차적 프로그래밍(Procedural Programming)이 일반적이었다. 이후 객체지향 프로그래밍(Object-Oriented Programming) 개념이 도입되며, 객체 단위로 코드 구조를 설계하는 방식이 확산되었다.

함수형 프로그래밍은 그 다음 개념으로, 데이터를 흐름처럼 처리하는 방식이다. 복잡한 상태 관리나 반복문 없이도 원하는 결과를 얻을 수 있으며, 코드의 가독성과 안정성이 높아지는 장점이 있다.

자바는 본래 함수형 언어는 아니지만, 자바 8부터 Stream API를 도입하여 함수형 프로그래밍 스타일을 일부 지원할 수 있게 되었다.
따라서 스트림을 잘 활용하면 더 선언적이고 명확한 코드를 작성할 수 있다고 할 수 있다.

15.1 스트림(stream)이란

  • 스트림은 곧 '데이터의 흐름'이다.
  • 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자다.
  • 스트림을 활용해서 필터링,데이터 변경, 다른 타입이나 자료구조로 변환 등을 할 수 있다.

코딩을 하다 보면 대부분의 작업은 데이터를 전달하고, 변환한 뒤, 다시 다른 곳에 전달하는 과정으로 이루어진다.
이러한 작업 흐름이 마치 데이터가 흐르는 것처럼 보이기 때문에, Stream이라는 이름이 붙었다고 볼 수 있다.

자바에서는 주로 컬렉션(Collection)을 처리할 때 스트림을 많이 사용한다.
그 이유는 자바 컬렉션에 대해 스트림을 바로 생성할 수 있는 메서드(stream())가 제공되기 때문이다.

즉, 데이터를 담고 있는 컬렉션으로부터 스트림을 생성하고,
그 스트림에 여러 개의 중간 연산(map, filter 등)과 최종 연산(forEach, collect 등)을 연결하여 처리하는 방식이 일반적이다.

이러한 구조 덕분에 스트림은 컬렉션 데이터를 간결하고 효과적으로 처리할 수 있는 도구로 널리 활용되고 있다.



15.2 스트림의 특징

  1. 스트림은 원본 데이터를 변경하지 않는다.
    스트림에서 map, filter 등으로 데이터를 변환하더라도, 원래의 데이터 소스(예: ArrayList)는 변하지 않는다.
    예를 들어, ["korea", "japan"]이라는 리스트를 스트림으로 만들어 toUpperCase()를 적용해도, 원본 리스트는 그대로 소문자를 유지한다.
    스트림의 각 단계는 이전 데이터를 기반으로 새로운 데이터를 생성하는 방식이며, 자바 관점에서는 서로 다른 객체로 간주된다.

  2. 스트림은 내부적으로 반복 처리된다.
    전통적인 for나 for-each 문처럼 개발자가 반복 처리를 직접 명시하지 않아도, 스트림 내부에서 자동으로 요소를 순회하며 처리한다.
    즉, stream.map(...).filter(...).forEach(...)처럼 한 줄로 작성해도, 내부적으로는 데이터의 크기만큼 반복적으로 연산이 수행된다.

  3. 스트림은 일회성이다.
    스트림은 한 번 소비되면 닫히기 때문에 재사용이 불가능하다.
    예를 들어, 하나의 스트림을 forEach로 출력한 다음, 다시 collect하려고 하면 예외가 발생한다.
    따라서 같은 데이터를 여러 번 처리하고자 한다면, 스트림을 새로 생성해야 한다.


15.3 스트림의 구조

  1. 스트림 생성
    스트림을 이용하기 위해 먼저 스트림을 생성해야 함.
    Stream<T> Collection.stream() 을 이용하여 해당하는 컬렉션을 기반으로하는 스트림을 생성할 수 있다.

  2. 중간 연산
    중간 단계로써 데이터의 형변환 혹은 필터링, 정렬 등 스트림에 대한 가공을 해준다.
    map(변환) / sorted(정렬) / skip(스트림 자르기) / limit(스트림 자르기) 등이 있다.

  3. 최종 연산
    스트림의 요소를 소모해서 결과를 반환하는 단계다. 최종 연산 이후에는 스트림이 닫히게 되고 더 이상 사용할 수 없다. (최종 연산의 결과값은 단일 값일 수도 있고 배열 혹은 컬렉션일 수도 있다.)
    collect()를 이용해서 다른 콜렉션으로 바꾸는 것, reduce를 이용해서 incremental calculation하는 것도 가장 많이 쓰이는 패턴이다.



15.4 스트림을 예제

15.4.1 스트림 예제 1: 필터링 후 새로운 데이터셋 만들기

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("서울");
        list.add("부산");
        list.add("속초");
        list.add("서울");
        System.out.println(list);
        List<String> result = list.stream() // 스트림 생성
                .limit(2) // 중간 연산
                .collect(Collectors.toList()); // 최종 연산

        System.out.println(result);
        System.out.println("list -> transformation -> set");
        Set<String> set = list.stream().filter("서울"::equals).collect(Collectors.toSet());

        set.forEach(System.out::println);
    }
}

15.4.2 스트림 예제 2: Array를 Stream으로 변환

public class Main {
    public static void main(String[] args) {
        String[] arr = {"엑셀보다 쉬운 SQL", "웹개발 종합반", "알고보면 알기쉬운 알고리즘", "웹개발의 봄,Spring"};
        Stream<String> stringStream = Arrays.stream(arr);
        stringStream.forEach(className -> System.out.println("수업명 : " + className));
        System.out.println();
    }
}

15.4.3 스트림 예제 3: map연산 활용

[코드스니펫] 스트림 예제 3

예제 3을 위해 build.gradle의 dependencies 코드 블럭에 다음과 같이 library를 복붙해서 추가하자.

dependencies {
    implementation 'org.apache.commons:commons-lang3:3.0'  
    // other codes..
}

복붙 후에 grale의 sync를 맞춰준 후
외부 라이브러리에 org.apache.commons:commons-lang3:3.0 가 잘 들어와있으면 준비 완료!

IntelliJ에서 잘만 썼는데 VSCode에서는 Import Error?

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.apache.commons.lang3.tuple.Pair;

class Sale {
    String fruitName;
    int price;
    float discount;

    public Sale(String fruitName, int price, float discount) {
        this.fruitName = fruitName;
        this.price = price;
        this.discount = discount;
    }
}


public class Main {
    public static void main(String[] args) {
        List<Sale> saleList =
                Arrays.asList(new Sale("Apple", 5000, 0.05f), new Sale("Grape", 3000, 0.1f),
                        new Sale("Orange", 4000, 0.2f), new Sale("Tangerine", 2000, 0));
        Stream<Sale> saleStream = saleList.stream();
        saleStream.map(sale -> Pair.of(sale.fruitName, sale.price * (1 - sale.discount))).forEach(
                pair -> System.out.println(pair.getLeft() + " 실 구매가: " + pair.getRight() + "원 "));
    }
}

15.4.4 스트림 예제 4: reduce 를 이용한 계산

public class Main {
    public static void main(String[] args) {
        List<Integer> numArr = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Integer result = numArr.stream().reduce(0, Integer::sum);
        // reduce와 sum을 활용하여 1부터 10까지 더하게 됩니다.
        System.out.println(result);
    }
}


15.5 스트림 퀴즈

문제 : '이'씨 성을 가진 사람들의 수를 세려고 한다.
이름 : ["김정우", "김호정", "이하늘", "이정희", "박정우", "박지현", "정우석", "이지수"]

다음 Javadoc (자바의 클래스 명세)를 확인하고 참고하자

https://docs.oracle.com/javase/8/docs/api/java/lang/String.html#startsWith-java.lang.String-
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#count--

답안

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("김정우", "김호정", "이하늘", "이정희", "박정우", "박지현", "정우석", "이지수");
        long count = names.stream()

                .filter(str -> str.startsWith("이")).count();
        System.out.println("count : " + count);
    }
}


15.6 stream 심화

15.6.1 중간 연산 (Intermediate Operations)

중간 연산은 스트림을 변환하거나 필터링하고, 최종 연산 전까지 지연 평가(lazy evaluation)된다.

메서드설명
filter(Predicate)조건에 맞는 요소만 필터링
map(Function)요소를 변환
flatMap(Function)다차원 → 1차원 평탄화
distinct()중복 제거
sorted()정렬 (Comparable 기반)
sorted(Comparator)정렬 (사용자 정의 정렬 기준)
limit(n)앞에서 n개만 유지
skip(n)앞에서 n개 건너뛰기
peek(Consumer)디버깅 용도로 중간 값 출력

1) 예제

List<String> names = List.of("kim", "lee", "lee", "park");

List<String> filtered = names.stream()
    .filter(n -> n.length() == 3)
    .distinct()
    .sorted()
    .collect(Collectors.toList()); // ["kim", "lee", "park"]

15.6.2. 최종 연산 (Terminal Operations)

최종 연산은 스트림을 닫고 결과를 생성한다.

메서드설명
collect()결과를 리스트, 셋 등으로 수집
count()요소 개수 반환 (long)
forEach()각 요소에 대해 작업 수행
reduce()누적 연산 수행
anyMatch()하나라도 조건 만족하면 true
allMatch()모두 조건 만족하면 true
noneMatch()모두 조건 불만족이면 true
findFirst()첫 번째 요소 반환 (Optional)
findAny()병렬일 경우 아무 요소나 반환

1) 예제

long count = names.stream().filter(n -> n.contains("k")).count();

boolean hasKim = names.stream().anyMatch(n -> n.equals("kim"));

Optional<String> first = names.stream().findFirst();

15.6.3. 수집 (Collectors)

collect()는 최종 연산이며, 결과를 다양한 형태로 반환할 수 있게 해주는 정적 메서드 집합인 Collectors 와 함께 자주 사용된다.

수집기설명
toList()리스트로 수집
toSet()집합으로 수집
toMap(keyMapper, valueMapper)맵으로 수집
joining()문자열 합치기
counting()요소 수 세기
summarizingInt()합계, 평균, 최댓값, 최솟값 등 요약 통계
groupingBy()그룹핑 (SQL의 GROUP BY)
partitioningBy()true/false 두 그룹 분리

1) 예제

Map<Integer, List<String>> grouped = names.stream()
    .collect(Collectors.groupingBy(String::length));
// {3=[kim, lee, lee], 4=[park]}

15.6.4. 고급 예제: flatMap(), reduce(), groupingBy()

flatMap() : 중첩 리스트를 평탄화

List<List<String>> data = List.of(
    List.of("a", "b"),
    List.of("c", "d")
);

List<String> flat = data.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList()); // ["a", "b", "c", "d"]

reduce() : 모든 값을 누적해서 하나로 만듦

int sum = List.of(1, 2, 3, 4).stream()
    .reduce(0, Integer::sum); // 10

groupingBy() : 조건 기준으로 그룹화

List<String> names = List.of("kim", "lee", "cho", "park");

Map<Integer, List<String>> grouped = names.stream()
    .collect(Collectors.groupingBy(String::length));
profile
Data Analytics Engineer 가 되

0개의 댓글