본문 바로가기
Java/람다와 스트림

스트림(stream) - 기본

by 재성스 2023. 9. 25.
반응형

스트림은 데이터 컬렉션(예: 리스트, 배열, 집합)을 함수형 프로그래밍 스타일로 처리하기 위해 주로 람다식과 메소드 레퍼런스와 같이 사용되며, 코드를 간결하게 작성하고 병렬 처리를 쉽게 수행할 수 있도록 데이터를 다루는 강력한 도구이다.

*참고* 병렬 처리란? 하나의 작업을 여러 부분으로 나누어 동시에 처리하는 컴퓨팅 기술이다. 컴퓨터의 성능을 최대한 활용해서 작업을 보다 빠르게 수행할 수 있도록 목표함.

데이터 집합에서 원하는 결과를 얻기 위해서는 일반적으로 for문이나 Iterator를 사용한다. 그러나 이러한 방식은 코드의 가독성이나 재사용성 면에서 떨어질 수 있으며, 또 다른 문제는 데이터 소스마다 각각의 방식대로 데이터를 다루고 각 컬렉션 클래스에는 같은 기능의 메서드들이 중복 정의가 되어 있다. 가령, Arrays.sort()나 Collection.sort()를 하나의 예로 들 수 있다. 

 

이러한 문제점들을 보완하기 위해 고안된 것이 '스트림(Stream)'이다. 스트림은 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓았으며, 데이터 소스를 일관성있는 방식으로 다룰 수 있게 한다. 

 


예제 코드를 통해 좀 더 자세히 알아보자.

String[] strArr = {"aaa", "bbb", "ccc"};		// 문자열 배열
List<String> strList = Arrays.asList(strArr);	// 문자열 리스트

Stream<String> strStream1 = strList.stream();	// 스트림을 생성
Stream<String> strStream2 = Arrays.stream(strArr) // 스트림을 생성

위 예제는 문자열 배열과 List가 있다고 가정했을 때, 두 데이터 소스를 기반으로 하는 스트림을 생성하는 방식에 대해서 보여준다.

 

아래 코드는 생성된 스트림의 데이터 소스를 읽어서 정렬하고 화면에 출력하는 스트림을 활용한 방식과 그렇지 않은 방식에 대한 비교이다.

// 스트림을 활용한 정렬 방식
strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);

// 그렇지 않은 방식
Array.sort(strArr);
Collections.sort(strList);

for(String str: strArr) System.out.println(str);
for(String str: strList) System.out.println(str);

예시에서 보이는 것처럼 스트림을 사용한 코드가 더 간결하고 가독성이나 재사용성이 높다는 것을 알 수 있다.


스트림은 데이터 소스를 변경하지 않는다.

위 예제 코드에서 한 가지 명심할 점은 스트림은 '데이터 소스를 읽어서' 내부적으로 정렬한 다음 출력하는 것이지, 데이터 소스 자체를 쓰는 것이 아니다. 즉, 데이터를 읽기만 하는 것 뿐, 데이터 소스의 내용을 변경하지 않지만, 읽어서 내부적으로 처리한 결과를 다른 배열이나 컬렉션에 담아서 반환하는 방법이 있다.

// 정렬된 결과를 새로운 List에 담아서 반환한다.
List<String> sortedList = strStream2.sorted().collect(Collectors.toList());

 

스트림은 일회용이다.

스트림은 일회성으로만 사용된다. 즉, 한번 사용하면 재 사용이 불가하며, 필요할 경우 새롭게 스트림을 생성해야 한다.

strStream1.sorted().forEach(System.out::println);
int numOfStr = strStream1.count()		// 에러. 스트림이 이미 닫혔음.

스트림의 연산

스트림을 활용한 코드가 간결해질 수 있는 것은 스트림 내부 처리 로직에 의해서이다. 스트림 내부 처리 로직은 데이터를 변환하거나, 필터링, 그룹화, 반복작업 등 다양한 작업을 수행하며, 이러한 처리 로직은 '중간 연산'과 '최종 연산'으로 분류된다.

 

중간 연산(Intermediate Operations)

중간 연산은 스트림의 요소를 변환하거나 필터링하는 등의 작업을 수행하는 연산이다. 연산 결과가 스트림으로 반환하기 때문에 중간 연산을 연속해서 연결할 수 있다. 연결된 중간 연산은 스트림의 최종 연산이 호출될 때까지 실제로 수행되지 않고 지연 평가를 사용하여 필요한 시점까지 연산을 미룰 수 있다. 즉, 중간 연산을 호출하는 것은 단지 어떤 작업이 수행되어야하는지를 지정해주는 것 뿐 최종 연산이 수행되어야만 연산 결과를 추출할 수 있다.

 

*참고* 지연 평가란? 최종 연산이 호출되기 전까지 실제로 연산을 수행하지 않는 동작 방식을 일컫는다. 이는 연산이 필요한 시점까지 연기되어 계산의 최적화와 효율성을 높이는데 도움을 준다.

중간 연산 목록

 

최종 연산(Terminal Operations)

연산 결과가 스트림이 아닌 연산이며, 연산 결과로 스트림의 요소를 소모하기 때문에 단 한번만 사용이 가능하다. 스트림이 아닌 연산이라는 것은, 모든 중간 연산에서는 연산 결과를 스트림으로 반환한다. 즉, 최종 연산은 연산 결과가 스트림 형태가 아니며, 스트림에서 데이터를 추출하여 스트림 자체를 닫고 그 결과를 반환한다. 따라서,  중간 연산을 거쳐 최종 연산이 수행되어야만 비로소 스트림의 데이터를 연산하고 소모할 수 있다.

 

 

기본형 타입 스트림

요소의 타입이 T인 스트림은 기본적으로 Stream<T>이지만, 오토박싱&언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 IntStream, LongStream, DoubleStream 인터페이스가 제공된다.

 

예시 코드

        // 0~11 사이의 값을 출력한다.
        IntStream.range(0, 11)
                .forEach(System.out::println);
        // of() 안에 숫자를 정렬해서 출력한다.
        LongStream
                .of(1,6,4,3,2,8,5,7)
                .sorted()
                .forEach(System.out::println);
        // of() 안에 실수를 정렬해서 출력한다.
        DoubleStream.of(3.5, 5.3,12.3,16.3,1.2)
                .sorted()
                .forEach(System.out::println);

스트림 최종 연산 목록


스트림의 예제

1. 중간 연산과 최종 연산의 비교

		// 리스트 
        List<String> list = List.of("java", "spring", "css", "react");
        
        // stream 생성
        Stream<String> stream = list.stream();

        // 메소드
        // 중간연산 : intermediate operation, 중간에 여러번
        // 최종연산 : terminal operation, 마지막 한번
        
        // 최종연산 후 stream 재사용 불가
        // 최종연산 시 중간연산을 같이 처리함

        stream.limit(5); // 중간연산
        stream.count(); // 최종연산

2, Stream의 흐름

List<String> list = List.of("ab", "de", "xy");
        list.stream()
                .map(e -> e + e) // 중간연산, 스트림의 요소를 하나씩 반환한다.
                .filter(e -> e.length() > 1) // 중간연산, 길이가 1보다 높은 요소만 반환
                .count(); // 최종연산	필터링된 요소의 개수를 카운트해서 반환한다.

3. Stream은 일회성이다.

        List<String> list = List.of("java", "css", "spring");
        Stream<String> stream = list.stream();

        stream.count();

        // 최종연산이 끝난 stream을 재사용할 수 없음
		//stream.count(); // exception 발생

        Stream<String> stream2 = list.stream();
        stream2.count();

- 요소를 소모한 stream을 재사용할 경우 exception이 발생한다.

 

4. filter().count() - 요소 카운트

        List<String> list = List.of("a","b","c","a","s");
        long count = list.stream()
                .filter( x-> x.equals("a"))
                .count();  // a = 2

5. distinct() - 중복 제거

List<Integer> list = List.of(3, 1, 2, 1, 3);
list.stream()
        .distinct()
        .forEach(System.out::println);  // 3, 1, 2 (중복 제거됨)

6. distinct().sorted() 중복 제거 후 정렬

var list = List.of(2, 2, 1, 3, 3, 4, 5, 5);
list.stream()
        .distinct()
        .sorted()
        .forEach(System.out::println);  // 1, 2, 3, 4, 5
*참고* 변수의 값에 의해 변수의 타입이 추론 가능한 경우, var로 대체할 수 있다.

 7. limit() - 번 인덱스부터 n개만 출력

var list = List.of(3, 9, 10, 11, 1, 0, -3);
list.stream()
                .limit(3)
                .forEach(System.out::println);	// 3, 9, 10

8. skip() - n만큼 스킵하기

var list = List.of(10, 9, 1, 2, 5, 3);
list.stream()
        .skip(3)
        .forEach(System.out::println);  // 2, 5, 3 출력 

9. .filter() - 2의 배수 필터링

var list = List.of(3, 1, 4, 9, 10, 33, 2);

list.stream()
        .sorted()
        .filter(e -> e % 2 == 0)   // 2의 배수만 필터링
        .forEach(System.out::println);      // 2, 4, 10

10.map() - 요소를 매개변수로 받아 원하는 결과 리턴하기

var list = List.of(3,1,2,5,7);

list.stream()
        .map(x->10)     // x는 매개변수 10은 리턴값 
        .forEach(System.out::println);  // 10, 10, 10, 10, 10

list.stream()
        .map(x-> -x)        // -x 리턴
        .forEach(System.out::println);  // -3. -1. -2, -5, -7

list.stream()
        .map(x->x*2)       // x*2 리턴
        .forEach(System.out::println);  // 6,2,4,10,14

list.stream()
        .map(x->x*x)      // x의 제곱 리턴
        .forEach(System.out::println);  // 9, 1, 4, 25, 49

11. map() - 요소를 매개변수로 받아 원하는 결과 리턴하기2

var list = List.of("java", "css", "react", "spring");

list.stream()
        .map(x -> x.length())// x.length를 반환
        .forEach(System.out::println);  // 4, 3, 5, 6

 

12. reduce() - 모든 값 더하기 

var list = List.of(5, 1, 3, 9, 11);
list.stream()
.reduce(0, (x, y)-> x+y); // 초기 값 = 0, x += y와 같음. 좌항에는 합계, 우항에는 list 요소가 대입 
// x(5) = x(0) + y(5) 
// x(6) = x(5) + y(1)
// x(9) = x(6) + y(3)
// x(18)= x(9) + y(9)
// x(29)= x(18)+ y(11)
// x = 29;
반응형

'Java > 람다와 스트림' 카테고리의 다른 글

java.util.function 패키지  (0) 2023.09.13
(람다식) 함수형 인터페이스와 메소드 레퍼런스  (0) 2023.09.05
람다식이란?  (0) 2023.09.04