# 스트림 API를 통한 데이터 처리 자바 8은 람다 표현식과 스트림 API의 도입으로 인해 프로그래밍 패러다임에 많은 변화를 가져왔습니다. 특히, 스트림 API는 데이터 처리 방식을 혁신적으로 변화시킨 도구입니다. ## 스트림 API의 등장 배경과 목적 옛날에 자바라는 나라가 있었습니다. 이 나라의 주민들은 매일매일 큰 상자 안에 들어있는 수많은 과일들을 정리하는 일을 하곤 했습니다. 이 일을 하기 위해서는 각각의 과일을 손으로 집어서 확인하고, 분류하고, 다시 상자에 넣어야 했습니다. 이런 방식은 간단하고 직관적이었지만, 과일이 너무 많아질 경우에는 시간이 많이 걸리고 힘들었습니다. 그러던 어느 날, 자바 나라에는 새로운 기계가 도입되었습니다. 이 기계의 이름은 '스트림 API'였습니다. 이 기계는 상자 속의 과일을 한 번에 획일적으로 처리할 수 있었습니다. 기계를 통해 과일을 분류하면, 과일의 종류에 따라 자동으로 분류되고, 필요한 과일만 선택해서 다시 상자에 담을 수 있었습니다. 또한 이 기계는 여러 사람들이 동시에 사용할 수 있어서, 과일 분류 작업을 더 빠르게 처리할 수 있었습니다. 스트림 API 기계의 도입으로 자바 나라의 주민들은 과일 정리 작업을 훨씬 더 효율적이고 빠르게 처리할 수 있게 되었습니다. 이로써 자바 나라는 더 큰 상자의 과일도 손쉽게 처리할 수 있게 되었고, 주민들은 더 복잡한 작업도 간단하게 수행할 수 있게 되었습니다. 이처럼 자바의 스트림 API는 큰 데이터 컬렉션을 효율적으로 처리하고, 병렬 처리를 통해 성능을 향상시키는 매우 유용한 도구입니다. 이를 통해 자바는 더 복잡하고 대규모의 데이터 처리 작업에 대응할 수 있게 되었습니다. ### 등장배경 자바 8 이전에는 데이터 컬렉션을 처리하기 위해 주로 for-each 루프나 Iterator를 사용했습니다. 하지만 이러한 방법은 복잡한 데이터 처리 작업을 수행하기 어렵고, 다중 쓰레드 환경에서의 병렬 처리를 지원하지 못했습니다. 또한, 데이터의 크기가 커지면 성능이 저하되는 문제도 있었습니다. 이런 문제들을 해결하기 위해 자바 8에서는 함수형 프로그래밍 패러다임을 도입하고, 이를 데이터 컬렉션 처리에 적용하기 위해 스트림 API를 도입하게 되었습니다. ### 목적 스트림 API의 주요 목적은 **데이터 컬렉션을 더욱 효율적으로 처리하는 것**입니다. **함수형 프로그래밍 방식을 활용**하여 더욱 간결하고 직관적인 코드를 작성할 수 있게 해줍니다. 또한 스트림 API는 내부 반복을 사용하여 개발자가 직접 반복문을 제어하는 대신, 컴파일러가 알아서 최적의 방법으로 처리하게 해줍니다. 이를 통해 병렬 처리를 쉽게 할 수 있어 성능을 향상시킬 수 있습니다. 따라서 스트림 API는 대용량 데이터 처리에 적합하며, 복잡한 데이터 처리 작업을 간단하게 할 수 있게 도와줍니다. ## 스트림 API 개념 ### 스트림 API의 개념과 특징 #### 개념 '스트림'이라는 개념을 중심으로 합니다. 스트림은 '데이터의 흐름'을 의미하며, 데이터 컬렉션을 함수형 프로그래밍 방식으로 처리할 수 있도록 합니다. 스트림은 데이터 소스로부터 데이터를 읽고, 함수를 통해 데이터를 변환하거나 필터링하며, 최종적으로 결과를 생성하거나 소비합니다. #### 특징 1. 비동기 처리 지원: 스트림 API는 병렬 처리를 지원하여 대용량 데이터를 빠르게 처리할 수 있습니다. 이는 '병렬 스트림'을 생성하여 가능합니다. 2. 함수형 프로그래밍: 스트림은 람다 표현식과 함께 사용되어 함수형 프로그래밍 패러다임을 지원합니다. 이를 통해 복잡한 연산을 간결하고 가독성 높은 코드로 표현할 수 있습니다. 3. 중간 연산과 최종 연산: 스트림은 '중간 연산'과 '최종 연산'으로 구분되는 일련의 연산들을 통해 데이터를 처리합니다. 중간 연산은 스트림을 반환하여 연산의 연결이 가능하고, 최종 연산은 스트림을 소비하여 결과를 생성하거나 다른 동작을 수행합니다. 4. 단 한번의 소비: 스트림은 '단 한 번만 소비'할 수 있는 특성이 있습니다. 즉, 한 번 소비한 스트림은 재사용할 수 없습니다. ### 스트림이 제공하는 주요 연산: 중간 연산과 최종 연산 스트림의 중간 연산과 최종 연산을 이해하고 적절히 사용하면, 복잡한 데이터 처리 작업을 단계별로 나누어 효율적으로 수행할 수 있습니다. #### 중간 연산 중간 연산은 스트림을 다른 스트림으로 변환하는 연산입니다. 중간 연산은 필터링, 매핑, 정렬 등의 작업을 수행합니다. 중요한 점은 중간 연산이 'lazy'하다는 것입니다. 즉, 중간 연산은 최종 연산이 수행되기 전까지 실제로 실행되지 않습니다. 이를 통해 불필요한 연산을 최소화하고 성능을 향상시킬 수 있습니다. 대표적인 중간 연산 메서드로는 filter(), map(), sorted() 등이 있습니다. #### 최종 연산 최종 연산은 스트림의 요소를 소비하여 최종 결과를 도출하는 연산입니다. 최종 연산이 호출되어야 중간 연산이 실행되며, 스트림의 요소들을 실제로 처리하게 됩니다. 최종 연산 후에는 스트림을 더 이상 사용할 수 없습니다. 대표적인 최종 연산 메서드로는 forEach(), toArray(), reduce(), collect() 등이 있습니다. ## 스트림 API 사용 예시 ### 간단한 배열이나 컬렉션을 스트림으로 변환하는 예시 1. **배열을 스트림으로 변환**: 자바에서는 Arrays.stream() 메서드를 사용하여 배열을 스트림으로 변환할 수 있습니다. 이 메서드는 주어진 배열을 소스로 하는 새로운 스트림을 반환합니다. 다음은 정수 배열을 스트림으로 변환하는 예시입니다. ```java int[] numbers = {1, 2, 3, 4, 5}; IntStream stream = Arrays.stream(numbers); ``` 여기서 Arrays.stream() 메서드를 호출하여 정수 배열을 스트림으로 변환하고 있습니다. 반환된 스트림은 IntStream 인터페이스의 인스턴스입니다. **배열을 스트림으로 변환 후 사용 방법**: 배열을 스트림으로 변환한 후에는 스트림 API의 메서드를 사용하여 데이터를 처리할 수 있습니다. 예를 들어, 다음은 정수 배열에서 홀수만 필터링하여 합계를 구하는 예시입니다. ```java int[] numbers = {1, 2, 3, 4, 5}; IntStream stream = Arrays.stream(numbers); int sum = stream.filter(n -> n % 2 == 1).sum(); ``` 여기서는 filter() 메서드를 사용하여 홀수만 선택하고, sum() 메서드를 사용하여 선택된 요소의 합계를 구하고 있습니다. 2. **컬렉션을 스트림으로 변환**: 컬렉션에서는 Collection 인터페이스의 stream() 메서드를 사용하여 컬렉션을 스트림으로 변환할 수 있습니다. 이 메서드는 컬렉션의 요소를 소스로 하는 새로운 스트림을 반환합니다. 다음은 리스트 컬렉션을 스트림으로 변환하는 예시입니다. ```java List<String> names = Arrays.asList("홍길동", "이순신", "강감찬"); Stream<String> stream = names.stream(); ``` 여기서는 리스트의 stream() 메서드를 호출하여 리스트를 스트림으로 변환하고 있습니다. 반환된 스트림은 Stream 인터페이스의 인스턴스입니다. **컬렉션을 스트림으로 변환 후 사용 방법**: 컬렉션을 스트림으로 변환한 후 스트림 API의 메서드를 사용할 수 있습니다. 예를 들어, 다음은 리스트에서 길이가 3 이상인 문자열만 선택하여 대문자로 변환하는 예시입니다. ```java List<String> names = Arrays.asList("홍길동", "이순신", "강감찬"); Stream<String> stream = names.stream(); List<String> result = stream.filter(name -> name.length() >= 3).map(String::toUpperCase).collect(Collectors.toList()); ``` 여기서는 filter() 메서드로 길이가 3 이상인 문자열만 선택하고, map() 메서드로 선택된 문자열을 대문자로 변환하고 있습니다. collect() 메서드를 통해 결과를 다시 리스트로 수집하고 있습니다. ### 필터링, 맵핑, 리듀싱 등 스트림 연산을 사용하는 코드 예시 1. **필터링**: filter() 메서드는 스트림의 요소 중 특정 조건을 만족하는 요소만 선택합니다. 예를 들어, 다음 코드는 리스트에서 홀수만 선택하는 예시입니다. ```java List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> oddNumbers = numbers.stream().filter(n -> n % 2 == 1).collect(Collectors.toList()); ``` 여기서는 filter() 메서드에 람다 표현식 `n -> n % 2 == 1`를 전달하여 홀수만 선택하도록 하였습니다. collect() 메서드를 사용하여 결과를 다시 리스트로 수집하였습니다. 2. **맵핑**: map() 메서드는 스트림의 각 요소를 특정 방식으로 변환합니다. 예를 들어, 다음 코드는 문자열 리스트의 각 요소를 대문자로 변환하는 예시입니다. ```java List<String> names = Arrays.asList("홍길동", "이순신", "강감찬"); List<String> upperCaseNames = names.stream().map(String::toUpperCase).collect(Collectors.toList()); ``` 여기서는 map() 메서드에 메서드 참조 `String::toUpperCase`를 전달하여 각 문자열을 대문자로 변환하도록 하였습니다. 3. **리듀싱**: reduce() 메서드는 스트림의 요소를 하나의 값으로 줄입니다. 예를 들어, 다음 코드는 정수 리스트의 모든 요소의 합계를 구하는 예시입니다. ```java List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream().reduce(0, Integer::sum); ``` 여기서는 reduce() 메서드에 초기값 0과 메서드 참조 `Integer::sum`을 전달하여 모든 요소의 합계를 구하도록 하였습니다. ## 스트림 API 사용 시 주의점 1. **스트림 재사용**: 스트림은 한 번만 사용할 수 있습니다. 한 번 소비된(최종 연산이 수행된) 스트림은 재사용할 수 없습니다. 즉, 최종 연산이 한 번 수행된 후에는 다시 중간 연산이나 최종 연산을 수행할 수 없습니다. 필요한 경우 새로운 스트림을 생성해야 합니다. 2. **병렬 처리 고려**: 스트림은 병렬 처리를 지원하므로 큰 데이터 집합을 처리할 때 성능 향상을 가져올 수 있습니다. 하지만 모든 경우에 병렬 스트림이 도움이 되는 것은 아닙니다. 데이터 크기, 소스 데이터 구조, 박스 비용, 코어 수 등 여러 요소를 고려해야 합니다. 때로는 병렬 스트림이 오히려 성능을 저하시킬 수 있습니다. 3. **부수 효과 주의**: 스트림 연산은 부수 효과 없이 계산을 수행하는 것이 가장 좋습니다. 스트림 연산 중에 데이터를 변경하거나 멀티 스레드 환경에서 공유된 가변 데이터에 접근하면 예상치 못한 결과나 동시성 문제를 일으킬 수 있습니다. 4. **순서 보장**: 스트림의 연산은 기본적으로 순서를 보장하지 않습니다. 특히 병렬 스트림에서는 전혀 다른 순서로 연산이 수행될 수 있습니다. 순서가 중요한 경우에는 이를 고려해야 합니다. 5. **무한 스트림 주의**: 스트림 API는 무한 스트림을 생성할 수 있습니다. 무한 스트림을 사용할 때는 주의가 필요하며, 반드시 종료 조건을 제공해야 합니다. ## 스트림 API 중간연산 메서드 및 최종연산 메서드 정리 | 종류 | 메서드 | 설명 | 사용 예 | | --- | --- | --- | --- | | 중간 연산 | filter() | 조건에 맞는 요소만 선택하여 새로운 스트림을 생성합니다. | 홀수만 선택하거나, 특정 문자열을 포함하는 요소만 선택할 때 사용합니다. | | 중간 연산 | map() | 각 요소를 특정 방식으로 변환하여 새로운 스트림을 생성합니다. | 문자열을 대문자로 변환하거나, 객체의 특정 필드만 추출할 때 사용합니다. | | 중간 연산 | flatMap() | 각 요소를 스트림으로 변환하고, 이 스트림들을 하나의 스트림으로 합칩니다. | 리스트의 리스트를 하나의 리스트로 합칠 때 사용합니다. | | 중간 연산 | sorted() | 요소를 정렬하여 새로운 스트림을 생성합니다. | 요소를 자연 순서나 특정 비교자로 정렬할 때 사용합니다. | | 중간 연산 | distinct() | 중복된 요소를 제거하여 새로운 스트림을 생성합니다. | 중복된 요소를 제거할 때 사용합니다. | | 중간 연산 | limit() | 주어진 개수만큼의 요소를 가진 스트림을 생성합니다. | 스트림의 크기를 제한할 때 사용합니다. | | 최종 연산 | forEach() | 각 요소에 대해 주어진 동작을 수행합니다. | 각 요소를 출력하거나, 각 요소에 대해 특정 동작을 수행할 때 사용합니다. | | 최종 연산 | toArray() | 스트림의 모든 요소를 배열로 변환합니다. | 결과를 배열로 받을 때 사용합니다. | | 최종 연산 | reduce() | 스트림의 모든 요소를 하나의 값으로 줄입니다. | 모든 요소의 합계나 곱을 구할 때 사용합니다. | | 최종 연산 | collect() | 스트림의 모든 요소를 컬렉션으로 변환합니다. | 결과를 리스트나 세트, 맵으로 받을 때 사용합니다. | | 최종 연산 | min(), max() | 스트림의 최소값 또는 최대값을 반환합니다. | 최소값 또는 최대값을 구할 때 사용합니다. | | 최종 연산 | count() | 스트림의 요소 개수를 반환합니다. | 요소 개수를 구할 때 사용합니다. | | 최종 연산 | anyMatch(), allMatch(), noneMatch() | 특정 조건에 맞는 요소가 있는지, 모든 요소가 조건에 맞는지, 어떤 요소도 조건에 맞지 않는지 검사합니다. | 특정 조건을 만족하는 요소의 존재 여부를 확인할 때 사용합니다. | ## 모의 코딩테스트 1. **질문**: 정수 리스트에서 홀수만 선택하여 그 합을 구하는 코드를 스트림 API를 사용하여 작성해 보세요. ```java List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); ``` **답변**: ```java List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream().filter(n -> n % 2 == 1).mapToInt(Integer::intValue).sum(); ``` 2. **질문**: 문자열 리스트에서 각 문자열의 길이를 리스트로 반환하는 코드를 스트림 API를 사용하여 작성해 보세요. ```java List<String> words = Arrays.asList("Hello", "World"); ``` **답변**: ```java List<String> words = Arrays.asList("Hello", "World"); List<Integer> lengths = words.stream().map(String::length).collect(Collectors.toList()); ``` 3. **질문**: 주어진 정수 범위에서 소수만을 선택하여 리스트로 반환하는 코드를 스트림 API를 사용하여 작성해 보세요. **힌트**: isPrime 메서드는 소수를 판별하는 메서드입니다. ```java int start = 1, end = 100; ``` **답변**: ```java int start = 1, end = 100; List<Integer> primes = IntStream.rangeClosed(start, end).filter(this::isPrime).boxed().collect(Collectors.toList()); ``` 4. **질문**: 문자열 배열에서 중복된 문자열을 제거한 후, 사전 순으로 정렬하여 리스트로 반환하는 코드를 스트림 API를 사용하여 작성해 보세요. ```java String[] words = {"apple", "banana", "apple", "orange"}; ``` **답변**: ```java String[] words = {"apple", "banana", "apple", "orange"}; List<String> distinctSortedWords = Arrays.stream(words).distinct().sorted().collect(Collectors.toList()); ``` ## 질문 스트림 API 적용 이전의 소스코드를 스트림 API를 적용하여 다시 구성하고 싶을때 고려해야하는 사항 1. **병렬 처리 가능성**: 스트림은 데이터 처리를 병렬로 수행하는 것을 지원합니다. 그러나 모든 작업이 병렬 처리에 적합한 것은 아닙니다. 데이터 처리 로직이 병렬 처리를 지원하는지, 그리고 병렬 처리가 성능을 실제로 향상시키는지 확인해야 합니다. 2. **순서 보장**: 스트림 연산은 기본적으로 순서를 보장하지 않습니다. 이는 특히 병렬 스트림에서 중요합니다. 원래의 코드가 처리 순서에 의존하는 경우에는 이를 고려해야 합니다. 3. **상태 유지 연산**: 스트림의 연산 중 일부는 상태를 유지합니다. 예를 들어, sorted() 연산은 모든 요소를 메모리에 유지해야 합니다. 큰 데이터 집합을 처리할 때 이런 연산은 메모리 문제를 일으킬 수 있습니다. 4. **예외 처리**: 스트림의 람다 표현식에서는 체크 예외를 직접 처리할 수 없습니다. 체크 예외를 던지는 코드를 스트림에서 사용하려면, 예외를 언체크 예외로 바꿔야 합니다. 5. **가독성**: 복잡한 스트림 표현식은 가독성을 저하시킬 수 있습니다. 코드의 복잡성과 가독성을 고려하여 스트림 API를 적절히 사용해야 합니다. 6. **리팩토링 검증**: 스트림 API를 적용한 후에는 기존 로직이 올바르게 동작하는지 테스트를 통해 검증해야 합니다.