[JAVA] Stream

buckshot·2024년 6월 17일

JAVA

목록 보기
7/18

지금까지 자바를 공부하면서 아주 갈략하게 스트림에 대하여 읽었던 기억이 있다. 하지만 이 기능을 자주 사용하지 않았다. (사실 당시에 너무 복잡해 보여서 겁이나서 안했다.)

그래서 다시 알아보고 사용하고 싶어서 정리를 한다.

🧐 Stream이란??

스트림은 자바8 API에 추가된 기능이다. 이 기능을 이용을 하면 선언형으로 컬렉션을 처리할 수 있다. 또한 멀티 쓰레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리할 수 있다.
정확하게 다시 설명을 한다면 '데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소'로 정의할 수 있다.
간단한 예제코드를 보면서 알아보겠다.

List<Dish> lowCaloricDishes = new ArrayList<>();
for(Dish dish : menu) {
    if(dish.getCalories() < 400>) {
        lowCaloricDishes.add(dish);
    }
}

Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
    public int compare(Dish dish1, Dish dish2) {
        return Integer.compare(dish1,getCalories(), dish2.getCalories());
    }
});

List<String> lowCaloricDishesName = new ArrayList<>();
for(Dish dish : lowCaloricDishes) {
    lowCaloricDishesName.add(dish.getName());
}

위 코드는 스트림 이전인 자바7의 코드이다.

이제 자바8에 추가된 Stream을 이용을 해보겠다.

List<String> lowCaloricDishName =
        menu.stream()
            .filter(d -> d.getCalories() < 400)
            .sorted(comparing(Dishes::getCalories))
            .map(Dish::getName)
            .collect(toList());

이렇게 작성을 했다.

위 코드에서 stream()parallelStream()으로 수정을 하게된다면 멀티코어 아키택처에서 병렬로 실행할 수 있다.

이렇게 스트림을 이용하게 된다면 스트림의 새로운 기능이 소프트웨어공학적으로 다양한 이득을 안겨준다.
간단하게 어떤 이득이 있는지 알아보면, 일단 선언형으로 코드를 구현할 수 있다는 점과 여러 빌딩 블록 연산을 연결하여 복잡한 데이터 처리 파이프 라인을 만들 수 있어 가독성과 명확성을 유지한다는 장점이 있다.

filter와 같은 연산은 고수준 빌딩 블록(high-level building block)으로 이루어져 있어 특정한 스레딩 모델에 제한되지 않고 자유롭게 어떤 상황에서도 사용할 수 있다.

고수준 빌딩 블록


복잡한 시스템이나 프로그래밍을 구성하는 기본 구성요소들 중 추상화 수준이 높은것들을 의미한다. 추상화, 재사용성, 조합 가능성, 가독성 이렇게 총 4가지의 특성을 갖고 있다.

처음에 이야기 했던 스트림에 대한 설명으로 '데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소'라고 했다. 이 부분에 대해서 자세히 알아 보겠다.

  • 연결된 요소
    컬렉션과 마찬가지로 스트림은 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스를 제공한다.

    컬렉션은 자료구조이므로 컬렉션에서는 시간과 공간의 복잡성과 관련된 요소 저장 및 접근 연산이 주를 이룬다.
    반면, 스트림은 표현 계산식이 주를 이룬다.

    다시 말해, 컬렉션의 주제는 데이터이고 스트림의 주제는 계산이다.

  • 소스
    스트림은 컬렉션, 배열, I/OI/O 자원 등의 데이터 제공 소스로부터 데이터를 소비한다.

  • 데이터 연산
    스트림은 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 데이터베이스와 비슷한 연산을 지원한다.

또한 스트림에는 아래와 같은 두가지의 중요한 특징이 있다.

  • 파이프라이닝
    대부분의 스트림 연산은 스트림 연산끼리 연결하여 커다란 파이프 라인을 구성할 수 있도록 스트림은 스트림 자신을 반환한다. 이러한 부분 때문에 게으름, 쇼트서킷같은 최적화도 얻을 수 있다.

  • 내부 반복
    스트림은 내부 반복을 지원한다.


스트림과 컬렉션

스트림과 컬렉션의 다른 부분을 알아보겠다.

일상의 예제로 차이점을 간단하게 설명을 하면 컬렉션은 DVD나 테이프로 생각을 하면 된다. 왜 이렇게 생각을 하냐면 전체 자료 구조가 저장되어 있어서다.

반면 인터넷을 이용하여 스트리밍은 스트림으로 생각하면 된다. 스트리밍으로 영상을 볼 때는 재생하는 부분만 몇 프레임을 미리 내려받아서 재생을 한다.

간단한 예제를 보면 스트림과 컬렉션의 차이는 데이터를 언제 계산을 하냐가 큰 관점이다. 컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료구조다.
반면, 스트림은 이론적으로 요청할 때만 요소를 계산하는 고정된 저료구조이다.

외부 반복과 내부 반복

컬렉션 인터페이스를 사용하려면 사용자가 직접 요소를 반복해야 한다.
이를 외부 반복이라고 한다. 반면에스트림 라이브러리는 내부 반복을 사용을 한다.

List<String> names = new ArrayList<>();
for(Dish dish : menu) {
    names.add(dish.getName());
}

for-each구문은 반복자를 사용하는 불편함을 어느 정도 해결해준다.
for-each를 이용하면 Iterator객체를 이용하는 것보다 더 쉽게 컬렉션을 반복할 수 있다.

List<String> names = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()) {
    Dish dish = iterator.next();
    names.add(dish.getName());
}

아래와 같이 스트림 내부 반복을 이용

List<String> names = menu.stream()
                    .map(Dish::getName)
                    .collect(toList());

스트림 라이브러리의 내부 반복은 데이터 표현과 하드웨어를 활용한 병렬성 구현을 자동으로 선택한다. 반면에 for-each 를 이용하는 외부 반복에서는 병렬성을 스스로 관리를 해야한다.

스트림의 연산

스트림 인터페이스의 연산은 크게 중간 연산최종 연산 두 가지로 구분할 수 있다.

  • 중간 연산

    중간 연산은 다른 스트림을 반환을 한다. 따라서 여러 중간 연산을 연결해서 질의를 만들 수 있다. 중간 연산의 중요한 특징은 단말 연산을 스트림 파이프라인에 실행하기 전까지 아무 연산도 수행하지 않는다는 것, 즉 게리르다는 것이다.
연산형식반환 형식연산의 인수함수 디스크립터
filter중간 연산Stream<T>Predicate<T>T -> boolean
map중간 연산Stream<R>Function<T, R>T -> R
limit중간 연산Stream<T>
sorted중간 연산Stream<T>Comparator<T>(T, T) -> int
distinct중간 연산Stream<T>
  • 최종 연산

    최종 연산은 스트림 파이프라인에서 결과를 도출한다. 보통 최종 연산에 의해 List, Interger 등 스트림 이외의 결과가 반환된다.
연산형식반환 형식목적
forEach최종 연산void스트림의 각 요소를 소비하면서 람다를 적용한다.
count최종 연산long
(generic)
스트림의 요소 개수를 반환한다.
collect최종 연산스트림을 리듀스해서 리스트, 맵, 정수 형식의 컬렉션을 만든다.

참고 : 모던 자바 인 액션

profile
let's go insane

0개의 댓글