Java

[Java] Stream

goblin- 2024. 9. 29. 23:07

자바의 Stream API는 자바 8에서 도입된 함수형 프로그래밍 개념을 지원하는 기능으로, 데이터 컬렉션(예: 배열, 리스트 등)을 효율적이고 간결하게 처리할 수 있게 해줍니다. 스트림은 데이터의 흐름을 처리하기 위한 추상화된 계층으로, 주로 데이터 필터링, 변환, 집계 같은 작업을 수행할 때 사용됩니다.

 

1. Stream의 개념

 

Stream은 데이터 소스를 기반으로 한 데이터 처리 연산의 흐름입니다. Stream은 컬렉션(예: List, Set, Map)이나 배열을 처리하는 데 사용됩니다.

스트림은 데이터를 처리할 때 원본 데이터를 변경하지 않고, 데이터 흐름을 함수형 방식으로 처리합니다. 즉, 기존 데이터를 변환하여 새로운 컬렉션을 생성하거나, 값을 집계하는 작업을 수행합니다.

 

2. Stream의 특징

 

함수형 스타일의 연산: 스트림은 함수형 프로그래밍의 핵심 기능인 맵(Map), 필터(Filter), 리듀스(Reduce) 같은 연산을 제공합니다.

지연 연산(Lazy Evaluation): 스트림의 연산은 필요할 때만 실행됩니다. 즉, 스트림에서 최종 연산(Terminal Operation)이 호출될 때까지 중간 연산은 수행되지 않습니다.

병렬 처리: 스트림은 기본적으로 순차적으로 처리되지만, **병렬 스트림(Parallel Stream)**을 사용하면 멀티코어 프로세서를 이용한 병렬 처리를 간단하게 구현할 수 있습니다.

불변성: 스트림은 원본 데이터를 변경하지 않습니다. 모든 연산은 데이터를 변환한 새로운 스트림을 반환합니다.

 

3. Stream의 주요 구성 요소

 

스트림 연산은 크게 두 가지로 나뉩니다:

 

1. 중간 연산(Intermediate Operation): 지연된 연산으로, 결과를 반환하지 않고 새로운 스트림을 반환합니다. 여러 중간 연산을 체인으로 연결할 수 있습니다.

2. 최종 연산(Terminal Operation): 즉시 실행되는 연산으로, 최종 연산이 호출될 때 스트림의 데이터 처리 흐름이 실행됩니다. 최종 연산은 스트림의 데이터 처리를 종료하고, 결과를 반환하거나, 데이터를 집계합니다.

 

Stream 연산의 예시

 

중간 연산: map(), filter(), sorted(), distinct(), limit(), skip()

최종 연산: forEach(), collect(), count(), reduce(), findFirst(), findAny()

 

4. Stream API 사용 예시

 

4.1 기본적인 Stream 사용 예시

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe", "John");

        // 중복을 제거하고 대문자로 변환한 후 출력
        List<String> uniqueNames = names.stream()
            .distinct()                  // 중복 제거
            .map(String::toUpperCase)     // 대문자로 변환
            .collect(Collectors.toList()); // 리스트로 수집

        System.out.println(uniqueNames);  // 출력: [JOHN, JANE, JACK, DOE]
    }
}

stream(): 리스트에서 스트림을 생성합니다.

distinct(): 리스트의 중복을 제거합니다.

map(): 각 요소를 대문자로 변환합니다.

collect(): 스트림의 결과를 리스트로 수집합니다.

 

5. Stream 연산의 종류

 

5.1 중간 연산 (Intermediate Operation)

 

filter(): 조건을 만족하는 요소들만을 필터링합니다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)  // 짝수만 필터링
    .collect(Collectors.toList());
System.out.println(evenNumbers);  // 출력: [2, 4]

 

map(): 요소를 변환합니다. 즉, 스트림의 각 요소에 함수를 적용하여 변환된 값을 반환합니다.

List<String> names = Arrays.asList("John", "Jane", "Jack");
List<Integer> nameLengths = names.stream()
    .map(String::length)  // 이름의 길이를 반환
    .collect(Collectors.toList());
System.out.println(nameLengths);  // 출력: [4, 4, 4]

 

sorted(): 스트림의 요소들을 정렬합니다. 기본적으로 자연 정렬(숫자, 알파벳) 기준으로 정렬하며, Comparator를 사용하여 맞춤형 정렬도 가능합니다.

List<String> names = Arrays.asList("John", "Jane", "Jack");
List<String> sortedNames = names.stream()
    .sorted()  // 기본 정렬 (알파벳 순)
    .collect(Collectors.toList());
System.out.println(sortedNames);  // 출력: [Jack, Jane, John]

 

5.2 최종 연산 (Terminal Operation)

 

collect(): 스트림의 데이터를 수집합니다. 보통 리스트으로 데이터를 모을 때 사용됩니다.

List<String> names = Arrays.asList("John", "Jane", "Jack");
List<String> collectedNames = names.stream()
    .collect(Collectors.toList());  // 스트림을 리스트로 변환

 

forEach(): 스트림의 각 요소에 대해 동작을 수행합니다. 주로 출력하거나 로그 기록을 할 때 사용됩니다.

List<String> names = Arrays.asList("John", "Jane", "Jack");
names.stream().forEach(System.out::println);  // 각 이름을 출력

 

reduce(): 스트림의 요소들을 하나의 값으로 집계할 때 사용됩니다. 예를 들어, 숫자의 합, 곱 등을 구할 수 있습니다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
    .reduce(0, (a, b) -> a + b);  // 숫자의 합
System.out.println(sum);  // 출력: 15

 

count(): 스트림의 요소 개수를 반환합니다.

List<String> names = Arrays.asList("John", "Jane", "Jack");
long count = names.stream().count();
System.out.println(count);  // 출력: 3

 

findFirst(): 스트림에서 첫 번째 요소를 반환합니다. Optional로 반환되며, 값이 없을 경우 안전하게 처리할 수 있습니다.

List<String> names = Arrays.asList("John", "Jane", "Jack");
Optional<String> first = names.stream().findFirst();
first.ifPresent(System.out::println);  // 출력: John

 

 

6. 병렬 스트림 (Parallel Stream)

 

스트림은 기본적으로 순차적으로 실행되지만, 병렬 스트림을 사용하면 병렬 처리가 가능합니다. 자바에서는 병렬 스트림을 사용하여 여러 코어를 활용해 대규모 데이터를 빠르게 처리할 수 있습니다.

 

병렬 스트림 사용 예시

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// 병렬 스트림으로 처리
numbers.parallelStream()
    .map(n -> n * 2)
    .forEach(System.out::println);

parallelStream(): 스트림을 병렬로 처리하도록 전환합니다. 병렬 처리는 멀티코어 프로세서를 활용하여 데이터 처리 속도를 높일 수 있습니다.

병렬 스트림을 사용할 때는 순서상호작용을 고려해야 하며, 잘못 사용하면 예상치 못한 결과를 초래할 수 있습니다.

 

7. Stream 사용 시 주의사항

 

1. 지연 실행(Lazy Evaluation):

스트림의 중간 연산최종 연산이 호출될 때까지 실행되지 않습니다. 즉, 최종 연산을 호출해야 데이터 처리가 이루어집니다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream().map(n -> n * 2);  // 실제로 실행되지 않음

 

 

2. 스트림은 1회성:

스트림은 한 번 사용하면 재사용할 수 없습니다. 스트림을 여러 번 사용하려면 다시 생성해야 합니다.

Stream<String> stream = names.stream();
stream.forEach(System.out::println);  // 정상 실행
stream.forEach(System.out::println);  // 오류 발생 (스트림은 이미 사용됨)

 

3. 병렬 스트림 사용 시 주의:

병렬 스트림은 멀티스레드 환경에서 실행되기 때문에, 데이터 처리 순서나 스레드 안전성이 중요한 작업에서는 신중히 사용해야 합니다.

 

8. 결론

 

Stream API는 자바에서 데이터 컬렉션을 함수형 스타일로 처리할 수 있는 강력한 도구입니다. 이를 통해 간결하고 읽기 쉬운 코드를 작성할 수 있으며, 특히 데이터 필터링, 변환, 집계 작업에서 큰 장점을 제공합니다. 스트림의 핵심은 지연 실행불변성으로, 원본 데이터를 변경하지 않고 새로운 데이터를 처리하는 방식이기 때문에, 안전하고 효율적인 데이터 처리가 가능합니다.

 

자바에서 스트림을 잘 활용하면 코드의 가독성을 높이고, 병렬 처리를 통한 성능 최적화도 쉽게 구현할 수 있습니다.