자바 Stream API: 데이터 처리를 더 효율적으로
자바 8에서 소개된 Stream API는 다양한 데이터 소스(컬렉션, 배열 등)를 표준화된 방법으로 다루는 강력한 도구입니다. 이 블로그 글에서는 자바 Stream API의 주요 특징과 사용법을 자세히 알아보겠습니다.
스트림(Stream)이란 무엇인가요?
스트림은 자바 8에서 도입된 데이터 처리 방법 중 하나로, 다양한 데이터 소스를 표준화된 방법으로 다룰 수 있게 해줍니다. 이전에는 List, Set, Map 등의 사용 방법이 각각 다르기 때문에 데이터 처리 코드가 복잡해졌습니다. 스트림은 이런 불편함을 해소하고 데이터 소스를 통일된 방식으로 다룰 수 있도록 합니다.
스트림은 데이터 소스를 스트림으로 변환한 후 여러 번의 중간 연산과 최종 연산을 통해 데이터를 처리합니다. 이를 통해 데이터 처리를 효율적으로 수행할 수 있습니다.
스트림의 처리 순서는 다음과 같습니다:
- 스트림 만들기
- 중간 연산(반복 적용 가능하며, 연산 결과가 스트림)
- 최종 연산 (스트림의 요소를 소모하고 결과 반환)
예를 들어, 다음 코드는 리스트에서 스트림을 생성하고 중간 연산과 최종 연산을 수행하는 예제입니다.
list.stream() // 스트림 만들기
.distinct() // 중간 연산
.limit(5) // 중간 연산
.sorted() // 중간 연산
.forEach(System.out::println); // 최종 연산
스트림의 특징
스트림의 특징을 알아보겠습니다.
- 스트림은 데이터를 담고 있는 저장소(컬렉션)가 아닙니다. 따라서 스트림은 원본 데이터 소스를 변경하지 않고 읽기 전용(read-only)입니다.
- 스트림은 일회용입니다. 한 번 사용하면 재사용할 수 없으며, 필요한 경우 다시 스트림을 생성해야 합니다.
- 최종 연산을 호출하기 전까지 중간 연산은 실제로 수행되지 않습니다. 이를 "게으른(lazy)" 연산이라고 합니다.
- 스트림은 무제한할 수도 있으며, Short Circuit 메소드를 사용하여 제한할 수 있습니다.
- 병렬 처리가 쉽게 가능하며 멀티 스레드를 활용할 수 있습니다. (.parallel 메소드를 사용하여 병렬 스트림으로 변환)
- 기본형 스트림(IntStream, LongStream, DoubleStream)이 제공되며, 오토박싱과 언박싱 과정을 줄여 효율적으로 처리할 수 있습니다.
스트림 사용하기
1. 스트림 생성
스트림을 생성하는 방법은 다양합니다.
- 컬렉션을 스트림으로 변환: collection.stream()
- 객체 배열로부터 스트림 생성: Stream.of(elements)
- 배열에서 스트림 생성: Arrays.stream(array)
- 람다식 iterate() 또는 generate()
예를 들어, 다음은 리스트를 스트림으로 변환하는 방법입니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = numbers.stream();
2. 스트림의 중간 연산
중간 연산은 스트림의 요소를 변환하거나 필터링하는 등의 작업을 수행합니다. 몇 가지 중요한 중간 연산은 다음과 같습니다:
- distinct(): 중복 요소 제거
- filter(Predicate<T> predicate): 조건에 맞지 않는 요소 필터링
- limit(long maxSize): 최대 요소 개수 제한
- skip(long n): 앞에서부터 n개 요소 건너뛰기
- sorted(): 요소를 기본 정렬로 정렬
- sorted(Comparator<T> comparator): 요소를 지정한 조건에 따라 정렬
- map(Function<T, R> mapper): 요소를 다른 형태로 변환
- peek(Consumer<T> action): 요소에 작업 수행하며 중간 결과 확인
- flatMap(Function<T, Stream<R>> mapper): 스트림의 스트림을 펼쳐서 하나의 스트림으로 변환
3. 스트림의 최종 연산
최종 연산은 스트림의 요소를 소모하고 결과를 반환하는 작업을 수행합니다. 몇 가지 중요한 최종 연산은 다음과 같습니다:
- forEach(Consumer<? super T> action): 각 요소에 작업 수행
- forEachOrdered(Consumer<? super T> action): 병렬 스트림에서 순서 유지하며 작업 수행
- count(): 스트림의 요소 개수 반환
- max(Comparator<? super T> comparator): 스트림의 최대값 반환
- min(Comparator<? super T> comparator): 스트림의 최소값 반환
- findAny(): 아무 요소나 반환
(병렬 스트림에 유용)
- findFirst(): 첫 번째 요소 반환 (순차 스트림에 유용)
- allMatch(Predicate<T> p): 모든 요소가 조건을 만족하는지 확인
- anyMatch(Predicate<T> p): 하나 이상의 요소가 조건을 만족하는지 확인
- noneMatch(Predicate<T> p): 모든 요소가 조건을 만족하지 않는지 확인
- toArray(): 모든 요소를 배열로 반환
- toArray(IntFunction<A[]> generator): 특정 타입의 배열로 반환
- reduce(BinaryOperator<T> accumulator): 스트림의 요소를 하나씩 줄여가며 계산
- collect(Collector<? super T, A, R> collector): 스트림의 요소를 원하는 자료형으로 변환
스트림 API 사용 예시
데이터 필터링 (Filter)
스트림을 사용하면 데이터 필터링이 간편해집니다. 예를 들어, 이름이 3글자 이상인 데이터만 새로운 스트림으로 변경하는 코드는 다음과 같습니다.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");
Stream<String> filteredNames = names.stream()
.filter(name -> name.length() >= 3);
데이터 변환 (Map)
스트림의 map 연산을 사용하여 데이터를 원하는 형태로 변환할 수 있습니다. 예를 들어, 파일 목록에서 파일 이름만 추출하는 코드는 다음과 같습니다.
List<File> files = Arrays.asList(new File("file1.txt"), new File("file2.txt"), new File("file3.txt"));
Stream<String> fileNames = files.stream()
.map(File::getName);
요소 제한 (Limit)과 건너뛰기 (Skip)
스트림에서 일부 요소만 선택하려면 limit과 skip 연산을 사용할 수 있습니다. 아래 예제는 스트림에서 처음 3개 요소를 선택하고, 다음 2개 요소는 건너뛰는 예제입니다.
Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Stream<Integer> limitedNumbers = numbers
.limit(3) // 처음 3개 요소 선택
.skip(2); // 처음 2개 요소 건너뛰기
조건 검사 (anyMatch, allMatch, noneMatch)
스트림의 요소들 중에서 특정 조건을 만족하는지 확인할 때 anyMatch, allMatch, noneMatch 연산을 사용할 수 있습니다. 아래 예제는 스트림에 "Java"라는 단어가 하나라도 포함되어 있는지 확인합니다.
List<String> topics = Arrays.asList("Java Programming", "Python Basics", "Machine Learning");
boolean containsJava = topics.stream()
.anyMatch(topic -> topic.contains("Java"));
reduce와 collect 연산
reduce 연산은 스트림의 요소를 하나씩 줄여가며 누적 연산을 수행합니다. 아래 예제는 스트림의 모든 요소를 더하는 방법을 보여줍니다.
Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.reduce((total, number) -> total + number);
collect 연산은 스트림의 요소를 원하는 자료형으로 변환하는데 사용됩니다. 예를 들어, 스트림의 문자열 요소를 쉼표로 구분된 하나의 문자열로 변환하는 코드는 다음과 같습니다.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
String joinedNames = names.stream()
.collect(Collectors.joining(", "));
Stream API는 자바 8부터 도입된 강력한 데이터 처리 도구로, 코드를 간결하고 효율적으로 만들어줍니다. 데이터를 필터링하고 변환하는 등 다양한 작업을 수행할 수 있으며, 병렬 처리 기능을 활용하여 성능을 향상시킬 수 있습니다. Stream API를 마스터하면 자바 애플리케이션의 코드 품질과 유지 보수성을 향상시킬 수 있을 것입니다.