스트림이란

수박참외메론·2023년 3월 5일
0

1. 스트림이란?

1-1. 개요

자바 애플리케이션을 만들때 우리는 거의 무조건 컬랙션을 사용하여 데이터를 처리한다.
하지만 이렇게 많은 양의 데이터를 처리하다 보면,SQL 처럼 조건문을 달아서 원하는 데이터만 query 하고 싶을 때가 종종 온다.
또 데이터베이스에서는 대용량의 데이터들을 관리하는데 자동적으로 멀티스레드나 성능개선이 자체적으로 이루어지지만, 컬랙션에서는 안에 데이터를 가공(search 등등)을 할 때 직접해야 하기 때문에 대용량의 데이터를 처리하기는 힘들다.

이를 위해서 자바에서 내놓은 해결책은 스트림이다.

1-2. 스트림이란

  • 스트림은 자바 8 API 에 새로 추가된 기능이다.
  • Query 형태로 컬렉션 데이터를 처리할 수 있다.
  • 멀티스레드 코드를 구현하지 않고도 데이터를 투명하게 병렬로 처리가 가능

1-2-1. 스트림 예시

기존에 컬렉션만을 사용한 코드에서 스트림으로 바뀌는 예시를 들어보자.
많은 음식들 가운데 400칼로리 이하의 low calory 음식들만 걸러내는 코드이다.

		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(dish.getCalories(), dish2.getCalories())
            }   
        });
        List<String> lowCaloricDishesName = new ArrayList<>();
        for(Dish dish : lowCaloricDishes) {
            lowCaloricDishesName.add(dish.getName());
        }

위 코드에서는 lowCaloricDishes 라는 가비지 변수를 사용하여 컨테이너 역할을 하여 중간에 넘겨주는 역할만 수행했다. 하지만 스트림을 사용한다면 이런 가비지 변수가 필요가 없다.

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

1-2-2. 스트림의 장점

  • 선언형으로 코드를 구현할 수 있다.(선연형)
    즉 루프나 조건문을 사용하지 않고도 "저칼로리 요리만 선택하라" 같은 동작의 수행을 지정하여 코드를 작성하면 명시한대로 수행하게 된다. 이렇게 선언형 코드를 사용하면 변하는 요구사항에 쉽게 대응할 수 있다.
  • filter sorted map collect 같은 여러 빌딩 블록 연산을 연결해 복잡한 데이터처리 파이프라인을 만들 수 있다. (조립할 수 있음 : 유연성이 좋아진다)
  • 병렬화 : 성능이 좋아진다.

1-3. 스트림 사용하는 법

스트림을 정의하자면 '데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소'이다. 이 정의를 하나씩 살펴보자면
1. 연속된 요소
컬렉션과 마찬가지로 스트림은 특정 요소 형식으로 이루어진 연속된 집합의 인터페이스를 제공한다.
2. 소스
스트림은 컬렉션, 배열, I/O 자원등의 데이터 제공 소스로부터 데이터를 소비한다.
3. 데이터 처리 연산
filter sorted map collect 이런 연산들처럼 데이터베이스와 비슷한 연산을 칭함.

1-3-1. 스트림의 중요한 특징

  • 파이프라이닝(pipelining)
    대부분의 스트림 연산은 스트림 연산끼리 연결해서 거대한 파이프라인을 구성할 수 잇도록 스트림 자신을 반환한다.
  • 내부 반복
    반복자를 사용해서 명시적으로 반복하는 컬렉션과 달리 스트림은 내부 반복을 지원함

1-3-2. 스트림 사용 예시

        List<String> threeHighCaloricDishNames = 
            menu.stream()  //메뉴에서 스트림을 얻는다.
            //파이프라인 연산 만들기시작
            .filter(dish -> dish.getCalories() > 300) //첫번째로 300칼로리 이상 요리를 필터링 (중간연산)
            .map(Dish::getName) //요리명 추출 (중간연산)
            .limit(3) //선착순 3개만 선택 (중간연산)
            .collect(toList()); // 결과를 다른 리스트로 저장 (종단연산)
  • 질의를 수행할 데이터소스
  • 스트림 파이프라인을 구섣ㅇ할 중간 연산 연결
  • 스트림 파이프라인을 실행하게 결과를 만들 최종 연산
    빌더패턴과 매우 유사하다.

2. 스트림 vs 컬랙션

자바에서 제공되는 기존 컬렉션과 스트림 모두 연속된 요소 형식의 값을 저장하는 자료구조의 인터페이스를 제공한다.

2-1. 데이터를 언제 계산하는가

스트림과 컬렉션의 가장 큰 차이점은 "데이터를 언제 계산하느냐" 이다. 컬렉션은 현재 자료구조가 포함되는 모든 값을 메모리에 저장하는 자료구조이므로, 컬렉션의 요소는 컬렉션에 추가되기 전에 계산되어야 하지만, 스트림은 이론적으로는 요청할 때만 요소를 계산하는 고정된 자료구조이다. 즉, 게으르게(lazy) 만들어지는 컬렉션과 같다고 볼 수 있다.

하나의 예로 들어보면 영상을 본다고 생각해보면 Collection은 DVD에 저장된 영화, 스트림은 인터넷으로 스트리밍하는 영화에 비유할 수 있다.

2-2. 딱 한번 탐색 가능

스트림은 반복자(foreach 에 사용되는 iterator)와 같이 한번만 탐색하고 소비가 된다.
한번 더 탐색하려면, 초기 데이터 소스에서 새로운 스트림을 만들어 사용하여야 한다.

2-3. 외부반복과 내부반복

컬렉션을 사용하는 코드는 for-each 구문을 이용하여 사용하게 된다.

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

위와 같이 for-each 구문은 내부적으로 반복자를 숨겨놔 외부적으로 반복하게 되는 것이다.

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

위의 코드를 stream을 이용하면 아래와 같다.

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

내부반복의 장점은 두가지가 있다.

  • 동시에 복수의 일을 처리할 수 있다.
  • 데이터 처리를 임의적으로 최적화 할 수 있다.

외부 반복으로 일을 처리하면 위의 일들을 다 명시하며 처리해야 하기 때문에 신경써야 될 점들이 많아진다.

profile
하루하루는 성실하게 인생전체는 되는대로

0개의 댓글