item 47 : 반환타입으로는 스트림보다는 컬렉션이 낫다.
1. 스트림 도입 이전과 이후의 차이
1) 스트림 도입 이전
스트림이 도입되기 이전에 원소 시퀀스, 즉 일련의 원소를 반환하는 메서드의 반환 타입으로 Collection, Set, List과 같은 컬렉션 인터페이스나 Iterable 또는 배열을 사용했다.
일반적인 반환 타입
Collection
,Set
,List
:가장 많이 사용되는 기본 인터페이스.
반복(iteration)과 다양한 유틸리티 메서드(
size
,contains
,add
,remove
등)를 제공.
Iterable
:단순 반복을 위한 인터페이스.
컬렉션보다 간단한 인터페이스로, for-each 루프에서 사용 가능.
배열(Array):
기본 타입을 다루거나 성능이 중요한 경우 사용.
크기가 고정되고, 컬렉션 인터페이스의 유연성을 제공하지 않음.
선택 기준
반복과 컬렉션 메서드 제공 여부:
컬렉션 인터페이스가 적합: 대부분의 경우, 컬렉션을 사용.
단순 반복만 필요한 경우:
Iterable
사용.
성능 민감도:
배열을 사용하여 오버헤드를 줄임.
2) 스트림 도입 이후 : Stream
을 Iterable
로 변환 문제점
Stream
을 Iterable
로 변환 문제점Java 8에서는 스트림(Stream)이 등장하며, 원소 시퀀스를 반환할 때 선택지가 복잡해졌다.
스트림은 반복(iteration)을 지원하지 않는다. 따라서 원소 시퀀스를 반환할때 스트림을 사용하면 아래와 같이 for-each
로 반복을 수행할 수 없다.
for-each
와 같이 향상된 for 문이 가능한 컬렉션은 Iterable
인터페이스를 구현하고 있어야 하기 때문이다. Stream
인터페이스는 Iterable
인터페이스가 정의한 추상 메서드를 포함하고 정의한 방식대로 동작하지만, 확장(extend) 하지 않았기 때문에 반복이 불가능하다.
하지만 ProcessHandle.allProcesses()
는 스트림(Stream<ProcessHandle>
)을 반환하므로, 이를 for-each
에서 바로 사용할 수 없다.
해결 방법:
::iterator
를 사용: 스트림을Iterable
로 변환하여for-each
사용.collect()
로 컬렉션으로 변환:Stream
을 명시적으로List
또는Set
으로 변환하여 처리.
기존 방법: ::iterator
로 Iterable 변환
타입 추론이 불편:
IDE가 기본적으로 타입을 정확히 추론하지 못하며, 수동 캐스팅이 필요할 수 있음.
Runnable
,Executable
등 다른 타입을 추천하는 경우도 발생.
가독성 저하:
::iterator
방식은 직관적이지 않으며, 코드를 읽는 사람에게 혼란을 줄 수 있음.
그렇다면 스트림을 반복할 수 있게 하려면 어떻게 해야 할까?
2. 스트림 반복을 위한 해결법
1) 어댑터 메서드를 이용한 Stream과 Iterable 간 변환
Java에서 스트림과 Iterable 타입을 필요에 따라 변환할 수 있도록 어댑터 메서드를 제공하여 유연하게 사용할 수 있다. 이는 데이터를 처리하거나 반복하려는 상황에서 코드의 활용성을 높여준다.
어댑터 메서드 구현
iterableOf()
와streamOf()
라는 두가지 어댑터 메서드로 서로의 타입을 쉽게 오갈 수 있게 만들어 문제를 해결할 수도 있다.Collection 인터페이스는 stream() 과 Iterable 구현 모두 하기 때문에 기왕이면 Collection 이나 그 하위 타입을 반환 혹은 파라미터 타입에 사용하는 게 최선이다.
단지 컬렉션을 반환한다는 이유로 를 메모리에 올려서는 안된다.
시퀀스가 크지만, 표현 방식이 간단해질 수 있다면, 전용 컬렉션을 구현해보자.
어댑터 메서드 활용 예제 : 어댑터 메서드를 통한 Stream -> Iterable -> Stream 변환 예제
스트림 활용이 간단한 경우: 스트림을 사용하고 직접
forEach()
를 이용해 데이터를 처리하자.Iterable 타입을 반드시 사용해야 하는 경우: 어댑터 메서드를 이용하여 스트림을 Iterable로 변환하거나 컬렉션으로 수집(
collect()
)하는 방법을 고려하자.
2) 컬렉션이 더 적합한 경우: 멱집합 구하기
멱집합(Power Set)
은 모든 부분 집합을 원소로 가지는 집합을 말한다. 이 경우 모든 데이터를 메모리에 올리기보다는 필요한 시점에 데이터를 제공하는 것이 더 효율적이다.
멱집합을 위한 전용 컬렉션 구현
입력 집합의 원소 수가 30을 넘으면 PowerSet.of가 예외를 던진다. 이는 Stream이나 Iterable이 아닌 Collection을 반환 타입으로 쓸 때의 단점을 잘 보여준다. 다시 말해, Collection의 size 메서드가 int 값을 반환하므로 PowerSet.of가 반환되는 시퀀스의 최대 길이는 Integer.MAX_VALUE 혹은 2^n-1로 제한된다. Collection 명세에 따르면 컬렉션이 더 크거나 심지어 무한대일 때 size가 2^n-1을 반환해도 되지만 완전히 만족 스러운 해법은 아니다.
멱집합을 구해야 하는 경우엔 굳이 항상 모든 컬렉션 요소를 메모리상에 올리고 있을 필요는 없다.
get() 메서드
를 통해 필요한 시점에 필요한 엘리먼트를 얻으면 된다.모든 요소를 가지고 있기엔 2^length만큼의 공간을 확보해야 하는 부담이 있다.
모든 요소를 가지고 있으면 매번 변경사항이 생길 때마다 멱집합을 새로 구해야 한다.
AbstractCollection 은 contains() 와 size() 만 구현해주면 구현 조건이 충족된다.
이 경우가 Collection 을 반환하기 적당한 형태다.
Stream 은 size 를 구할 수 없기 때문에 이 경우에 적합하지 않다.
큰 시퀀스를 처리할 때: 모든 데이터를 메모리에 올리기보다 필요한 시점에 동적으로 생성하도록 설계하자.
컬렉션을 사용하는 이유: 멱집합과 같이 데이터 재사용이 중요하거나, 반복(iteration)이 많이 발생하는 경우에 적합하다.
Stream에 size()
또는 length
가 없는 이유
게으른 평가로 인해 전체 데이터의 크기를 알 수 없기 때문
게으른 평가로 인해 정확한 크기 예측 불가:
스트림은 데이터를 필요할 때만 게으르게 평가한다.
평가가 이루어지기 전까지, 데이터가 몇 개의 원소를 포함할지 알 수 없기 때문에, 정확한 크기를 반환하는
size()
와 같은 메서드를 제공할 수 없다.
데이터 변환의 불확실성:
스트림은 데이터 변환을 여러 단계에 걸쳐 수행할 수 있다.
변환 과정에서 필터링되거나 추가되는 데이터가 있을 수 있기 때문에, 최종 크기를 예측하기 어렵다.
대신
count()
메서드 제공:스트림은
size()
대신count()
메서드를 제공한다.count()
메서드는 스트림의 원소 개수를 계산하여 반환하지만, 스트림을 소모한다. 즉, 데이터를 한 번 소비하여 개수를 계산하기 때문에 이후에는 해당 스트림을 다시 사용할 수 없다.
count()
사용 예제
count()
사용 예제count()
를 사용하여 스트림의 개수를 계산할 수 있지만, 이 과정에서 스트림이 소모되기 때문에 이후에 동일한 스트림을 다시 사용할 수 없다.
3) 스트림이 더 적합한 경우: 부분 리스트 생성
리스트의 부분 리스트를 생성하고 처리하는 작업은 스트림을 사용하면 간결하고 가독성이 좋다.
부분 리스트 스트림 생성
부분 리스트 스트림 활용 예제
데이터 변환이나 필터링이 주요한 경우, 스트림을 사용하여 데이터를 처리
부분 리스트와 같은 반복이 자연스러운 경우 스트림이 가독성과 성능 모두에서 더 유리다.
4) 추가 코드 예제와 주석
스트림을 이용한 간단한 필터링 및 변환
컬렉션을 사용한 재사용 가능한 데이터 처리
컬렉션 사용 이유: 데이터를 여러 번 반복하거나 재사용해야 할 때 컬렉션이 더 적합하다.
스트림은 일회성 사용이 기본이기 때문에 반복 사용이 필요한 경우 컬렉션이 더 효율적이다.
📚 핵심 정리
Stream 은 반환 타입으로 사용하기보다는 단순히 컬렉션 처리를 위해 사용하는 것이 좋다. 반환은 다시 컬렉션으로 변경해주는 것이 활용성이 좋다. someStream.collect(Collectors.toList())
와 같은 함수를 이용하면 쉽다.
반환 전부터 이미 원소들을 컬렉션에 담아 관리하고 있거나 원소 개수가 적다면 ArrayList
과 같은 표준 컬렉션에 담아 반환하자. 그렇지 않다면, 전용 컬렉션을 구현할 수도 있다. 컬렉션을 반환하는게 불가능하다면 스트림과 Iterable
중 더 자연스러운 것을 반환하면 된다.
컬렉션은 반복과 재사용에 적합하고, 스트림은 변환과 처리에 더 강력
스트림은 나름대로의 장단점이 있어서, 경우에 맞게 사용하는 것이 좋다.
가장 큰 장점이자 단점이
지연 평가
가 된다는 것이다.기본적으로는 컬렉션을 반환하는 게 유연하다. 가능한 경우 컬렉션을 반환하여
stream()
과for-each
를 모두 지원하도록 하고, 데이터가 크거나 일회성이라면 스트림을 반환하는 것이 좋다.
컬렉션 사용 추천 시점:
데이터가 여러 번 반복되거나 재사용이 필요할 때.
데이터의 전체 크기를 파악해야 할 때.
스트림 사용 추천 시점:
데이터 변환 및 필터링이 주요 작업일 때.
지연 실행을 통해 효율적으로 데이터를 처리해야 할 때.
📚 참고 : 스트림에서 평가란?
1) 스트림에서 평가(Evaluation)란?
평가(Evaluation)는 스트림이 연산을 끝내고 결과를 반환하는 과정을 의미한다. 스트림은 지연 실행(Lazy Evaluation)을 통해 필요한 데이터만 처리하며, 최종 연산(Terminal Operation)이 호출되기 전까지는 데이터 처리를 수행하지 않는다. 평가 단계에서 스트림은 더 이상 스트림 타입(Stream)이 아니며, 결과를 특정 자바 객체로 반환한다.
평가의 의미
평가는 스트림 연산이 완료되어 결과가 반환되는 상태를 말한다. 다음과 같은 최종 연산(Terminal Operation)을 통해 평가가 이루어진다:
데이터를 컬렉션, 배열 등으로 변환:
예:
toArray()
,collect()
연산 결과를 하나의 값으로 축소:
예:
reduce()
데이터를 소비하거나 출력:
예:
forEach()
단일 조건을 평가:
예:
findFirst()
,anyMatch()
,allMatch()
2) 평가의 특징
스트림은 평가 전까지 실행되지 않는다 (지연 실행)
중간 연산(Intermediate Operation)
은 데이터를 변환하는 작업을 정의하지만, 실제로 데이터를 처리하지는 않는다.스트림은 최종 연산(Terminal Operation)이 호출될 때만 실행된다.
예제: 지연 실행
filter
는 중간 연산으로 정의만 되었으며, 실제로 데이터는 처리되지 않는다.collect
가 호출되면서 스트림이 평가되고 결과가 반환된다.
단락 조건 (Short-Circuiting Condition)
일부 최종 연산은 모든 데이터를 처리하지 않고도 결과를 반환할 수 있다.
단락 조건은 데이터 처리가 완료될 수 있는 조건을 말한다.
단락 조건이 적용되는 연산
findFirst()
첫 번째 요소를 찾으면 스트림 처리가 종료됩니다.
stream.findFirst()
anyMatch()
조건을 만족하는 요소를 하나라도 찾으면 처리 종료.
stream.anyMatch(x -> x > 10)
allMatch()
조건이 모든 요소에 만족하지 않을 경우 즉시 처리 종료.
stream.allMatch(x -> x > 0)
noneMatch()
조건을 만족하는 요소가 하나도 없을 경우 처리 종료.
stream.noneMatch(x -> x < 0)
limit(n)
최대 n
개의 요소만 처리하고 평가를 종료.
stream.limit(5).collect(Collectors.toList())
단락 조건 예제
findFirst
는 조건에 맞는 첫 번째 요소를 찾으면 스트림 처리를 종료한다.
3. 평가가 발생하는 최종 연산
최종 연산은 스트림을 평가하며, 결과를 반환하거나 데이터 소비를 완료합니다.
3-1. 컬렉션으로 변환
toList()
: 스트림 데이터를List
로 변환.toSet()
: 스트림 데이터를Set
으로 변환.toMap()
: 키-값 매핑을 기반으로Map
으로 변환.
예제
3-2. 축소 (Reduction)
스트림 데이터를 하나의 값으로 축소.
예:
reduce()
,count()
,max()
,min()
.
예제
3-3. 조건 검사
데이터가 특정 조건을 만족하는지 검사.
예:
anyMatch()
,allMatch()
,noneMatch()
.
예제
3-4. 데이터 소비
데이터를 순회하며 소비.
예:
forEach()
,forEachOrdered()
.
예제
4. 평가와 병렬 스트림
스트림은 기본적으로 순차 처리를 수행하지만, 병렬 스트림을 사용하면 여러 데이터 요소를 병렬로 처리할 수 있습니다. 평가 단계에서도 병렬 처리가 가능합니다.
병렬 스트림 평가 예제
병렬 스트림은 데이터 요소를 병렬로 처리하며, 최종 결과를 결합합니다.
평가 과정에서도 병렬 처리가 가능하므로 대용량 데이터 처리에 유리합니다.
5. 요약
평가(Evaluation): 스트림 데이터가 최종 연산을 통해 특정 자바 객체로 변환되는 과정.
평가 발생 조건: 최종 연산이 호출될 때만 평가가 이루어짐.
단락 조건: 특정 조건에 따라 스트림 처리가 중단될 수 있음 (
findFirst
,anyMatch
등).지연 실행: 스트림은 평가 전까지 실행되지 않으며, 정의만 이루어짐.
최종 연산 종류:
데이터 소비:
forEach
데이터 변환:
collect
,toList
,toMap
데이터 축소:
reduce
,count
,max
,min
조건 검사:
anyMatch
,allMatch
,findFirst
스트림 평가를 잘 활용하면 대규모 데이터를 효율적으로 처리하고, 가독성과 성능을 모두 높일 수 있습니다.
출처 및 참고
Last updated