되자!백엔드개발자

Java(자바) - 스트림(Stream)1 : 개념 본문

개발공부/JAVA

Java(자바) - 스트림(Stream)1 : 개념

HyunJng 2022. 9. 12. 19:12

Stream의 개념


기존에는 컬렉션(or 배열)에 데이터를 담고 sort한 뒤 출력하는 등의 원하는 결과를 얻으려면 복잡하고 긴 코드를 사용해야했다. 또한 각 컬렉션 클래스에는 같은 기능의 메서드들이 중복해서 정의되어 데이터 소스마다 다른 방식으로 다뤄야했다. 예를들어 List를 정렬할 땐, Collections.sort()를 이용하고, 배열을 정렬할 떄는 Arrays.sort()를 이용해야 한다. 

이러한 문제를 해결하기 위해 Java8부터 '스트림(stream)' 기능을 추가 되었다.

스트림이란 '데이터 처리연산을 지원하도록 소스에서 추출된 연속된 요소'이다. 쉽게 말해 데이터 소스로부터 데이터를 읽어와서 내부반복으로 데이터를 처리하는 것이다.(이해가 안되면 밑에 예시를 보자) SQL의 쿼리문과 같이 Java의 컬렉션(or 배열 or 파일)을 선언형으로 처리할 수 있는 기능을 가지고 있다.

 

Stream의 장점


  • 선언형이라 코드가 더 간결해진다.
  • 스트림은 병렬처리를 별도의 멀티스레드 구현 없이도 쉽게 구현할 수 있다. <<쓰레드 공부를 제대로 안해서 안 다룸
  • 데이터 소스가 무엇이든 같은 방식으로 다룰 수 있게 되었다.

직접 예시를 보면 얼마나 코드가 간결해지는지 체감이 될 것이다.

 

예시. 리스트를 정렬한 뒤 출력하는 경우

[Stream 사용 X]

List<Apple> redApples = Arrays.asList(new Apple(90), new Apple(5), new Apple(10), new Apple(7));
// 사과를 무게 순으로 정렬
redApples.sort(Comparator.comparing(Apple::getWeight));
// 출력 방법1 : iterator사용
Iterator<Apple> it = redApples.iterator();
while (it.hasNext())
	System.out.println(it.next());

+) 다른 출력 방법

// 방법2 : for-each사용
for(Apple apple : redApples)
	System.out.println(apple);
// 방법3 : 람다 사용
redApples.forEach(System.out::println);

[스트림 사용 O]

List<Apple> redApples = Arrays.asList(new Apple(90), new Apple(5), new Apple(10), new Apple(7));

redApples.stream()
.sorted(Comparator.comparing(Apple::getWeight))
.forEach(System.out::println);

보기 쉽게 분리해놓긴 했지만, 한줄이면 끝난다.

 

스트림의 특징


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

데이터 소스로부터 '읽어'오는 것이기 때문에 스트림은 원본 데이터가 아니다. 그래서 데이터 원본을 건드리지 않지만 필요하다면 결과를 컬렉션이나 배열에 담아 원본으로 반환할 수도 있다.

 

2. 스트림은 '일회용'이다.

Iterator처럼 스트림도 일회용이기 때문에 한번 사용하면 닫혀서 다시 사용할 수 없다. 필요하면 다시 생성해야한다.

 

3. 스트림은 작업을 내부 반복으로 처리한다.

스트림을 이용한 작업이 간결할 수 있는 비결 중 하나가 바로 '내부 반복'이다. 내부반복은 반복문을 메서드 내부에 숨길 수 있다는 것을 의미한다. 위의 예시에서도 sorted()와 forEach()의 내부에는 반복문이 있어 stream 안의 모든 데이터에 적용되는 것을 확인할 수 있다.

 

4. 파이프라이닝(Pipelining) 구조이다.

[스트림 = 중간연산 + 최종연산]

파이프라이닝은 여러개의 스트림이 연결되어 있는 구조를 뜻한다. 파이프라이닝이 가능하려면 서로 연결할 수 있도록 스트림 자신을 반환해야하는데 중간연산이 그 역할을 한다.

 

스트림의 연산


스트림에서 제공하는 연산은 중간연산최종연산으로 분류할 수 있다.

중간연산 : 연산결과가 스트림인 연산. 스트림에 연속해서 중간연산을 할 수 있다.
최종연산 : 연산결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 한번만 가능하다.

이런 식으로 쓰인다.

stream.distinct().limit(5).sorted().forEach(System.out::println);
/*
중간연산: distinct(), limit(), sorted
최종연산: forEach()
*/

 

지연된 연산

스트림 연산에서 한가지 중요한점은 최종연산이 수행되기 전까지 중간 연산이 수행되지 않는다는 것이다.

중간연산은 단지 어떤 작업이 수행되어야 하는지 지정해주는 역할로 최종연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모된다. 그렇기에 반드시 최종연산을 써주어야한다.

 

Stream<Integer>과 Stream

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

Stream<integer>을 쓰는 것보다 IntStream을 사용하는 것이 더 효율적이고, IntStream에는 int값으로 작업하는데 유용한 메서드들도 포함되어있다.