⚡ Stream 이란?
Java Stream은 자바 8부터 추가된 기능으로, 컬렉션과 배열과 같은 데이터 소스에서 요소를 처리하고 다양한 연산을 수행할 수 있는 API이다. Java Stream API는 함수형 프로그래밍의 개념을 기반으로 만들어졌기 때문에 람다식과 함께 사용할 수 있다.
Java Stream은 크게 생성, 중간 연산, 최종 연산으로 구성된다. 생성 연산은 Stream 객체를 생성하는 역할을 하고 중간 연산은 Stream 객체의 요소를 처리하거나 필터링하는 등의 작업을 수행하며 최종 연산은 Stream 객체의 요소를 이용하여 결과를 도출하는 작업을 수행한다.
Java Stream API에는 다양한 중간 연산과 최종 연산이 제공되며 이를 활용하여 데이터를 처리하고 다양한 연산을 수행할 수 있다. 또한 Java Stream API는 병렬 처리를 지원하여 대용량 데이터의 처리 속도를 향상시킬 수 있다.
Java Stream을 사용하면 코드의 가독성이 향상되며 작성한 코드가 간결해지고 유지보수가 용이해진다. 또한 Java Stream API를 활용하면 데이터를 처리하는데 필요한 반복문이나 조건문 등을 직접 작성하지 않아도 되므로 코드의 재사용성과 생산성이 향상된다.
⚡ Stream의 특징
- 지연 평가(Lazy Evaluation)
Stream은 요소를 저장하지 않고 요청이 있을 때마다 필요한 요소만 처리하여 반환한다. 이렇게 요청이 있을 때마다 처리하는 방식을 "지연 평가"라고 하며 이러한 방식은 메모리 절약과 연산 속도 향상에 도움을 준다. - 파이프라인(Pipeline)
Stream은 여러 개의 연산을 연결하여 파이프라인을 만들 수 있다. 이러한 방식은 각 연산을 병렬로 처리할 수 있는 기능을 제공하며 더 효율적인 처리를 가능하게 한다. - 병렬 처리(Parallel Processing)
Stream은 병렬 처리를 지원하여 요소를 병렬로 처리할 수 있다. 이러한 방식은 대용량 데이터의 처리를 빠르게 처리할 수 있다. - 내부 반복(Internal Iteration)
Stream은 내부적으로 반복(iteration)을 처리한다. 이러한 방식은 코드의 간결성을 높여준다. - 재사용 가능(Reusability)
Stream은 여러 번 사용할 수 있다. 이러한 방식은 중간 연산 결과를 저장하고 다시 사용할 수 있으므로 연산을 반복해서 수행할 필요가 없다.
추가적으로 stream은 일회용이다. stream을 한 번 사용하면 다시 사용할수 없다는 소리이다. 이는 stream이 내부적으로 상태를 가지고 있지않고 처리하는 동안 요소를 소비하기 때문이다.
/* 예시 */
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = numbers.stream();
stream.forEach(System.out::println); // Stream을 소비하여 출력
stream.forEach(System.out::println); // 에러 발생: Stream has already been operated upon or closed
⚡ 기본적인 사용 방식
❗ 생성하는 방법
java Stream을 생성하는 방법은 다양하다. 그중에서 가장 많이 사용하는 방식에 대한 설명이다.
// 컬렉션에서 Stream을 생성하는 방법
List<String> list = Arrays.asList("apple", "banana", "orange");
Stream<String> stream = list.stream();
// 배열에서 Stream을 생성하는 방법
String[] array = {"apple", "banana", "orange"};
Stream<String> stream = Arrays.stream(array);
// Stream.builder() 메소드를 이용하여 Stream을 생성하는 방법
Stream<String> stream = Stream.<String>builder()
.add("apple")
.add("banana")
.add("orange")
.build();
// Stream.generate() 메소드나 Stream.iterate() 메소드를 이용하여 생성하는 방법
Stream<Integer> stream1 = Stream.generate(() -> 1); // 1로 이루어진 무한 Stream
Stream<Integer> stream1 = Stream.generate(() -> 1).limit(5); // limit로 크기 제한 가능.
Stream<Integer> stream2 = Stream.iterate(1, n -> n + 1); // 1부터 시작하여 1씩 증가하는 무한 Stream
Stream<Integer> stream2 = Stream.iterate(1, n -> n + 1).limit(5); // limit로 크기 제한 가능.
보통 내가 사용하는 방식은 list로 데이터를 받아왔다고 가정을 했을때, 해당 데이터를 기준으로 그대로 데이터를 가공하고 최종 연산을 통해서 결과를 도출해서 사용을 하고 있다.
❗❗ Stream에서 데이터 가공 방법
데이터를 가공하는데 있어서 가장 많이 사용하는 중간 연산에 대한 설명이다.
👉 filter() 연산
/* 특정 조건을 만족하는 요소만을 걸러내는 작업을 수행하도록 한다. */
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = numbers.stream().filter(n -> n % 2 == 0); // 짝수만 걸러내는 Stream
👉 map() 연산
/* 다른 형태로 변환하는 작업을 수행한다. */
List<String> strings = Arrays.asList("apple", "banana", "orange");
Stream<Integer> stream = strings.stream().map(s -> s.length()); // 문자열 길이로 변환하는 Stream
👉 flatMap() 연산
/* 요소를 하나 이상의 Stream으로 변환하는 작업을 수행한다. */
List<List<Integer>> list = Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4, 5));
Stream<Integer> stream = list.stream().flatMap(Collection::stream); // 모든 요소를 하나의 Stream으로 변환
👉 distinct() 연산
/* 요소에서 중복된 값들을 제거하는 작업을 수행 한다. */
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5);
Stream<Integer> stream = numbers.stream().distinct(); // 중복된 값을 제거하는 Stream
👉 sorted() 연산
/* 요소를 정렬하는 작업을 수행 한다. */
List<Integer> numbers = Arrays.asList(5, 4, 3, 2, 1);
Stream<Integer> stream = numbers.stream().sorted(); // 오름차순으로 정렬하는 Stream
👉 limit() 연산
/* 요소 중 일부만을 선택하는 작업을 수행 한다. */
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = numbers.stream().limit(3); // 처음 3개의 요소만 선택하는 Stream
👉 skip() 연산
/* 요소 중 일부를 제외하는 작업을 수행 한다. */
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = numbers.stream().skip(3); // 처음 3개의 요소를 제외한 Stream
👉 peek() 연산
/* 요소를 소비하면서 각 요소에 대해 특정 작업을 수행한다. 주로 디버깅 용도로 사용된다. */
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.peek(System.out::println) // 각 요소를 출력
.filter(n -> n % 2 == 0) // 짝수만 선택
.forEach(System.out::println); // 선택된 짝수 요소만 출력
❗ 최종 연산 (결과 도출 방법)
요소를 소비하여 결과를 반환하는 연산이다. 최종 연산은 Stream의 처리가 끝난 후에만 수행되며 이 연산은 Stream의 처리 결과를 반환하거나 Stream의 요소를 수집하거나 Stream의 요소 중 하나를 선택하는 등 다양한 작업을 수행할 수 있다.
👉 forEach() 연산
/* 요소를 소비하기만 하고 반환값이 없다. */
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.forEach(System.out::println); // 각 요소를 출력
👉 toArray() 연산
/* 요소를 배열로 수집하여 반환한다. */
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Integer[] array = numbers.stream()
.toArray(Integer[]::new); // Integer 배열로 변환하여 반환
👉 reduce() 연산
/* 이 연산은 초기값과 람다식을 이용하여 Stream의 요소를 계산하고 최종 결과를 반환한다. */
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, (x, y) -> x + y); // 모든 요소의 합을 계산하여 반환
👉 collect() 연산
/* 이 연산은 초기값과 수집기를 이용하여 Stream의 요소를 수집하고, 최종 결과를 반환 한다. */
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0) // 짝수만 선택
.collect(Collectors.toList()); // 짝수 요소를 리스트로 수집하여 반환
// 자주 사용되는 수집기로는 toList(), toSet(), toMap() 등이 있다.
⚡ 알면 좋은 내용들 (추가적으로 정리 예정)
- 동작 순서
- 성능 비교
- 스트림 재사용
⚡ 생각
stream의 경우 자바8에서 추가된 기능이지만 람다식과 함께 개발을 하면서 현재까지도 계속해서 유용하게 사용하고 있다. 해당 api를 모르는 사람이라면 아마 for문을 통해서 데이터를 가공하고 결론을 도출 할것이라고 생각한다. 처음 사용하는 개발자라면 익숙하지 않아 코드의 가독성이 높아지지 않고 개발을 하면서 어색함을 많이 느낄수 있다고 생각을 한다. 하지만 stream을 사용하면서 동작 방식과 메커니즘을 이해하고 사용을 한다면 높은 수준의 추상화를 통해 데이터 처리를 간결하게 작성하고 이를 통해 코드의 가독성을 높이고, 유지보수성을 향상 시킬수 있다고 생각한다.개발을 하면서 사용을 하고 있는데 내용을 한번쯤은 정리하고 싶어서 정리를 하게 되었다. 내가 사용을 하면서 문제가 됬던 부분에 대해서도 정리를 할 예정이다.
'Java' 카테고리의 다른 글
[JPA] 하이버네이트(Hibernate) 에 대한 내용 정리 (0) | 2023.05.01 |
---|---|
[Java] Wrapper Class 의 캐싱에 대한 내용 정리 (0) | 2023.04.16 |
[JPA] QueryDsl에서 Date Type 사용 방법 정리 (0) | 2023.02.05 |
[JAVA] Tomcat error page 호출 관련 내용 정리 (0) | 2022.12.19 |
💀 디자인 패턴 및 리팩터링 개선 작업 내용 정리 (0) | 2022.11.27 |