스트림(stream)이란 무엇인가?

Jaeho Kim·2022년 5월 6일
2

자바의정석

목록 보기
7/7
post-thumbnail

✏️ 스트림이 무엇이죠??

자바에서는 파일이나 콘솔의 입출력을 직접 다루지 않고, 스트림(stream)이라는 흐름을 통해 다룹니다. 스트림이란 실제의 입력이나 출력이 표현된 데이터의 이상화된 흐름을 의미합니다. 즉, 스트림은 운영체제에 의해 생성되는 가상의 연결 고리를 의미하며, 중간 매개자 역활을 합니다.

  • 하지만 이 스트림은 Java SE 8부터 추가된 스트림 API라는 개념과 별개의 개념입니다.

그럼 스트림이 뭔가요..?

  • 8버전부터 등장하는 스트림 API는 데이터를 추상화하여 다루므로, 다양한 방식으로 저장된 데이터를 읽고 쓰기 위한 공통된 방법을 제공합니다. 자바에서는 많은 양의 데이터를 저장하기 위해서 배열이나 컬렉션을 이용합니다. 이렇게 저장된 데이터에 접근하기 위해서는 반복문이나 반복자(iterator)를 사용하여 매번 새로운 코드를 작성해야 합니다. 필자도 항상 이런방식으로 데이터에 접근해 왔었습니다. 하지만 이렇게 작성된 코드는 길이가 너무 길고 가독성도 떨어지며, 코드의 재사용이 거의 불가능합니다. (맞지) 즉, 데이터베이스의 쿼리와 같이 정형화된 처리 패턴을 가지지 못했기에 데이터마다 다른 방법으로 접근해야만 했습니다.(맞지) 이러한 문제점을 극복하기 위해서 JAVA SE 8부터 스트림(stream) API를 도입했다고 하네요~ 실제로 학습을 하면서 람다식과 메서드참조 그리고 중간연산과 최종연산을 거치면서 데이터에 맞는 방식(Map, list 등등)으로길게 써야 했던 코드를 짧게 이용할 수 있음을 경험했습니다.

스트림 API의 특징은?

  1. 스트림은 외부 반복을 통해 작업하는 컬렉션과는 달리 내부 반복(Internal iteration)을 통해 작업을 수행합니다.
  2. 스트림은 재사용이 가능한 컬렉션과는 달리 단 한번만 사용할 수 있습니다.
  3. 스트림은 원본 데이터를 변경하지 않습니다.
  4. 스트림의 연산은 필터-맵(filter-map) 기반의 API를 사용하여 지연(lazy) 연산을 통해 성능을 최적화합니다.
  5. 스트림은 parallelStream() 메소드를 통한 손쉬운 병렬 처리를 지원합니다.

그렇다면 스트림 API의 동작 흐름은?

  1. 스트림의 생성
  2. 스트림의 중개 연산(스트림의 변환)
  3. 스트림의 최종 연산(스트림의 사용)

우리는 이 123의 순서를 한줄에 작성할 수 있음을 알 수 있습니다. 하지만 지연된연산을 통하여 최초 스트림 생성부터 중개연산을 거듭하여 최종연산까지의 과정을 지연됨을 알고 있습니다.


자 그러면 이제 스트림을 사용하는 방법에 대해 알아봐야겠죠?

  • 우리는 이제 스트림의 개념과 스트림의 작동 방식에 대해서 간략하게 알고 있어요!!

    스트림을 어떻게 생성하고, 어떻게 중간연산하며, 어떻게 최종연산 하는지 알아봐요.

스트림을 생성해보기

  • 어떤 다양한 데이터 소스에서 생성할 수 있나요?
  1. 컬렉션
  2. 배열
  3. 가변 매개변수
  4. 지정된 범위의 연속된 정수
  5. 특정 타입의 난수들
  6. 람다 표현식
  7. 파일
  8. 빈 스트림
  • 스트림 생성하기
	Stream<String> strStream1 = strList.stream();
    Stream<String> strStream2 = Arrays.stream(strArr);

이때 포인트는,
1. 스트림을 생성한다고 해도 clone과 같이 스트림은 데이터 소스를 변경하지 않는다.
2. 스트림은 일회용이다.
3. 스트림은 작업을 내부 반복으로 처리한다.(작업이 간결할 수 있는 비결중의 하나)

내부 반복이란?
반복문을 메서드 내부에 숨길 수 있다는 것을 의미힌다.

	for(String str : strList)
    	System.out.println(str);
  							↓
							↓
							↓
    stream.forEach(System.out::println); // 메서드 참조
    stream.forEach(System.out.println(str)); // 람다식

스트림의 연산[중간연산(여러번가능), 최종연산(한번가능)]

  • 스트림이 제공하는 다양한 연산을 이용해서 복잡한 작업들을 간단히 처리할 수 있다. 마치 데이터베이스에 SELECT문으로 질의(쿼리, query)하는 것과 같은 느낌이다. 스트림에 정의돈 메서드 중에서 데이터 소스를 다루는 작업을 수행하는것을 연산(operation)이라고 해요!!
	String[] strArr = { "dd", "aaa", "CC", "cc", "b" };	// 최초 문자열 데이터
    // of()를 사용하여 stream객체에 값 입력
    Stream<String> stream = Stream.of(strArr); // 문자열 배열이 소스인 스트림 생성
    // filter()를 사용하여 조건에 맞지 않는 요소 제외
    Stream<String> filteredStream = stream.filter(); // 걸러내기(중간 연산)
    // distinct()를 사용하여 중복제거
    Stream<String> distinctedStream = stream.distinct(); // 중복제거(중간 연산)
    // 직접 코드를 쳐봤는데 sort()가 없음.. 자바의 정석 오타인가?
    sorted()를 이용한 정렬
    Stream<String> sortedStream = stream.sorted(); // 정렬(중간 연산)
    Stream<String> limitedStream = stream.limit(5); // 스트림 자르기(중간 연산)
    int			   total		 = stream.count(); // 요소 개수 세기(최종 연산)

스트림의 중간 연산 목록

- Stream<T> distinct() 									중복을 제거
- Stream<T> filter(Predicate<T> predicate) 				조건에 안 맞는 요소 제외
- Stream<T> limit(long maxSize) 						스트림의 일부를 잘라냄
- Stream<T> skip(long n) 								스트림의 일부를 건너뜀
- Stream<T> peek(Consumer<T> action) 					스트림의 요소에 작업수행
- Stream<T> sorted()									스트림의 요소를 정렬
  Stream<T> sorted(Comparator<T> comparator) 		
- Stream<R>    map(Function<T,R> mapper)				스트림의 요소를 변환
  DoubleStream mapToDouble(ToDoubleFunction<T> mapper)
  IntStream    mapToInt(ToIntFunction<T> mapper)
  LongStream   mapToLong(ToLongFunction<T> mapper)
  Stream<R>    flatmap(Function<T,Stream<R>> mapper)
  DoubleStream flatMapToDouble(Function<T,DoubleStream> m)
  IntStream    flatMapToInt(Function<T,IntStream> m)
  LongStream   flatMapToLong(Function<T,LongStream> m)

스트림의 최종 연산 목록

- void forEach(Consumer<? super T> action)				 각 요소에 지정된 작업 수행
  void forEachOrdered(Consumer<? super T> action)
- long count()											 스트림의 요소의 개수 반환
- Optional<T> max(Comparator<? super T> comparator)      스트림의 최대값/최소값을 반환
  Optional<T> min(Comparator<? super T> comparator)
- Optional<T> findAny()		// 아무거나 하나				 스트림의 요소 하나를 반환
  Optional<T> findFirst()	// 첫 번째 요소				
- boolean allMatch(Predicate<T> p) // 모두 만족하는지		주어진 조건을 모든 요소가 만족시키는지, 만족시키지 않는지 확인
  boolean anyMatch(Predicate<T> p) // 하나라도 만족하는지
  boolean noneMatch(Predicate<> p) // 모두 만족하지 않는지
- Object[] toArray()
  A[]      toArray(IntFunction<A[]> generator)			  스트림의 모든 요소를 배열로 반환
- Optinal<T> reduce(BinaryOperator<T> accumulator)		  스트림의 요소를 하나씩 줄여가면서(리듀싱) 계산
  T reduce(T identity, BinaryOperator<T> accumulator)
  U reduce(U identity, BiFunction<U,T,U> accumulator,BinaryOperator<U> combiner)
- R collect(Collector<T,A,R> collector)					  스트림의 요소를 수집한다. 주로 요소를 그룹화 하거나 분할한 결과를 컬렉션에 담에 반환하는데 사용한다.

중간 연산은 map(), flatMap() / 최종 연산은 reduce(), collect()가 핵심


지연된연산

  • 위에서 설명 하였듯이, 최종연산을 하기전까지의 중간연산들은 계획일뿐 실제 실행은 최종연산시 일괄 실행된다.

Stream< Integer >와 IntStream

  • 오토박싱 & 언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림이 제공된다.
  • IntStream, LongStream, DoubleStream

    혹시 오토박싱과 언박싱이 무엇인지 기억나시나요?
    자바에는 기본타입과 Wrapper클래스가 존재합니다.

// 박싱
int i = 10;
Integer num = new Integer(i);
// 언박싱
Integer num = new Integer(10);
int i = num.intValue();

병렬 스트림

  • 스트림의 장점으로 병렬처리가 쉽다.
  • 병렬처리(Parallel Operation)란 멀티 코어 환경에서 하나의 작업을 분할해 각각의 코어가 병렬적으로 처리하는 것이다.
  • parallel() : 병렬화 <--> sequential() : 병렬화 취소
  • 이러한 병렬처리가 항상 더 빠른 결과를 얻게 해주는 것은 아니다.

✏️ 스트림 만들기

  • Collection에 stream() 정의
Stream<T> Collection.stream()

배열

  • 배열을 소스로 하는 스트림을 생성하는 메서드는 Stream과 Arrays에 static 으로 정의되어 있다.
  • of(), stream() 둘다 스트림을 생성하는데 무슨차이지..? 했는데 직접 코딩해보니 Stream에 직접 접근은 of()로 접근, Arrays로 접근할때는 stream()을 이용한다.

특정 범위의 정수

  • IntStream과 LongStream은 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range()와 rangeClosed()를 가지고 있다.
IntStream	IntStream.range(int begin, int end)
IntStream	IntStream.rangeClosed(int begin, int end)
  • range() : end 포함 x
  • rangeClosed() : end 포함

임의의 수

  • 난수를 생성하는데 사용하는 Random클래스에는 아래와 같은 인스턴스 메서드들이 포함되어 있다. 이 메서드들은 해당 타입의 난수들로 이루어진 스트림을 반환한다.
// 이 메서드들은 크기가 정해지지 않은 무한스트림을 반환한다. 
// 그러므로 limit()를 같이 사용하여 스트림의 크기를 제한해주는게 좋다.
// 하지만 아래와 같이 가인수가 있으면 제한해주지 않아도 된다.
IntStream ints(long streamSize)
LongStream longs(long streamSize)
DoubleStream doubles(long streamSize)
  • 각 무한스트림은 해당 타입에 해당하는 난수 범위를 가진다.

람다식 - iterate(), generate()

작성중입니다.

profile
Hello, World!

1개의 댓글

comment-user-thumbnail
2023년 12월 14일

쉽게 설명해주셔서 많은 참고가 됐습니다. 이후 람다식 내용이 궁금합니다.

답글 달기