본문 바로가기

Java

자바의 스트림(stream)

728x90

 

많은 수의 데이터를 다룰 때, 보통 컬렉션이나 배열에 데이터를 저장한 후 for문과 Iterator를 이용해서 데이터에 접근할 수 있었습니다. 하지만, 이렇게 작성한 코드는 너무 길기도 하고 재사용성도 떨어진다는 단점이 있습니다. 

또한, Colletions.sort()와 Arrays.sort()와 같이 각 컬렉션 클래스에는 같은 기능의 메서드들이 중복해서 정의되어 있는 단점이 있습니다. 즉, 데이터 소스마다 다른 방식으로 다루어야 한다는 점이 불편한 점입니다. 

이러한 문제점들을 해결할 수 있는 자료구조가스트림(Stream)입니다. 이번 포스팅은 이 스트림에 관해서 다루어 보았습니다. 

 

(날카로운 피드백은 환영입니다~!!)


스트림이란?

스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓은 자료구조입니다. 즉, 데이터 소스를 추상화하였기 때문에 데이터 소스가 무엇이던 상관없이 같은 방식으로 다룰 수 있게 되고, 그로 인해 코드의 재사용성이 높습니다.

아래 코드를 통해 컬렉션 클래스와 배열, 그리고 스트림의 차이를 확인하실 수 있습니다.


// 배열과 컬랙션
String[] strArr = {"aaa", "bbb", "ccc"};
List<String> strList = Arrays.asList(strArr);

// 배열과 리스트를 동일한 방법으로 스트림으로 생성 가능
Stream<String> strStream1 = strList.stream();
Stream<String> strStream2 = Arrays.stream(strArr);



// 1. sort 함수 적용 - 배열과 컬렉션
Arrays.sort(strArr);
Collections.sort(strList);

for(String str : strArr)
	System.out.println(str);
for(String str : strList)
	System.out.println(str);
    
// 2. sort 함수 적용 - stream
strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);

스트림의 특징

1. 스트림의 read-only 속성

스트림은 데이터 소스를 read-only로만 접근하고, 데이터 소스를 변경하지 않습니다. 즉, 위의 코드에서 배열이나 컬렉션은 sort() 함수를 적용하면 실제로 데이터 소스들이 리스트 내에서 정렬이 되지만, stream의 경우 sorted()를 적용하여도 실제로 데이터 소스들이 정렬되지는 않습니다. 따라서, 보통 정렬된 결과를 컬렉션이나 배열에 담아서 반환하여 사용합니다.


List<String> sortedList = strStream2.sorted().toList(); // 정렬된 결과를 List로 반환

2. 스트림은 일회용

Iterator처럼, 컬렉션의 요소를 모두 읽고 다시 사용할 수 없습니다. 필요하다면 다시 생성하여 사용해야 합니다.


   strStream1.sorted().forEach(System.out::println);
   int num = strStream1.count(); // 불가!

3. 스트림은 작업의 내부 반복

스트림은 작업을 내부 반복으로 처리함으로써 반복문을 메서드의 내부에 숨길 수 있어 코드가 간결해집니다. forEach()는 스트림에 정의된 메서드 중의 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용합니다. (실제로 forEach()는 메서드 안에 for문을 넣어서 구현되어 있습니다.)


 for(String str: strList)
        System.out.println(str);
            
strStream1.forEach(System.out::println); // 한 문장으로 위의 반복문 치환

   


스트림의 연산

스트림이 제공하는 연산은 중간 연산과 최종 연산으로 분류할 수 있습니다.

  1. 중간 연산: 연산 결과로 스트림을 반환하기 때문에 중간 연산을 연속해서 연결할 수 있습니다.
  2. 최종 연산: 연산으로 인해 스트림의 요소를 소모하므로 단 한번만 연산이 가능합니다.

 

아래 코드를 통해 중간 연산과 최종 연산을 볼 수 있습니다.


String[] strArr = {"aaaaa", "bbbb", "ccc", "dd", "e", "ff","ggg","hhhh","iiiii"};
Stream<String> strStream = Arrays.stream(strArr);

strStream.filter(str->str.length()>2) // 중간연산
                .distinct()	// 중간연산
                .sorted()	// 중간연산
                .limit(5)	// 중간연산
                .count();	// 최종연산

 


지연된 연산

스트림 연산에서 최종 연산이 수행되기 전까지 중간 연산이 수행되지 않고 지연하다가, 실제로 연산이 필요할 때 연산을  수행합니다. distinct()나 sorted()와 같은 중간 연산을 호출해도 바로 수행하지 않다가, 최종 연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모됩니다.


Int/Long/Double형 Stream

요소의 타입이 T인 스트림은 기본적으로 Stream<T>를 이용하지만, 데이터 소스의 요소를 기본형으로 다루는 스트림은 IntStream, LongStream, DoubleStream이 제공됩니다. 따라서,  이 타입의 데이터 소스를 요소로 하는 스트림을 다룰 경우에는 IntStream, LongStream, DoubleStream을 사용하는 것이 효율적입니다.


병렬 스트림

모든 스트림은 기본적으로 병렬 스트림이 아닙니다.  하지만 스트림으로 데이터를 다룰 때  parallel() 메서드를 호출해서 병렬로 처리할 수 있습니다. 그러면 스트림은 내부적으로 fork&join 프레임워크를 이용해서 자동적으로 연산을 병렬로 수행합니다. 만약, parallel()을 호출한 것을 취소하고 순차적으로 처리하고 싶다면 sequential() 메서드를 호출하면 됩니다.

 


String[] strArr = {"aaaaa", "bbbb", "ccc"};
Stream<String> strStream = Arrays.stream(strArr);
int sum = strStream.parallel().mapToInt(String::length).sum(); // 12

 


[참고자료]

남궁 성, [Java의 정석 3rd Edition], 도우출판, 2016

728x90
반응형

'Java' 카테고리의 다른 글

자바의 직렬화 (Serialization)  (0) 2023.11.10
Networking (네트워킹): IP주소와 URL  (0) 2023.11.05
자바의 람다(Lambda)  (0) 2023.11.01
volatile 키워드  (2) 2023.10.29
Lock과 Condition (await()과 signal(),signalAll())  (2) 2023.10.29