본문 바로가기
Java/topic

모던 자바 인 액션 - Stream을 사용해보자

by Ellery 2023. 3. 5.

자바 8에서는 Stream API가 도입되어 데이터 처리를 간단하게 하고 가독성을 높혀서 코드 유지보수성을 높이고, 개발자의 생산성을 향상시킬 수 있다. 

Stream API를 사용하면 배열, 리스트, 파일 등 다양한 소스에서 요소를 추출하고 처리할 수 있다

스트림은 데이터 소스를 변경하지 않으며, 작업을 수행하는 중간 단계와 최종 작업으로 나뉘어진다.

이러한 중간 작업은 Lazy Evaluation 방식으로 처리되며, 최종 작업이 호출되기 전까지는 실행되지 않는다. 이러한 특징 때문에 처리 속도를 높일 수 있다. 

// 배열 스트림 예시
int[] numbers = {1, 2, 3, 4, 5};
IntStream stream = Arrays.stream(numbers);
stream.forEach(System.out::println);

// 컬렉션 스트림 예시
List<String> names = Arrays.asList("Java", "C", "Python", "JavaScript", "Ruby");
names.stream()
     .filter(name -> name.startsWith("J"))
     .map(String::toUpperCase)
     .sorted()
     .forEach(System.out::println);

자바8 Stream의 특징

1. 선언형 코드

스트림을 이용해서 선언형 코드로 컬렉션 데이터를 처리할 수 있다. (loop, if condition 을 사용해서 동작을 구현하지 않아도 됨)

따로 멀티스레드 코드를 구현하지 않아도 데이터를 병렬처리할 수 있다. stream() 대신 parallelStream() 을 사용한다

2. 파이프라이닝

스트림 연산끼리 연결해서 파이프라인을 구성할 수 있도록 스트림 자기 자신을 반환한다 - laziness, short-circuiting 최적화가 가능해짐

컨테이너 역활만 하는 중간 변수 등의 세부 구현을 스트림 라이브러리 내에서 중간연산으로 처리할 수 있다.

filter, sorted, map, collect 같은 고수준의 빌딩 블록 연산을 조립해서 복잡한 데이터처리 파이프라인을 만들어서 사용해도 가독성, 명확성이 유지되고 유연하다.

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

Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));

{
    FISH = [fish1, fish2, ...],
    OTHER = [cola, cider, ...],
    MEAT = [beef, pork, ...]
}

3. 내부 반복

내부 반복을 지원함 ↔ 컬렉션은 반복자를 이용해서 명시적으로 반복한다.

스트림, 컬렉션의 차이점

스트림과 컬렉션의 차이점은 데이터를 언제 계산하는지의 여부, 단 한번만 '소비'할 수 있다는 점의 차이가 있다

스트림의 요소는 lazy하게 계산된다. 최종연산이 실행되는 시점부터 중간연산이 실행되기 때문.

스트림을 한번 소비하면 요소들의 순서가 바뀌거나 값이 변경될 수 있어서 설계 시 단 한번만 소비할 수 있게 설계되었다.

여러번 사용시도를 하면 java.lang.IllegalStateException이 발생한다.(만약 계속 사용하고 싶다면 재할당을 하던가, 새로운 스트림 객체를 반환하는 Supplier 를 구현할 수도 있다

참고 - https://stackoverflow.com/questions/36255007/is-there-any-way-to-reuse-a-stream

컬랙션을 순회하려면 사용자가 iterator나 foreach문 등을 이용해서 명시적인 반복문을 이용해야된다.

스트림 라이브러리는 filter, map, sorted 등의 연산 내에서 반복을 추상화해서 처리하고, 결과 스트림값을 저장해주는 내부 반복을 사용한다.

이런 식의 내부반복은 외부 반복에 비해 병렬연산, 최적화 등에 더 이점이 있다.스트림의 내부반복을 이용하면 스트림 내부적으로 스트림 요소들을 여러 개의 작업으로 나눠서 병렬적으로 처리하고 결과를 결합하기 떄문에 멀티코어 관련 코드를 크게 신경쓰지 않아도 된다.

만약 멀티코어 연산을 하는 도중 외부반복문이 있다면 synchronized 등으로 처리해야되는데 병렬 연산의 이점을 완전히 날리는 방식이 된다.

만약 멀티코어 연산을 하고싶다면 스트림을 parallelStream()으로 선언하기만 하면 된다.

스트림 연산의 종류

중간연산: 연결할 수 있는 스트림 연산
빌더 패턴처럼 파이프라인을 만들 수 있음. 최종연산 없이 중간연산만로는 어떤 결과도 생성할 수 없다.

filter(Predicate<T>)
map(Function<T, R>)
limit(n)  맨 처음 n개가 담긴 Stream이 반환되기 때문에 short circuit 최적화가 적용됨
sorted(Comparator<t>)
distinct()

최종연산: 스트림을 닫는 연산. 스트림이 아닌 결과를 반환한다.

forEach  스트림의 각 요소를 소비하면서 람다를 적용한다. return void
count 스트림의 요소 갯수를 반환한다 return Long
collect  스트림을 reduce 해서 list, map, Integer 형식의 컬렉션을 반환함

 

더 많은 메서드들은 https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/Stream.html

 

Stream (Java SE 17 & JDK 17)

Type Parameters: T - the type of the stream elements All Superinterfaces: AutoCloseable, BaseStream > A sequence of elements supporting sequential and parallel aggregate operations. The following example illustrates an aggregate operation using Stream and

docs.oracle.com