[Java] Chap16 스트림과 병렬 처리

Seunghee Lee·2023년 4월 2일
0

Java

목록 보기
11/12

💡 이번 챕터는 이것이 자바다 개정판으로 정리했다. 이 책에서는 "Chap17 스트림 요소 처리"에서 이번 단원을 다루고 있다.


1. 스트림이란 ?

스트림(Stream)은 컬렉션 및 배열의 요소를 반복 처리하기 위해 사용한다.

스트림은 요소들이 하나씩 흘러가면서 처리된다는 의미를 가지고 있다.
예를 들어, List 컬렉션에서 요소를 반복 처리하기 위해 스트림을 사용한다고 하면 아래와 같이 작성할 수 있다.

Stream<String> stream = list.stream();
stream.forEach( item -> //item 처리 );
  • List 컬렉션의 stream() 메소드로 Stream 객체를 얻고,
  • foreEach() 메소드로 요소를 어떻게 처리할지를 람다식으로 제공한다.

ex) 스트림을 사용해 Set 컬렉션의 요소를 하나씩 읽고 출력해보자.

  • [ StreamExmpale.java ]
public class StreamExample {
    public static void main(String... args) {
    	//Set 컬렉션 생성
        Set<String> set = new HashSet<>();
        set.add("홍길동");
        set.add("김길동");
        set.add("이길동");

		//Stream을 이용한 요소 반복 처리
        Stream<String> stream = set.stream();	//스트림 얻기
        stream.forEach(name -> System.out.println(name));	//요소 처리 (람다식)
    }
}

💡 Stream은 Iterator와 비슷한 반복자이지만, 다음과 같은 차이점을 가지고 있다.

  • Stream은 내부 반복자이므로, 처리속도가 빠르고 병렬 처리에 효율적이다.
  • Stream은 람다식으로 다양한 요소 처리를 정의할 수 있다.
  • Stream은 중간 처리와 최종 처리를 수행하도록 파이프 라인을 형성할 수 있다.

2. 내부 반복자

스트림은 요소 처리 방법을 컬렉션 내부로 주입시켜서 요소를 반복 처리하는데, 이것을 내부 반복자라고 한다.

반대로, for문과 Iterator는 컬렉션의 요소를 컬렉션 바깥쪽으로 반복해서 가져와 처리하는데, 이것을 외부 반복자라고 한다.

  • 외부 반복자일 경우 :
    컬렉션 요소를 외부로 가져오는 코드와 처리하는 코드를 모두 개발자 코드가 가지고 있어야 한다.
  • 내부 반복자일 경우 :
    개발자 코드에서 제공한 데이터 처리 코드(람다식)를 가지고 컬렉션 내부에서 요소를 반복 처리한다.

내부 반복자는 멀티 코어 CPU를 최대한 활용하기 위해 요소들을 분배시켜 병렬 작업을 할 수 있다.

하나씩 처리하는 순차적 외부 반복자보다는 효율적으로 요소를 반복시킬 수 있다는 장점이 있다.

ex) List 컬렉션의 내부 반복자를 이용해 병렬 처리를 해보자.

  • [ ParallelStreamExample.java ]
    - parallelStream() = 병렬 처리 스트림을 얻는다.
    - forEach() = 요소 처리 방법인 람다식을 제공한다.
public class ParallelStreamExample {
    public static void main(String... args) {
        //List 컬렉션 생성
        List<String> list = new ArrayList<>();
        list.add("홍길동");
        list.add("김길동");
        list.add("신길동");
        list.add("오길동");
        list.add("이길동");

        //병렬 처리
        Stream<String> parallelStream = list.parallelStream();  //병렬 스트림 얻기
        parallelStream.forEach(name -> {    //람다식: 요소 처리 방법
            System.out.println(name + ": " + Thread.currentThread().getName());
        });
    }
}

신길동을 중심으로 2개씩 나뉘어 병렬 처리된 결과를 확인할 수 있다.


3. 중간 처리와 최종 처리

스트림은 하나 이상 연결될 수 있다. ⇒ 스트림 파이프 라인

아래 그림과 같이, 컬렉션의 오리지널 스트림 뒤에 필터링 중간 스트림이 연결될 수 있고, 그 뒤에 매핑 중간 스트림이 연결될 수 있다. 이와 같이 스트림이 연결되어 있는 것을 스트림 파이프라인(piplines)이라고 한다.

  • 중간 스트림들은 최종 처리를 위해 요소를 걸러내거나(필터링), 요소를 변환시키거나(매핑), 정렬하는 작업을 수행한다.
  • 최종 처리는 중간 처리에서 정제된 요소들을 반복하거나, 집계(카운팅, 총합, 평균) 작업을 수행한다.

ex) Student 객체를 요소로 갖는 컬렉션에서 최종 집계 처리로 score 평균을 구하는 과정을 나타내보자.

  • 오리지널 스트림 : Student 스트림
  • 중간 스트림 : score 스트림 (Student 객체를 score 로 변환)
  • 집계 처리 : 평균 계산 (변환된 스트림으로 score 평균 계산)

이것을 코드로 표현하면 다음과 같다.

//Student 스트림
Stream<Student> studentStream = list.stream();

//score 스트림
//Student객체를 getScore() 메소드의 리턴값으로 매핑
IntStream scoreStream = sudentStream.mapToInt(student -> student.getScore());

//평균 계산
double avg = scoreStream.average().getAsDouble();
  • mapToInt() = 객체를 int로 매핑해서 IntStream으로 변환시킨다. (람다식)

💡 메소드 체이닝 패턴을 이용하면 앞의 코드를 더 간결하게 작할 수 있다.

double avg2 = list.stream()
				.mapToInt(student -> student.getScore())
                .average()
                .getAsDouble();

⚠️ 스트림 파이프라인으로 구성할 때 주의할 점은 파이프라인의 맨 끝에는 반드시 최종 처리 부분이 있어야 한다는 것이다 !

※ 만약 최종 처리가 없다면 오리지널 및 중간 처리 스트림은 동작하지 않는다.

  • [ Student.java ]
public class Student {
    private String name;
    private int score;

    public Student (String name, int score) {
        this.name = name;
        this.score = score;
    }

    public String getName() {return name;}
    public int getScore() {return score;}
}
  • [ StreamPipeLineExample.java ]
public class StreamPipeLineExample {
    public static void main(String... args) {
        List<Student> list = Arrays.asList(
                new Student("홍길동", 10),
                new Student("김길동", 20),
                new Student("이길동", 30)
        );

        //방법 1 - 오리지널-중간-최종 
        Stream<Student> studentStream = list.stream();
        IntStream scoreStream = studentStream.mapToInt(student -> student.getScore());
        double avg1 = scoreStream.average().getAsDouble();

        System.out.println("avg1 = " + avg1);

        //방법 2 - 메소드 체이닝 패턴 이용
        double avg2 = list.stream()
                .mapToInt(student -> student.getScore())
                .average()
                .getAsDouble();

        System.out.println("avg2 = " + avg2);
    }
}


4. 리소스로부터 스트림 얻기

java.util.stream 패키지에는 스트림 인터페이스들이 있다. BaseStream에는 모든 스트림에서 사용할 수 있는 공통 메소드들이 정의되어 있다.

  • Stream은 객체 요소를 처리하는 스트림이다.
  • IntStream, LongStream, DoubleStream은 각 기본 타입인 int, long, double 요소를 처리하는 스트림이다.

✅ 스트림 인터페이스들의 구현 객체는 다양한 리소스로부터 얻을 수 있다.

주로 컬렉션과 배열에서 얻지만, 다음과 같은 리소스로부터 스트림 구현 객체를 얻을 수도 있다.

📌 컬렉션으로부터 스트림 얻기

java.util.Collection 인터페이스는 스트림과 parallelStream() 메소드를 가지고 있기 때문에 자식 인터페이스인 List와 Set 인터페이스를 구현한 모든 컬렉션에서 객체 스트림을 얻을 수 있다.

profile
자라나라 개발개발 ~..₩

0개의 댓글