[Java] 컬렉션과 스트림
스트림은 데이터의 흐름을 나타내고, 흐름에 다양한 연산을 수행한다.
선언형으로 컬렉션 데이터를 처리할 수 있고, 이 작업을 병렬로 처리할 수 있다.
List<String> expensiveCarNames = cars.stream()
.filter(car -> car.getPrice() > 1000)
.sorted(Comparator.comparing(Car::getPrice))
.map(Car::getName)
.collect(Collectors.toList());
List<String> expensiveCarNames2 = cars.parallelStream()
.filter(car -> car.getPrice() > 1000)
.sorted(Comparator.comparing(Car::getPrice))
.map(Car::getName)
.collect(Collectors.toList());
public class Car {
public int price;
public String name;
public String color;
}
stream을 사용하기 전에는 if 구문을 사용해 특정 가격 이상인 객체만 가져오는 식으로 처리하고, 컬렉션을 두 개 만들어서 이름을 반환하도록 작성해야 했지만 stream을 사용하면 제어문을 사용하지 않고 선언형으로 작성할 수 있어 변화에 대응하기 쉽다.
이 외에도 예시에서 확인할 수 있듯 메서드 체인 방식으로 객체를 생성할 수 있어 유연하게 변경할 수 있고, 데이터 처리를 병렬로 처리할 수 있어 성능을 향상시킬수 있다는 장점을 가진다.
Stream은 데이터 처리 연산을 지원하기 위해 소스 (컬렉션, 배열 등..) 에서 추출된 연속된 요소 (계산식) 이다.
컬렉션에서 스트림을 얻고, 파이프라인으로 구성된 데이터 처리 연산을 수행해 스트림을 반환한다.
이후 스트림을 통해 계산된 결과는 .collect 연산으로 컬렉션으로 반환된다.
즉, 스트림 자신을 반환하는 스트림 연산을 통해 파이프라인을 구성하고 마지막 collect로 스트림을 리스트로 변환한다.
스트림 연산은 마치 SQL의 WHERE 조건으로 데이터를 분류하는 작업과 비슷하다.
컬렉션과 스트림은 데이터를 언제 계산하는가 와 반복에서 차이가 발생한다.
컬렉션은 계산된 데이터만 저장할 수 있고, 모든 데이터가 메모리에 미리 저장되어 있다.
반면 스트림은 요청할 때만 데이터를 계산해 데이터를 효과적으로 처리하는 파이프라인을 구성한다. (Lazy)
for(String k : list) {
System.out.println(k);
}
List<String> newList = list.stream()
.map(Apple::getColor)
.collect(toList());
컬렉션은 외부 반복을 사용하지만 스트림은 내부 반복을 사용한다.
스트림이 처리할 데이터를 스트림 자체가 관리함을 내부 반복이라고 부른다.
작업을 병렬적으로 분산시키거나 멀티스레딩을 수행하는 등 세세한 부분은 스트림 API가 직접 처리한다.
따라서 개발자는 반복 과정을 신경쓰지 않아도 돼 비즈니스 로직에 집중할 수 있다.
내부 반복에서 사용되는 여러 중간 연산자들은 가능한 한 최대한 지연되고, 이를 통해 불필요한 연산을 줄여 성능을 개선한다.
중간 연산
filter : Predicate를 인수로 받아 일치하는 모든 요소를 포함하는 스트림을 반환한다.
distinct : 고유 요소로 이루어진 스트림을 반환한다. (hashCode, equals로 판단)
takeWhile : filter는 풀스캔을 돈다. 이미 정렬된 컬렉션에 대해서 효과적으로 처리할 때 사용한다.
dropWhile : takeWhile과 반대 작업을 수행한다. Predicate가 처음으로 거짓이 되는 지점까지의 요소를 버린다.
limit : 주어진 값 이하의 크기를 가지는 스트림을 반환한다.
skip : 처음 n개 요소를 제외한 스트림을 반환한다.
map : 함수를 인수로 받아 함수를 적용한 결과를 새로운 요소로 매핑한다.
flatmap : 스트림의 각 값을 다른 스트림으로 만든 후 모든 스트림을 하나의 스트림으로 연결한다.
중간 연산 중 표현식에서 하나라도 거짓인 결과가 나오면 표현식 결과와 상관없이 전체 결과도 거짓이 되는 경우가 있다.
이런 상황을 쇼트서킷이라고 부르고, 스트림은 쇼트서킷으로 성능을 끌어올린다.
마지막 연산
allMatch : Predicate를 받고 스트림의 모든 값이 일치하는지 검사한다. boolean을 반환한다.
noneMatch : allMatch와 반대 연산을 수행한다.
collect : 스트림을 컬렉션으로 반환한다.
findAny : 현재 스트림에서 임의의 요소를 반환한다.
forEach : 스트림의 각 요소에 대해 연산을 수행하고 스트림을 소비한다. void 반환타입이다.
마지막 연산을 수행하면 스트림을 소비하게 된다.
마지막 연산 전 디버깅 하려면 peek 연산을 사용하자.
reduce
리듀스 연산도 마무리 연산 중 하나인데, 스트림이 하나의 값으로 줄어들 때 까지 특정 람다식으로 각 요소를 반복해서 조합한다.
int sum = numbersList.stream().reduce(0, (a, b) -> a + b);
int sum = numbersList.stream().reduce(0, Integer::sum));
Integer 클래스는 두 수를 더하는 sum 메서드를 제공하니 람다식 대신 활용할 수 있다.
리듀스 연산은 결과를 누적할 내부 상태가 필요함을 기억하자.
예시처럼 0으로 명시적으로 시작 값을 정해줘도 되고, 정해주지 않는 경우 Optional 을 사용해야 한다.
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
stream.forEach(System.out::println);
// 스트림은 한 번 만 사용할 수 있음.
stream = list.stream();
stream.forEach(System.out::println); // "a", "b", "c"를 다시 출력합니다.
스트림은 한 번 만 사용할 수 있음을 기억하자.
자바8은 박싱 비용을 줄이기 위해 기본형 특화 스트림을 제공한다.
IntStream, LongStream, DoubleStream 세 가지로 Stream<Integer> 를 사용할 때와 달리 박싱을 하지 않아도 돼 성능 저하를 줄일 수 있다.
기본형 특화 스트림은 max min average 등 유용한 기능을 쉽게 사용할 수 있고, mapToInt 등 메서드를 통해 얻을 수 있다.
기본형 특화 스트림을 일반 스트림으로 변환하려면 boxed 메서드를 사용하자.
Collection : 자료구조를 나타내는 인터페이스로 List, Map, Set 등의 하위 인터페이스를 포함한다.
collect() : 스트림의 요소를 다른 형태로 묶어주는 최종 연산이다. 자료구조로 반환할 때 사용한다.
Collector : 인터페이스로 collect 메서드에서 사용하는 수집 동작을 정의한다. 다양한 팩토리 메서드를 제공해 다양한 자료구조로 쉽게 반환할 수 있도록 돕는다.
collect 메서드를 응용해 스트림을 편하게 다룰 수 있다.
List<String> collected = Stream.of("a", "b", "c")
.collect(Collectors.toList());
Optional<Integer> sum = Stream.of(1, 2, 3, 4)
.collect(Collectors.reducing(Integer::sum));
Stream.of 메서드로 입력받은 집합을 스트림으로 변환한다.
지금까지 사용해왔듯 스트림을 리스트로 변환할 때 사용하기도 하지만 reducing 팩토리를 사용해 SQL에서 자주 사용하던 집계 함수처럼 자바를 다룰 수 있다.
이 외에도 스트림은 groupingBy로 스트림의 요소를 그룹화하거나, partitioningBy로 스트림의 요소를 분할하는 등 데이터를 선언적으로 다룰 수 있는 기능을 제공한다.
스트림은 데이터를 선언적으로, 함수형 방식으로 처리하고 필요할 때만 연산을 수행하며 병렬 처리를 지원해 성능을 최적화하는 자바의 데이터 처리 기능이다.
선언형 프로그래밍은 SQL 처럼 어떤 데이터를 찾는지 정의하고 실행 방법은 시스템이 결정하도록 하는 방식이고
명령형 프로그래밍은 기존 자바에서 수행하는 방식처럼 개발자가 직접 실행 알고리즘을 생각해서 구현하는 방식이다.
스트림은 기존 명령형 프로그래밍에서 벗어나 선언적으로 데이터를 다룰 수 있게 도와주는 기술이다.
SQL 으로 데이터를 처리하고 자바에서는 처리된 데이터만 다루는것도 방법이지만, 자바에서 데이터를 처리하게 되면 데이터베이스와 애플리케이션 간 주고받는 데이터의 양을 줄일 수 있다.
항상 그렇듯 상황에 따라 적절하게 사용하자.
'Programming Language > Java' 카테고리의 다른 글
[Java] 스트림과 병렬 실행 (0) | 2023.08.02 |
---|---|
[Java] 람다 표현식 (0) | 2023.06.09 |
자바 예외 이해하기 (0) | 2022.09.03 |
[Java] 네트워킹 (Networking) 2 (0) | 2021.12.13 |
[Java] 네트워킹 (Networking) 1 (0) | 2021.12.05 |
댓글
이 글 공유하기
다른 글
-
[Java] 스트림과 병렬 실행
[Java] 스트림과 병렬 실행
2023.08.02 -
[Java] 람다 표현식
[Java] 람다 표현식
2023.06.09 -
자바 예외 이해하기
자바 예외 이해하기
2022.09.03 -
[Java] 네트워킹 (Networking) 2
[Java] 네트워킹 (Networking) 2
2021.12.13