티스토리 뷰

Java+Spring/Java

[Java]스트림(Stream)

Vagabund.Gni 2022. 7. 20. 19:47
728x90
반응형

스트림(Stream)은 <Java 8>부터 지원하기 시작한 기능이다.

 

람다를 이용해 배열과 컬렉션의 요소들을 순회하며 처리할 수 있는 반복자라고 보면 된다.

 

스트림(Stream)은 또한 '흐름'이라는 뜻을 가지고 있는데, 자바에서는 '데이터의 흐름'이라고 생각하면 편하다.

 

배열, 컬렉션에서 데이터의 흐름(스트림)을 생성해 원하는 대로 필터링하고 가공해서 결과를 얻는 로직이라는 뜻이다.

 

이 과정에서 람다가 사용되어 코드의 길이를 줄이게 된다.

 

스트림은 크게 세 가지 과정으로 나뉘는데,

 

  1. 스트림 생성하기
  2. 중간 연산 - 데이터의 가공
  3. 최종 연산 - 결과 만들기

가 그것이다.

 

과정에 들어가기에 앞서, 스트림의 특징에 대해 조금 더 알아보자.

 


스트림의 특징

 

선언형 프로그래밍

 

스트림의 가장 큰 특징은 데이터를 선언형으로 처리한다는 데 있다.

 

선언형 프로그래밍의 반대편에 있는 말은 명령형 프로그래밍으로, 

 

코드의 세부 절차와 순서를 따라가며 이해해야 사용할 수 있는 코드를 말한다.

 

즉, "무엇을" 수행하는지보다 "어떻게" 수행하는지가 중요한 프로그래밍 패러다임이다.

 

반면 선언형 프로그래밍은 반대로 "어떻게" 보다 "무엇을"에 중심을 두는데,

 

코드의 내부 동작 원리보다도 그 코드가 어떤 결과를 내는지만을 직관적으로 알 수 있다.

 

같은 기능을 가진 코드를 각각 명령형과 선언형으로 비교해 보자.

import java.util.List;

public class ImperativeProgrammingExample {
    public static void main(String[] args){
        // List에 있는 숫자들 중에서 4보다 큰 짝수의 합계 구하기
        List<Integer> numbers = List.of(1, 3, 6, 7, 8, 11);
        int sum = 0;

        for(int number : numbers){
            if(number > 4 && (number % 2 == 0)){
                sum += number;
            }
        }

        System.out.println("# 명령형 프로그래밍 : " + sum);
    }
}
import java.util.List;

public class DeclarativeProgramingExample {
    public static void main(String[] args){
        // List에 있는 숫자들 중에서 4보다 큰 짝수의 합계 구하기
        List<Integer> numbers = List.of(1, 3, 6, 7, 8, 11);

        int sum = numbers.stream()
                        .filter(number -> number > 4 && (number % 2 == 0))
                        .mapToInt(number -> number)
                        .sum();

        System.out.println("# 선언형 프로그래밍: " + sum);
    }
}

두 코드는 똑같이 14라는 결과를 반환하지만, 아래쪽이 훨씬 가독성이 좋은 것을 알 수 있다.

 

또한 람다식으로 요소를 처리하기 때문에 직관적으로 코드를 받아들일 수가 있다.

 

 

내부 반복자

 

스트림의 또 다른 특징으로는 내부 반복자를 사용한다는 것이 있다.

 

외부 반복자는 그림에서 보이는 것처럼 코드로 직접 요소들을 가져오는 것을 말한다.

 

iterator를 쓰는 while 문이나, index를 사용하는 for문이 이에 해당한다.

 

이에 반해 내부 반복자는 컬렉션 내부에서 요소들을 처리하고 순회한다.

 

따라서 어떤 방식으로 순회하는지는 컬렉션에게 맡기고, 데이터의 가공에 집중할 수 있게 된다.

 

이는 요소의 병렬 처리에도 도움이 되는데,

 

스트림의 parallel()메서드만 사용해주면 쉽게 요소를 분배시켜 병렬 작업을 할 수가 있다.

 

 

파이프라인 구성(.)

 

위의 예에서 잠깐 봤듯이, 스트림은 중간 연산(데이터의 가공)을 "."을 이용해 순차적으로 진행한다.

 

이를 파이프라인(Pipelines)이라고 하는데, 여러 개의 스트림이 연결되어 있는 구조를 지칭한다.

 

위 그림은 회원 컬렉션에서 데이터를 가공해 최종 결과를 내는 파이프라인을 나타내고 있다.

 

특이한 점은 "중간 연산은 최종 연산 시작시까지 지연된다"는 점인데, 중간 스트림을 생성해도 일단 대기상태에 머문다.

 

위의 파이프라인을 코드로 작성하면 다음과 같다.

double ageAve = list.stream() //오리지널 스트림
.filter(m-> m.getGender() == Member.MALE) //중간 연산 스트림
.mapToInt(Member::getAge) //중간 연산 스트림
.average() //최종 연산
.getAsDouble();

중간 연산 결과마다 지역 변수를 생성해주지 않아도 돼서 코드가 직관적이고 깔끔하다.

 

그렇다면 이렇게 직관적이고 깔끔한 함수형 프로그래밍 스트림은 장점만 있을까?

 

당연히 그렇지 않다. 대략적으로 다음과 같은 단점들이 존재한다.

 

  • 디버그가 어렵다 - 스트림은 한 번에 수행되기 때문에 파이프라인을 전부 확인해야 한다.
  • 재사용 불가능 - 스트림은 일회용이기 때문에 재사용이 불가능하다.
  • 데이터 양이 늘어나면 기존 반복문에 비해 성능이 떨어진다.

스트림의 가독성이 마음에 들어 자주 사용하면 효율성에 문제가 생길 수도 있다는 뜻이다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함