모던 자바 인 액션 4~6장을 보며 정리한 내용
스트림이란?
데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소.
자바 8에 추가된 선언형 API
스트림과 컬렉션
컬렉션
// 컬렉션: 모든 데이터를 메모리에 미리 준비
List<Integer> collection = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> collectionResult = new ArrayList<>();
for(int i : collection) {
// 데이터가 계산된다.
if(i % 2 == 0) {
// 데이터가 추가된다. 메모리에 올라간다.
collectionResult.add(i);
}
}
컬렉션은 데이터를 저장하기 위한 자료구조이다.
컬렉션에 추가되는 데이터는 메모리에 올라가게 된다.
그렇기에 컬렉션을 사용할 때 데이터는 추가되기 전에 모든 계산이 완료된 상태여야 한다.
스트림
// 처리를 위한 데이터
List<Integer> collection = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = collection.stream() // 컬렉션을 스트림으로 변환한다.
.filter(n -> n % 2 == 0) // 중간 연산 지정
List<Integer> streamResult = stream.toList(); // 최종 연산
스트림은 데이터를 저장하지 않고, 데이터의 흐름을 처리하는 방식이다.
중간 연산을 지나며 데이터를 어떻게 처리할지 계획하고 최종 연산에서 중간 연산을 파이프라인으로 연결하여 데이터를 요소 단위로 처리한다.
요소 단위 처리 과정
데이터 "1"이 처리되는 과정
- 스트림으로 변환된 데이터 중 "1"이 선택된다.
- 중간 연산 filter 가 수행된다.
- "1"은 짝수가 아니기에 최종 연산은 수행되지 않는다.
데이터 "2"가 처리되는 과정
- 스트림으로 변환된 데이터 중 "2"가 선택된다.
- 중간 연산 filter 가 수행된다.
- "2"는 짝수이기 때문에 최종 연산 toList가 수행된다.
- 해당 연산이 처음이라면 결과를 누적할 ArrayList를 생성한다.
- 생성된 ArrayList에 "2"를 누적한다.
최종 연산 toList
toList
는 collect(Collectors.toList())
의 간소화된 메서드다.
// Collectors.toList의 자세한 구현
public static <T> Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>(
ArrayList::new, // 결과가 누적될 ArrayList 생성
List::add, // n-1까지 누적한 ArrayList에 n번째 요소를 누적한다.
(left, right) -> { left.addAll(right); return left; }, // 병렬 처리시 두 개의 ArrayList를 병합하는 방법
CH_ID // Collector의 동작 특성
);
}
스트림과 컬렉션의 차이
많은 책에서 스트림과 컬렉션의 차이로 데이터의 계산 시점을 이야기한다.
컬렉션은 각각의 요소가 작성된 코드를 따라가며 즉시 계산되고 스트림은 각각의 요소가 최종 연산을 만났을 때 계산된다.
스트림의 이러한 처리를 지연 연산이라 한다.
지연 연산의 장점
불필요한 계산 방지
필요한 경우에만 정의된 파이프라인을 통해 요소를 처리하기 때문에 불필요한 계산을 방지할 수 있다.
대표적으로 limit
와 같은 연산자를 통해 간편하게 필요한 양을 처리할 수 있다.
List<Integer> collection = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> streamResult = collection.stream()
.filter(n -> n % 2 == 0)
.limit(2) // 짝수 요소 2개까지만 처리
.toList();
코드 가독성 및 유지 보수성
그런데 위와 동일한 코드를 컬렉션을 통해서도 구현할 수 있다.
List<Integer> collection = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> collectionResult = new ArrayList<>();
for(int i : collection) {
// limit(2)와 동일한 코드
if(collectionResult.size() >= 2) {
break;
}
if(i % 2 == 0) {
collectionResult.add(i);
}
}
limit
를 사용한 코드와 동일한 코드지만 컬렉션 코드는 데이터 처리 과정을 더 복잡하게 표현하고 있다.
대량 데이터 처리 및 병렬 처리
명령형으로 코드를 작성해야 하는 컬렉션 코드는 대량 데이터 처리를 위해 병렬 처리를 수행할 때도 그 처리 과정을 복잡하게 구현해야 한다는 단점이 있다.
하지만 선언형의 스트림의 경우 paralleStream
을 통해 간단히 데이터 처리 작업을 병렬로 처리할 수 있도록 지원하고 있다.
List<Integer> collection = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> streamResult = collection.paralleStream() // 병렬 처리
.filter(n -> n % 2 == 0)
.toList();
스트림에 대한 생각
선언형 프로그래밍
선언형 프로그래밍이란 원하는 결과가 무엇(What)인지에 초점을 맞춰 표현하는 프로그래밍을 말한다.
개발자는 명령형 프로그래밍에서와 다르게 작업의 세부 절차(How)에 대해서 알 필요가 없다.
선언형 API를 사용하면 개발자가 무엇에만 집중할 수 있게 되면서 이를 잘 활용하면 간결하고 가독성이 좋은 코드를 작성할 수 있다.
성능의 측면에서도 개발자는 두 가지 선택을 할 수 있다.
우선 코드 제공자가 제공한 성능 개선 코드를 잘 활용하는 것이다.
만약 제공하는 코드를 통해 성능을 개선하지 못한다면 작업의 세부 절차를 직접 구현해야 하는 명령형으로 구현을 변경하면 된다.
스트림은 자바에서 제공하는 컬렉션을 다루기 위한 선언형 API이다.
모든 컬렉션을 스트림을 활용해 다루어야 할 필요는 없지만 스트림이 어떤 API를 제공하는지 잘 알고 그것을 잘 사용하기 위해 고민하는 것은 좋은 코드를 작성하기 위해 개발자가 고민해야 할 지점이지 않을까 생각한다.
'자바' 카테고리의 다른 글
세마포어 (0) | 2025.03.06 |
---|---|
Kotlin Data Classes (0) | 2025.03.04 |
Thread Pool 정리 (1) | 2025.01.01 |
스트림 메서드 정리 (0) | 2024.12.19 |
Thread와 Lock 관련된 개념 정리 (1) | 2024.09.28 |