JAVA8의 스트림 알아보기

adam2·2020년 2월 20일
4

자바를자바라

목록 보기
5/5
post-thumbnail

👩‍🔬스트림이란?

스트림은 자바8에 새롭게 추가된 기능으로, 선언형(sql같은 질의형)으로 데이터(컬렉션, 배열, 파일, iterate...)를 처리할 수 있다.
자바8의 함수형 패러다임의 시작으로 람다를 이용해 함수형으로 데이터 처리가 가능해졌다.

스트림을 왜 만들었을까?

지금까지 컬렉션 데이터를 잘 사용하고 있었는데 스트림을 만든 이유는 무엇일까? 그리고 스트림과 컬렉션이 어떻게 다른걸까?

쓰던거 쓰면 안될까요..?

스트림과 컬렉션

스트림과 컬렉션 모두 연속된 요소 형식의 값을 저장하는 자료구조의 인터페이스를 제공한다. 둘다 연속된 자료를 처리하기 위해 만들어진 것잇다.

둘의 가장 큰 차이점은 이 데이터를 언제 계산하느냐 이다.

컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료구조이기 때문에 어떤 계산을 한 값들을 저장하기 위해서는 모든 요소들이 컬렉션에 추가되기 전에 미리 계산이 되어야 한다. 또한 컬렉션 인터페이스를 사용하기 위해서는 외부 반복을 사용해서 사용자가 직접 요소를 반복해야 한다. (for-each 사용)

하지만 스트림은 이론적으로 요청할 때만 요소를 계산하는 고정된 자료구조이다. 그렇기 때문에 스트림에 요소를 추가하거나 제거하는 작업은 할 수 없다. (컬렉션은 가능) 그리고 내부반복을 사용하기 때문에 추출할 요소만 선언해주면 알아서 반복을 처리하게 된다.

컬렉션은 모든 영화 데이터가 이미 저장되어있는 DVD📀와 같고 스트림은 그때그때 필요한 부분을 불러 재생하는 유튜브와 같은 것이다.

외부 반복과 내부 반복

내부 반복을 사용하게 되면 작업을 병렬로 처리하거나 최적화된 순서로 처리를 할 수 있는 장점이 있다. 반면 외부적으로 반복을 하게 되면 명시적으로 컬렉션의 항목을 하나씩 가져와서 처리를 하게 되고, 이와 같은 최적화를 달성하기가 어렵다.

스트림 라이브러리의 내부 반복은 데이터 표현과 하드웨어를 활용한 병렬성 구현을 자동으로 선택하지만(Parallel()은 공유된 thread pool을 사용하기 때문에 심각한 성능장애를 일으킬 수 있다), 컬렉션에서는 병렬성을 포기하던지 synchronized를 이용해서 관리를 해주어야 한다.

이렇게 컬렉션 인터페이스와 비슷하면서도 반복자가 없는(외부반복을 하지 않는) 요구가 생겼고 그렇게 스트림이 탄생했다고 한다!

스트림 특징

  1. 데이터 구조가 아니다. 데이터의 흐름이다.

  2. 데이터를 변경하지 않고 결과를 새로운 스트림에 저장한다.

  3. 필요한 데이터만 메모리에 로드해 처리한다. (cf 컬렉션은 모든 데이터를 메모리에 로드해 처리)

  4. Iterator처럼 데이터에 1번만 접근한다. (추가적은 접근을 위해서는 스트림을 새로 생성해 접근)

왜 사용해야 할까?

  1. 가독성 향상
private static int sumIterator(List<Integer> list) {
    Iterator<Integer> it = list.iterator();
    int sum = 0;
    while (it.hasNext()) {
        int num = it.next();
        if (num > 10) {
            sum += num;
        }
    }
    return sum;
}
private static int sumStream(List<Integer> list) {
    return list.stream().filter(i -> i > 10).mapToInt(i -> i).sum();
}
  1. 손쉬운 병렬 처리

🌝 스트림 톺아보기

스트림은 크게 중간연산과 최종연산으로 나눌 수 있다. filter, map, limit 등 파이프라이닝 할 수 있는 스트림 연산을 중간 연산이라고 하고, count, collect 등 스트림을 닫는 연산을 최종연산이라고 한다.

왜 스트림은 연산을 두가지로 나누는 걸까?

방금 중간연산은 파이프라이닝을 할 수 있다고 했다. 이 말은 중간 연산자는 스트림을 반환해야한다는 것이다. 중간 연산은 스트림 파이프라인에 실행하기 전까지 아무 연산도 수행하지 않고 중간 연산을 합친 다음 합쳐진 연산을 최종 연산으로 한번에 처리하게 된다.

중간 연산으로는 어떠한 결과도 생성할 수 없기 때문에 결과를 반환하는 최종 연산이 존재하는 것이다.

    List<String> names = menu.stream()
    			.filter(d->d.getCaloriest()>300)
                          .map(d->d.getName())
                          .limit(3)
                          .collect(tpList());
    
    // filter pork
    // mappling pork
    // filter beef
    // mapping beef
    //fiter chicken
    // mapping chicken
    // names = [pork, beef, chicken]

스트림의 lazy한 생성 때문에 300칼로리가 넘는 음식이 처음에 3개만 선택되어 연산이 수행되었다. 그리고 filter 와 map은 서로 다른 연산이자만 한과정으로 병합되었다. (loop fusion) 이렇게 연산이 되는 이유는 중간연산을 합쳐 연산을 하기 때문이다.

컬렉션으로 해당 결과를 생성했다면? 칼로리가 300이상인 메뉴를 모두 찾고, 이름을 추출하고, 거기서 3개를 선택해야 했을 것이다.

중간 연산

name설명
Stream filter(Predicate)Boolean을 반환하는 함수(Predicate)를 인자로 받아 true인 요소를 포함하는 스트림 반환
Stream distinct()중복을 필터링한다
Stream limit(n)주어진 사이즈 이하의 크기를 갖는 스트림을 반환한다.
Stream skip(n)처음 요소 n개를 제외한 스트림을 반환한다.
Stream map(Function)매핑 함수의 result로 구성된 스트림 반환
Stream flatMap()매핑된 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑. map()과 달리 평면화된 스트림을 반환한다.

최종 연산

name설명
boolean allMatch(Predicate)스트림의 모든 요소가 프리디케이트와 일치하는지 검사.
any,all,noneMatch는 and, or와 같은 연산이고 결과를 찾는 즉시 실행을 종료하는 스트림 쇼트서킷이다.
boolean anyMatch(Predicate)
boolean noneMatch(Predicate)
Optional findAny()현재 스트림에서 임의의 요소를 반환.
스트림 쇼트서킷
Optional findFirst()스트림에서 첫번째 요소를 찾는다.
T reduce()모든 스트림 요소를 처리해서 값으로 도출하는 연산을 리듀싱 연산이라고 한다. reduce()는 두개의 인수를 갖는다. 초기값과 함수.
R collect()스트림을 리듀스해서 list, map, 정수 형식의 컬렉션을 만든다.
void forEach()스트림의 각 요소를 소비하면서 람다를 적용한다.
void를 반환한다.
Long count스트림의 요소 개수를 반환. long을 반환

Optional

Optional클래스는 값의 존재나 여부를 표현하는 컨테이너 클래스이다.

  • optional을 이용해 null확인 관련 버그를 피할 수 있다.
  • Optional은 값이 존재하는지 확인하고 싶고 값이 없을 때 어떻게 처리할 것인지 강제하는 기능을 제공한다.
  • isPresent()는 Optional이 값을 포함하면 참을 반환
  • ifPresent(Consumer block)은 값이 있으면 주어진 블록을 실행

map-reduce pattern

map과 reduce를 연결하는 기법으로 쉽게 병렬화 하는 특징이 있다.

    // 스트림의 요리 개수 세기
    int count = menu.stream().map(d->1).reduce(0,(a,b)-> a+b)

reduce를 이용하면 내부 반복이 추사와되면서 내부 구현에서 병렬로 reduce를 실행할 수 있다.

스트림에 대한 시선들?

stream 을 사용할때 흔히하는 실수 10가지
for-loop 를 Stream.forEach() 로 바꾸지 말아야 할 3가지 이유
Java8 Parallel Stream, 성능장애를 조심하세요!
스트림에 대해서 학습할때만 해도 스트림 짱인데? 앞으로 많이 적용해봐야겠다~~ 라고 생각했다.
그런데 성능에 대한 회의적인 시선들도 많이 있는듯 했다. 병렬처리를 하다보니 성능적인 이슈가 발생할 확률이 높아지고, 이걸 개발자 입장에서 컨트롤 하는게 어렵기도 하고, 이런 이슈를 감수하면서까지 stream을 써야하는 이유가 없다고 생각하는 것 같다.

0개의 댓글