JAVA - Stream

최정환·2023년 4월 2일
0

Practice JAVA

목록 보기
10/10

Stream

스트림은 병렬 또는 순차적으로 처리할 수 있는 요소 시퀀스(원소를 일렬로 늘어놓은 것)입니다.
스트림은 컬렉션, 배열, 파일 또는 난수와 같은 다양한 소스에서 만들 수 있습니다.
스트림은 map, reduce, filter, sort 등과 같은 요소에 대한 기능적 스타일 작업을 지원합니다.
스트림은 또한 지연(Lazy)되어 필요할 때만 요소를 계산합니다.

⭐️
스트림은 데이터를 저장하지 않고 액세스하고 처리하는 방법만 제공합니다.(원본 데이터를 변경 ❌)
스트림은 재사용할 수 없습니다. => 사용이 끝나면 GC 대상이 됨
데이터를 한 번에 하나씩 처리합니다.
병렬 처리를 지원합니다.

💡 일반 기능 작업을 왜 사용하지 않고 stream을 태울까요?

map 메서드를 예로 들면 stream.map과 map은 모두 스트림의 요소를 다른 형태로 변환하는 연산입니다. 하지만 stream.map은 스트림의 중간 연산으로, 스트림을 리턴하고 지연되어 수행됩니다. 반면에 map은 컬렉션의 메소드로 컬렉션을 리턴하고 즉시 수행됩니다

List<String> list = Arrays.asList("a", "b", "c");
// stream.map 사용
list.stream()
    .map(String::toUpperCase) // 스트림의 요소를 대문자로 변환하는 스트림 객체 생성
    .forEach(System.out::println); // 최종 연산으로 요소 출력
// map 사용
list = list.map(String::toUpperCase); // 리스트의 요소를 대문자로 변환하는 새로운 리스트 생성
System.out.println(list); // 리스트 출력

위의 코드에서 stream.map은 스트림의 중간 연산으로, 스트림을 리턴하고 지연되어 수행됩니다. 즉, forEach() 메소드가 호출되기 전까지는 실제로 요소를 변환하지 않습니다. 반면에 map은 리스트의 메소드로, 리스트를 리턴하고 즉시 수행됩니다.
즉, map() 메소드가 호출되면 바로 요소를 변환하고 새로운 리스트를 생성합니다.

따라서 stream.map과 map의 차이점은 다음과 같습니다.


stream.map은 스트림을 리턴하고 지연되어 수행되며, 다른 중간 연산이나 최종 연산과 연결할 수 있습니다.
map은 컬렉션을 리턴하고 즉시 수행되며, 다른 컬렉션 메소드와 연결할 수 있습니다.

1️⃣ 일반 스트림

1. 컬렉션에서 스트림 생성하기
List<String> list = Arrays.asList("a", "b", "c"); // List 생성
Stream<String> stream = list.stream(); // stream() 메소드로 스트림 객체 생성
stream.forEach(System.out::println); // forEach() 메소드로 요소 출력

2. 배열에서 스트림 생성하기
int[] arr = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(arr); // Arrays.stream() 메소드로 스트림 객체 생성
intStream.forEach(System.out::println); // forEach() 메소드로 요소 출력

3. 빌더를 이용해서 스트림 생성하기
Stream<String> builderStream = Stream.<String>builder()
    .add("Apple")
    .add("Banana")
    .add("Melon")
    .build(); // builder() 메소드로 빌더 객체 생성 후 add() 메소드로 요소 추가하고 build() 메소드로 스트림 객체 생성
builderStream.forEach(System.out::println); // forEach() 메소드로 요소 출력

4. generate() 메소드를 이용해서 스트림 생성하기
Stream<String> generateStream = Stream.generate(() -> "Hello")
    .limit(5); // generate() 메소드에 Supplier 함수형 인터페이스를 구현한 람다식을 인자로 주어서 "Hello" 문자열을 무한히 생성하는 스트림 객체 생성 후 limit() 메소드로 5개만 제한
generateStream.forEach(System.out::println); // forEach() 메소드로 요소 출력

5. iterate() 메소드를 이용해서 스트림 생성하기
Stream<Integer> iterateStream =Stream.iterate(100, n -> n + 10)
    .limit(5); // iterate() 메소드에 UnaryOperator 함수형 인터페이스를 구현한 람다식을 인자로 주어서 100부터 10씩 증가하는 수열을 생성하는 스트림 객체 생성 후 limit() 메소드로 5개만 제한
iterateStream.forEach(System.out::println); // forEach() 메소드로 요소 출력

6. empty() 메소드를 이용해서 빈 스트림 생성하기
Stream<String> emptyStream = Stream.empty(); // empty() 메소드로 빈 스트림 객체 생성
System.out.println(emptyStream.count()); // count() 메소드로 요소 개수 출력 (0)

7. 기본 타입 스트림 생성하기
IntStream intStream = IntStream.range(1, 10); // range() 메소드로 1부터 9까지의 정수를 생성하는 스트림 객체 생성
intStream.forEach(System.out::println); // forEach() 메소드로 요소 출력

2️⃣ 연산 스트림

1. filter 연산 
List<String> names = Arrays.asList("Jun", "James", "Chris", "Uni");
names.stream()
    .filter(name -> name.startsWith("J")) // filter() 메소드에 Predicate 함수형 인터페이스를 구현한 람다식을 인자로 주어서 "J"로 시작하는 이름만 걸러내는 스트림 객체 생성
    .forEach(System.out::println); // forEach() 메소드로 요소 출력
    
2. map 연산
List<String> fruits = Arrays.asList("apple", "banana", "melon", "grape");
fruits.stream()
    .map(String::toUpperCase) // map() 메소드에 Function 함수형 인터페이스를 구현한 람다식을 인자로 주어서 각 요소를 대문자로 변환하는 스트림 객체 생성
    .forEach(System.out::println); // forEach() 메소드로 요소 출력

3. sorted 연산 
List<Integer> numbers = Arrays.asList(5, 3, 1, 4, 2);
numbers.stream()
    .sorted() // sorted() 메소드로 자연 순서대로 정렬하는 스트림 객체 생성
    .forEach(System.out::println); // forEach() 메소드로 요소 출력

4. 집계(count, sum, average, min, max) 연산 
int[] scores = {80, 90, 100, 70, 85};
IntSummaryStatistics stats = Arrays.stream(scores)
    .summaryStatistics(); // summaryStatistics() 메소드로 IntSummaryStatistics 객체 생성
System.out.println("개수: " + stats.getCount()); // getCount() 메소드로 요소 개수 출력
System.out.println("합계: " + stats.getSum()); // getSum() 메소드로 요소 합계 출력
System.out.println("평균: " + stats.getAverage()); // getAverage() 메소드로 요소 평균 출력
System.out.println("최대: " + stats.getMax()); // getMax() 메소드로 요소 최대값 출력
System.out.println("최소: " + stats.getMin()); // getMin() 메소드로 요소 최솟값 출력

5. 매칭(anyMatch, allMatch, noneMatch) 연산 
List<String> colors = Arrays.asList("red", "blue", "green", "yellow");
boolean result1 = colors.stream()
    .anyMatch(color -> color.equals("red")); // anyMatch() 메소드에 Predicate 함수형 인터페이스를 구현한 람다식을 인자로 주어서 적어도 하나의 요소가 조건에 만족하는지 검사 (true)
boolean result2 = colors.stream()
    .allMatch(color -> color.length() == 4); // allMatch() 메소드에 Predicate 함수형 인터페이스를 구현한 람다식을 인자로 주어서 모든 요소가 조건에 만족하는지 검사 (true)
boolean result3 = colors.stream()
    .noneMatch(color -> color.startsWith("p")); // noneMatch() 메소드에 Predicate 함수형 인터페이스를 구현한 람다식을 인자로 주어서 모든 요소가 조건에 만족하지 않는지 검사 (true)
System.out.println(result1); // true 출력
System.out.println(result2); // true 출력
System.out.println(result3); // true 출력

6. reduce 연산 
List<String> animals = Arrays.asList("dog", "cat", "bird", "fish");
String result4 = animals.stream()
    .reduce("", (a, b) -> a + b); // reduce() 메소드에 초기값과 BinaryOperator 함수형 인터페이스를 구현한 람다식을 인자로 주어서 모든 요소를 결합하는 스트림 객체 생성
System.out.println(result4); // dogcatbirdfish 출력

7. collect 연산
List<String> fruits = Arrays.asList("apple", "banana", "melon", "grape");
List<String> result5 = fruits.stream()
    .filter(fruit -> fruit.length() == 5) // filter() 메소드로 길이가 5인 과일만 걸러내는 스트림 객체 생성
    .collect(Collectors.toList()); // collect() 메소드에 Collectors.toList() 메소드를 인자로 주어서 List 컬렉션으로 변환하는 스트림 객체 생성
result5.forEach(System.out::println); // apple, grape 출력

⭐️ 병렬 처리

병렬 처리(Parallel Processing)는 한 번에 여러 작업을 처리하는 것을 말합니다.
이는 대용량 데이터 처리에 있어서 매우 유용하며, 스트림 API에서도 병렬 처리를 지원합니다.

스트림을 사용할 때 parallel() 메소드를 호출하면 내부적으로 ForkJoin 프레임워크가 사용되며, 이를 통해 여러 개의 스레드가 생성되어 병렬 처리가 이루어집니다.

import java.util.Arrays;

public class ParallelStreamExample {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

        long start = System.currentTimeMillis();

        // 스트림을 이용한 짝수 개수 구하기
        long count = Arrays.stream(numbers)
                .parallel() // 병렬 처리를 위해 parallel() 메서드를 사용합니다.
                .filter(n -> n % 2 == 0)
                .count();

        long end = System.currentTimeMillis();
        System.out.println("짝수의 개수: " + count);
        System.out.println("걸린 시간(ms): " + (end - start));
    }
}

☕️ 카페에서 여러 손님이 커피를 주문한다고 가정해보겠습니다.
만약에 그냥 한 명의 점원이 모든 주문을 처리하려면, 손님들은 오래 기다려야 하게 됩니다.

이때, 병렬 처리를 이용하여 여러 명의 점원이 동시에 주문을 처리한다면, 손님들이 기다리는 시간이 줄어들고 더 빠른 서비스를 제공할 수 있습니다.

이러한 예를 자바 스트림에 적용해보면, 리스트에서 여러 개의 요소를 처리해야 하는 상황에서, 병렬 처리를 이용하여 리스트의 각 요소를 병렬적으로 처리할 수 있습니다.

🔑 자바는 멀티 코어 CPU를 활용하여 각각의 코어에 작업을 할당하며, 작업이 분할되어 각각의 스레드에서 병렬적으로 처리됩니다. 이렇게 병렬 처리를 이용하면, 전체 작업이 빠르게 처리될 수 있습니다.

⭐️ 하지만, 병렬 처리를 이용하는 경우, 스레드 간에 작업이 분할되어 처리되므로, 작업이 분할될 때마다 스레드 간에 데이터를 공유해야 합니다.
이때, 데이터를 공유하는 과정에서 동기화 문제가 발생할 수 있으므로, 적절한 동기화 처리가 필요합니다.
따라서, 병렬 처리를 이용할 때에는 스레드 간에 데이터를 공유할 때에 대한 적절한 처리가 필요합니다.

즉, parallel() 메소드를 호출하면 자바는 내부적으로 병렬 처리를 위해 여러 개의 스레드를 사용하며, 각각의 스레드는 리스트를 나눠서 작업을 수행합니다.
이렇게 병렬 처리를 하면 일반적으로 단일 스레드로 처리할 때보다 처리 속도가 빨라지는 경우가 있습니다.

❌ 하지만, 병렬 처리를 사용하는 경우에도 이에 대한 부가적인 비용이 들어갈 수 있으므로, 리스트의 크기와 작업의 성격에 따라 병렬 처리가 적합한지를 고려해야 합니다.
또한, 스트림의 처리 순서가 중요한 경우에는 병렬 처리를 사용하지 않아야 합니다.

0개의 댓글