Dev/Java

[Java] Stream

syuare 2025. 4. 28. 20:18

월요병..

 

Stream이란?

컬렉션이나 배열 등의 데이터를 함수형 스타일로 처리할 수 있게 해주는 데이터 파이프라인

- 반복문, 제어문 등 대신 Lambda(람다)를 사용해서 데이터 필터링, 변환, 집계 등을 간결하고 명확하게 표현할 수 있는 방법


Stream 탄생

"컬렉션(List)에 담긴 각 정수 요소 중 3보다 큰 숫자만 고르고, 해당 숫자에 10을 곱한 값을 새 리스트에 담으세요."

를 코드로 작성하면 아래와 같다.

List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));

List<Integer> ret1 = new ArrayList<>();
for (Integer num : arrayList) {
    if(num > 3) {
        Integer multipliedNum = num * 10;
        ret1.add(multipliedNum);
    }
}
System.out.println("ret1 = " + ret1); // 조회: ret1 = [40, 50]

 

해당 코드를 작성하려면 반복문(for), 조건문(if), 변환(num*10), 수집(add)를 모두 작성해야 하는데, 코드가 길고 헷갈릴 수 있다.

  • 향후 이러한 처리들을 병렬로 처리할 경우 더 고민해야하는 포인트(스레드, 동기화, 락 등) 가 많아질 것이다. 

Java가 아닌 다른 언어(JS, Python 등) 에서는

filter, map, reduce 와 같은 함수를 사용하면 "무엇을 할지"만 작성하면 알아서 반환이 된다.

// JS 예시

const arrayList = [1, 2, 3, 4, 5];

// filter: num > 3인 것만, map: 10을 곱한다
const ret1 = arrayList.filter(num => num > 3).map(num => num * 10);

console.log(ret1); // [40, 50]

 

Java에서도 반복문, 조건문, 수집 코드 등을 모두 작성하지 않고 보다 간결하게 작성을 필요로 하였고,

Lambda 표현식이 도입된 이후 "(x -> x>3 )" 과 같이 작은 함수식을 바로 작성할 수 있게 됨에 따라

fillter(), map() 같은 메서드를 통해 "무엇을 할 지"를 선언한 후 collect(), forEach 와 같은 최종 연산(명령)을 호출하면 한번에 실행되도록 할 수 있게 한 것을 "Stream" 이라고 한다.

  • 병렬 쳐리도 stream을 사용하면 parallelStream() 메서드를 통해 간편하게 처리할 수 있다.
더보기
// 기존 방식
ExecutorService exec = Executors.newFixedThreadPool(4); // 스레드/작업 청크 생성
List<Integer> result = Collections.synchronizedList(new ArrayList<>());

for (Integer num : arrayList) {
    exec.submit(() -> {
        if (num > 3) {
            result.add(num * 10);
        }
    });
}
exec.shutdown();
exec.awaitTermination(1, TimeUnit.MINUTES);


// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

// stream을 사용하면 한 줄로 작성 가능
List<Integer> ret = arrayList
    .parallelStream() // 병렬 처리 > 스레드 분배, 작업 분할, 동기화 등은 stream이 알아서 처리
    .filter(num -> num > 3)
    .map(num -> num * 10)
    .collect(Collectors.toList());

Stream 특징

  • 데이터 소스와 연산 분리: 컬렉션에서 꺼낸 데이터 흐름(stream)에 연산을 적용한다
  • 파이프라인 구조: 여러 개의 중간 연산(처리)를 차례대로 연결한 후, 최종 연산(결과 생성)을 실행한다.
  • 지연 처리: 중간 연산은 실제 결과가 필요해질 때까지 실행되지 않는다.
  • 재사용 불가: 최종 연산이 실행된 Stream은 닫혀버리기 때문에 재사용이 불가능하다.

Stream 구조

1. 스트림 생성 (Source)

  • Collection.stream()
  • Collection.parallelStream(): 병렬 처리 필요 시

2. 중간 연산

  • filter(Predicate<T>): 조건에 맞는 요소만 남긴다
  • map(Function<T,R>): 각 요소를 다른 형태로 변환한다
  • flatMap(Function<T,Stream<R>>): 1:N 매핑하여 평탄화
  • distinct(), sorter(), limit(n), skip(n) 등
  • 특징
    • 여러 중간 연산을 계속 연결할 수 있다.
    • 최종 연산 전까지 실제 데이터 처리를 미룬다 > 지연 실행 가능

3. 최종 연산

  • forEach(Consumer<T>): 각 요소를 소비한다.
  • collect(Collector): 스트림 결과를 List, Set, Map 등을 수집한다.
  • reduce(BinaryOperator<T>): 전체 요소를 하나의 값으로 집계한다.
  • count(), anyMatch(), allMatch(), findFirst() 등
  • 특징
    • 결과를 반환한다
    • 호출 즉시 Stream이 한 번에 처리된다.
// 예시 1 (위의 예시를 stream으로 표현)
List<Integer> ret2 = arrayList.stream()
    .filter(num -> num > 3)
    .map(num  -> num * 10)
    .collect(Collectors.toList()); // 조회: [40, 50]


// 예시2
List<String> words = List.of("java", "stream", "lambda", "api");

// 1) filter + map + collect
List<String> result = words.stream()
    .filter(w -> w.length() > 4)       // 길이 5 이상
    .map(String::toUpperCase)          // 대문자 변환
    .sorted()                          // 알파벳 순 정렬
    .collect(Collectors.toList());     // List로 수집

System.out.println(result);  // [LAMBDA, STREAM]

'Dev > Java' 카테고리의 다른 글

[Java] 반복문  (0) 2025.04.30
[Java] HashMap  (0) 2025.04.29
[Java] Lambda  (0) 2025.04.25
[Java] System.out.print의 진실 (feat. toString() Override)  (0) 2025.04.24
[Java] Generic  (0) 2025.04.23