스트림(Stream)

스트림은 자바 8부터 추가된 컬렉션(배열포함)의 저장 요소를 하나씩 참조해서 람다식(험수적 -스타일)으로 처리할 수 있도록 해주는 반복자이다. 스트림에 대해서 저는 전혀 몰랐었고(자랑이다…), 프로젝트를 진행하면서 구글링할때 다른 개발자분이 스트림으로 작성한 코드와 스프링 부트 책의 예제에서 잠깐 보고 말았던 것인데 따로 공부를 하면서 정말 편하게 컬렉션의 요소들을 처리 할 수 있다는게 너무 좋아서 공부를 해봤습니다.

자바7 이전까지는 List 컬렉션에서 요소를 순차적으로 처리하기 위해 Iterator 반복자를 사용하여 처리하곤 했습니다.

ex)

List<String> list = Arrays.asList("홍길동", "임준영", "자바킴");
Iterator<String> iterator = list.iterator();//반복처리를 위해 Iterator 객체를 얻어옴
while(iterator.hasNext()){ // 해당 컬렉션에 요소가 있는지 확인 있으면 true 리턴
    String name = iterator.next(); //요소를 name 변수에 저장
    System.out.println(name); 
}

위의 코드처럼 iterator.hasNext()로 컬렉션 타입에 요소가 있는지 확인하고 꺼내서 처리를 하지만 스트림을 이용하면 다음처럼 구현할 수가 있습니다.

List<String> list = Arrays.asList("홍길동", "임준영", "자바킴");
Stream<String> stream = list.stream(); //컬렉션의 stream()메소드로 스트림 객체를 얻음 
stream.forEach(name -> System.out.println(name)); //해당 메소드로 컬렉션의 요소 출력

스트림 객체의 forEach 메소드의 구현부를 보면 Consumer 함수적 인터페이스 타입의 매개값을 가집니다.
따라서, 컬렉션의 요소를 소비할 코드를 람다식으로 기술할 수 있습니다.

void forEach(Consumer<? super T> action);

스트림을 사용하기전에 함수적 인터페이스와 람다식에 대해서 간단하게 설명하자면 자바 코드를 간결하고 컬렉션의 요소를 필터링하거나 매핑해서 원하는 결과를 쉽게 집계하기 위해 만들어졌고, 이벤트 지향 프로그래밍과 병렬처리를 수행하기 위해 스트림과 마찬가지로 자바 8부터 지원하는 함수적 프로그래밍이라고 생각하면 됩니다.

  • 객체지향 언어 + 함수지향 언어 == 람다식
(s) -> System.out.println(s);  // 매개변수와 실행문으로 구성되어 있습니다.

람다식의 형태는 위의 코드처럼 매개 변수를 가진 코드 블록이지만, 런타임 시에는 구현 객체를 생성합니다.

람다식이 하나의 메소드를 정의하기 때문에 두개 이상의 추상 메소드가 선언된 인터페이스는 익명으로 구현이 불가능합니다. 그렇기 때문에 하나의 추상 메소드가 선언된 인터페이스만이 람다식의 타겟타입이 될 수 있는데, 이러한 인터페이스를 함수적 인터페이스라고 부릅니다.

자바에서 제공되는 표준 API에서 한 개의 추상 메소드를 가지는 인터페이스들은 모두 람다식을 이용해서 익명 구현 객체로 표현이 가능합니다. 자바 8부터는 빈번하게 사용되는 함수적 인터페이스는 java.util.function 표준 API 패키지로 제공합니다. 이 패키지에서 제공하는 함수적 인터페이스의 목적은 메소드 또는 생성자의 매개 타입으로 사용되어 람다식을 대입할 수 있도록 하기 위해서입니다.

스트림의 forEach 메소드의 매개값에 대해서 이해하기 위해서 뭔가를 쓰긴했는데…서론이 쓰잘대 없이 길었네요ㅎㅎ… Consumer는 함수적 인터페이스로 특징은 리턴값이 없는 accept() 메소드를 가지고 있습니다. accept()메소드는 단지 매개값을 소비하는 역할만 하는데. 소비하다는 말은 사용만 할뿐 리턴값이 없는 딱히 의미있는 건 아닙니다.

Consumer<String> consumer = t -> { t를 소비하는 실행문; }; //람다식으로 익명구현 객체 생성

대충 이런식으로 매개값을 소비합니다.

이제 본격적으로 스트림에 대해서 알아보겠습니다. 위의 Iterator를 사용한 코드와 Stream을 사용한 코드를 비교해보면 Stream을 사용하는 것이 훨씬 간결해 보입니다.

스트림의 특징

Stream은 Iterator와 비슷한 역할을 하는 반복자이지만, 람다식으로 요소 처리 코드를 제공하는 점과 내부 반복자를 사용하므로 병렬 처리가 쉽다는 점 그리고 중간 처리와 최종 처리 작업을 수행하는 점에서 많은 차이를 가지고 있습니다.

Stream이 제공하는 대부분의 요소 처리 메소드는 함수적 인터페이스 매개 타입을 가지기 때문에 람다식 또는 메소드 참조를 이용해서 요소 처리 내용을 매개값으로 전달할 수 있습니다.

아래 이해하기 쉬운 좋은 예제가 있습니다.

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

public class LamdaExpressionsExample {

    public static void main(String[] args) {

        List<Student> list = Arrays.asList(
                new Student("임준영",100),
                new Student("노성운",92)
        );

        Stream<Student> stream = list.stream(); //스트림 얻기
        stream.forEach(s -> {//List컬렉션에서 Student를 가져와 람다식의 매개값으로 제공.
            String name = s.getName();
            int score = s.getScore();
            System.out.println("학생이름: " + name + " " + "점수: " + score);
        });
    }
}
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;
    }
} 

스크린샷 2019-10-05 오전 1 11 58

스트림의 강력한 장점 중 하나는 내부 반복자 패턴이다. Iterator는 컬렉션의 요소를 가져오는 것에서부터 처리하는 것까지 모두 개발자가 작성해야 하지만, 스트림은 람다식으로 요소 처리 내용만 전달할 뿐, 반복은 컬렉션 내부에서 일어납니다. 스트림을 이용하면 코드도 간결해지지만, 무엇보다도 요소의 병렬 처리가 컬렉션 내부에서 처리되므로 일석이조의 효과를 누릴수 있습니다 ㅎㅎ.

또 다른 장점은 제가 스트림에 대해서 가장 많은 매력을 느낀점인데…
중간 처리와 최종처리가 가능하다는 점입니다!!!
이게 무슨 소리냐면 스트림은 컬렉션의 요소에 대해 중간 처리와 최종 처리를 수행할 수 있는데, 중간 처리에서는 매핑, 필터링, 정렬을 수행하고 최종처리에는 반복, 카운팅, 평균, 총합 등의 집계처리를 수행합니다.

예를 들어 학생 객체를 요소로 가지는 컬렉션이 있다고 가정해봅시다. 중간 처리에서는 학생의 점수를 뽑아내고, 최정처리에서는 점수의 평균 값을 산출합니다.

스크린샷 2019-10-05 오전 1 12 09

중간처리: 학생의 개별 점수를 뽑아낸다. (Student 객체를 점수로 매핑)

최종처리: 점수의 평균값을 산출한다.(집계)

여윽시…코드로 봐봅시다.

mport java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class MapAndReduceExample {
    public static void main(String[] args) {

        List<Student> studentList = Arrays.asList(
          new Student("임준영", 10),
          new Student("임광빈", 20),
          new Student("박찬욱", 30)
        );

        double avg = studentList.stream()
        .mapToInt(Student::getScore)
        .average()
        .getAsDouble();  //한줄로 평균을 구하는... 스트림의 위엄....

        System.out.println("평균 점수: " + avg);
        Stream<Student> stream = studentList.stream();
        stream.forEach(s -> System.out.println(s.getName()));
    }
}

스트림과 람다식을 사용하면 정말 간결하게 해당 객체의 점수를 뽑아내서 평균을 구할 수 있습니다.

List 컬렉션의 studentList변수에는 Student 타입의 객체 요소들을 가지고 있습니다.
스트림 객체를 얻어온 다음에 체인방식으로 mapToInt 메소드를 이용하여 중간처리에서 해당객체를 score 필드값으로 매핑을 합니다.

IntStream mapToInt(ToIntFunction<? super T> mapper);

마찬가지로 해당 메소드의 구현부를 보면 함수적 인터페이스를 매개값으로 받습니다.
ToIntFunction 함수적 인터페이스는 객체 T를 매개로 받아서 int 타입으로 매핑합니다.
이런식으로 컬렉션 내부적으로 스트림이 반복처리를 하고 중간처리에서 매핑한 score 필드값들로 score의 평균값을 산출하는 과정입니다. 정말 unbelievable 합니다.

스트림에서 제공하는 메소드에 대해서 아래 URL을 참조해주세요.
스트림 처리 메소드 - https://webcoding-start.tistory.com/48

'SpringFramework > JAVA' 카테고리의 다른 글

프록시 패턴  (0) 2019.11.20
Object- 1장 객체, 설계  (0) 2019.11.04
@Annotation 이란?  (0) 2019.10.05
클린코드 OOP의 장점  (0) 2019.10.05
빌더 패턴(Builder Pattern)  (0) 2019.10.05

+ Recent posts