Java 8에서 추가된 스트림(Stream)은 컬렉션에 있는 요소(List, Map, Set)들을 더 편리하게 가공하고 처리하도록 해주는 반복자입니다.
배열이나 컬렉션에 저장되어 있는 데이터를 접근할 때 이전에는 반복문(for)이나 iterator를 사용하여 접근을 했습니다.
기존 for문 사용
String str = "12345";
int[] digits = new int[str.length()];
for(int i=0; i<str.length(); i++)
digits[i] = str.charAt(i) - '0';
// digits[i] = Integer.parseInt(String.valueOf(str.charAt(i)));
System.out.println( Arrays.toString(digits) );
// [1, 2, 3, 4, 5]
그러나 이러한 접근은 코드도 길고, 정형화되어있는 패턴이 없기 때문에 캐스팅을 해주거나 데이터마다 각각 다른 접근을 해야만 했습니다.
Stream 사용
String str = "12345";
int[] digits = Stream.of(str.split("")).mapToInt(Integer::parseInt).toArray();
System.out.println( Arrays.toString(digits) );
// [1, 2, 3, 4, 5]
Stream을 사용하면 위와같이 코드의 양도 획기적으로 줄고, 간결하게 표현할 수 있습니다.
Stream이란❓
우선 영단어 Stream의 뜻 자체를 보면 개울(N), 시내(N), 줄줄 흐르다(V), 줄을 지어 이어지다(V) 등입니다.
그러면 개발에서는 Stream은 물이 아닌 데이터의 흐름을 의미한다고 볼 수 있습니다.
위키피디아에서는 Stream을 "시간상에 나타나는 일련의 데이터 요소"라고 정희 합니다. 여기서 정의한 것과 영단어의 뜻을 생각해 보면 "연속적인 데이터의 흐름"에 초점을 맞추고 있는 것을 알 수 있습니다.
앞서 말했듯이 Stream은 Java 8부터 추가된 기술로 람다를 활용해 배열과 컬렉션을 함수형으로 간단하게 처리하며 데이터의 형태가 무엇이든 간에 같은 방식으로 다룰 수 있게 하는 기술입니다.
Stream의 동작 흐름❕
Stream의 동작 흐름은 크게 3가지로 구분할 수 있습니다.
1. Stream 생성
2. Stream 중개 연산
3. Stream 최종 연산
각 단계는 따로 포스팅을 정리하겠습니다.
Stream 특징
1. 내부 반복
위 코드는 [10분 테코톡] 크리스, 로마의 stream vs for에서 참조했습니다.
외부 반복 : Iterator와 같이 사용자가 직접 별도의 객체를 생성하여 명시적으로 컬렉션의 각 요소를 가져와 처리하는 방식. 개발자가 반복의 처음과 끝을 제어할 수 있습니다.
내부 반복 : Stream과 같이 개발자가 직접 반복을 제어하지 않고 컬렉션 내부적으로 반복을 처리. 사용자는 각 요소에 대한 처리 로직에만 집중할 수 있으며 코드의 가독성을 높일 수 있습니다.
2. 가독성 향상
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);
numbers.stream()
.filter(number -> number > 5)
.map(Distance::new)
.collect(Collectors.toList());
}
기존 기본 for문에 비해 직관적이고, 가동성이 좋아졌습니다.
3. 유지보수 향상
가독성이 향상된 만큼 간결하고 명확한 코드가 되었기 때문에 유지보수성도 자연스럽게 향상됩니다.
코드에 주석을 달지 않더라도 코드의 의도를 파악하기 쉬워지고, 변경이 필요한 부분도 쉽게 수정할 수 있습니다.
4. 병렬 처리 지원
데이터를 병렬 처리 지원은, 멀티 스레드를 이용하기 때문에 데이터를 효율적으로 처리할 수 있습니다.
Stream은 병렬 처리를 쉽게 할 수 있도록 메서드를 제공해 줍니다.
개발자가 직접 스레드 혹은 스레드풀을 생성하거나 관리할 필요 없이 parallelStream(), parallel()라는 연산을 추가하는 만으로도 ForkJoinFramework 관리 방식을 이용하여 작업들을 분할하고 병렬처리가 가능합니다.
- ForkJoinFramework 관리 방식
- 대규모 병렬 처리를 위해 설계된 고성능 멀티스레딩 프레임워크
- 분할 정복(devide and conquer) 알고리즘을 기반으로 작업을 더 작은 작업으로 분할(fork)하고, 이를 병렬 처리하고 난 뒤 최종 결과를 결합(join)하는 방식으로 동작합니다.
5. 디버깅
Stream은 내부적으로 수행하는 작업이 많기 때문에 Stack trace가 복잡합니다.
또한 최종연산을 하기 전까지 중간 연산들을 계획일 뿐 실제 실행은 최종 연산 시 일괄 실행됩니다.
즉 "지연 연산"으로 작업을 수행하기 때문에 실제 라인과 다른 순서로 Stack trace가 출력되어 어디서 에러가 발생했는지 디버깅이 for문에 비해 쉽지 않습니다.
6. 일회용성
Iterator로 컬렉션의 요소를 끝까지 다 읽고 나면 다시 사용할 수 없는 것처럼 Stream API도 일회용이기 때문에 한번 사용이 끝나면 재사용이 불가능합니다.
Stream이 또 필요한 경우에는 Stream을 또다시 생성해주어야 합니다.
만약 닫힌 Stream을 다시 사용한다면 "IllegalStateException" 에러가 발생하게 됩니다.
7. read-only
Stream API는 데이터 소스로부터 데이터를 읽기만 할 뿐, 원본을 변경하지 않습니다.
필요하다면 배열이나 컬렉션에 저장해야 합니다.
String A = "1234"
int[] num = Stream.of(A.split("")
.mapToInt(Integer::parseInt)
.toArray();
다음 포스팅에서는 Stream API의 사용법에 대해 알아보겠습니다.
'Language > Java' 카테고리의 다른 글
[JAVA] Stream API에 대해 알아보기 _ Stream 중간 연산(가공) (3/5) (0) | 2024.05.17 |
---|---|
[JAVA] Stream API에 대해 알아보기 _ Stream 생성 (2/5) (0) | 2024.05.16 |
[JAVA] 큰 수 다루기 (BigInteger, BigDecimal) (0) | 2024.05.13 |
[JAVA] char에서 String으로 변환하기 (value of() , charAt()) (0) | 2024.05.12 |
[JAVA] 입출력, BufferedReader, StringTokenizer, StringBuilder 알아보기 (0) | 2024.05.12 |