이번에는 자바의 스트림(Stream)에 대해서 정리해보려고 한다.
이전 포스팅에서 람다식을 정리하면서 스트림 API를 살짝 다뤘었는데, 이번 포스팅에서 본격적으로 알아보자 👀
스트림은 컬렉션, 배열 등의 저장 요소를 하나씩 참조하면서 함수형 인터페이스(람다식)를 적용하며 반복적으로 처리할 수 있도록 해주는 기능이다.
자바 8에서 추가된 이 기능은 '데이터의 흐름'이라고 생각하면 이해하기 쉽다.
왜 스트림이 필요한지 간단한 예제를 통해 살펴보자!
Sample Code
List<String> names = Arrays.asList("김자바", "이파이썬", "박코틀린"); List<String> longNames = new ArrayList<>(); for(String name : names) { if(name.length() >= 4) { longNames.add(name); } } for(String name : longNames) { System.out.println(name); }
위 코드는 이름 목록에서 길이가 4 이상인 이름을 찾아 출력하는 간단한 로직이다.
하지만 로직이 복잡해질수록 for문이 중첩되고 코드의 가독성이 떨어진다는 문제점이 있다.
위 코드를 스트림을 사용하면 다음과 같이 작성할 수 있다:
Sample Code
List<String> names = Arrays.asList("김자바", "이파이썬", "박코틀린"); names.stream() .filter(name -> name.length() >= 4) .forEach(System.out::println);
위 코드처럼 .
을 통해서 스트림의 메서드들을 체이닝하는 것이 바로 스트림을 이용하는 방법이다.
스트림의 특징 중 하나는 이전 연산의 결과가 다음 메서드의 입력이 된다는 것이다.
따라서, .filter(name -> name.length() >= 4)
의 결과가 .forEach(Systm.out::println);
의 매개변수로 전달되는 것이다.
이처럼 스트림을 사용하면 코드의 흐름을 파악하기 쉬워지고 간결해진다는 특징이 있다!
필자는 그냥 1번 방법처럼 필요한 메서드들을 가져다가 붙여서 사용하면 되는 줄 알았다.
하지만, 강의를 수강하면서 스트림에도 구조가 있다는 것을 알게되었고, 이제부터 스트림의 구조에 대해서 정리해보려고 한다.
스트림은 크게 세 가지 단계로 동작한다.
동작 구조
- 스트림 생성
- 중간 연산 (필터링, 매핑 등)
- 최종 연산 (결과 도출)
참고
위 사진 처럼 스트림은 주로 컬렉션과 배열을 통해서 생성하고, 중간 처리 과정을 거친 이후, 결과를 리턴한다.
이제부터 각각의 단계를 자세히 살펴보자.
스트림은 다양한 데이터 소스로부터 생성할 수 있다.
코드를 통해 살펴보자
Sample Code
// 1. 컬렉션으로부터 생성 List<String> list = Arrays.asList("a", "b", "c"); Stream<String> stream1 = list.stream(); // 2. 배열로부터 생성 String[] arr = {"a", "b", "c"}; Stream<String> stream2 = Arrays.stream(arr); // 3. 숫자 범위로부터 생성 IntStream stream3 = IntStream.range(1, 5); // 1,2,3,4 IntStream stream4 = IntStream.rangeClosed(1, 5); // 1,2,3,4,5 // 4. 직접 값을 지정하여 생성 Stream<String> stream5 = Stream.of("a", "b", "c");
계층 구조
위 코드처럼 .
을통해서 stream()메서드를 호출하여 스트림 인스턴스를 생성할 수 있다.
1번, 2번은 각각 컬렉션과 배열로부터 스트림 인스턴스를 생성하는 예시를 보여준다.
3번과 4번은 계층 구조 사진에 있는 Stream
과 IntStream
인터페이스를 이용해서 인스턴스를 직접 생성하고 있다.
(주로 사용하는 방법은 1번과 2번이다)
중간 연산은 스트림을 다른 스트림으로 변환하는 연산이다.
이전에 소개했던 것처럼 여러 개의 중간 연산을 연결할 수 있다는 특징이 있다.
예제 코드를 통해 자주 사용되는 중간 연산을 한번 살펴보자
Sample Code
// Sample Data List<String> names = Arrays.asList("김자바", "이파이썬", "박코틀린", "최자바", "김자바");
// 1. filter: 조건에 맞는 요소 선택 names.stream() .filter(name -> name.startsWith("김")) .forEach(System.out::println);
// 2. map: 요소를 다른 요소로 변환 names.stream() .map(String::length) .forEach(System.out::println); // 3 4 4 3 3 출력
// 3. sorted: 요소 정렬 names.stream() .sorted() .forEach(System.out::println); // 김자바 김자바 박코틀린 이파이썬 최자바 출력
// 4. distinct: 중복 제거 names.stream() .distinct() .forEach(System.out::println); // 김자바 이파이썬 박코틀린 최자바
자주 사용되는 스트림의 중간 연산을 샘플 코드로 정리해봤다.
필자도 아직 모든 중간 연산자가 어떤 기능을 하는지는 모른다.
하지만, 위와 같은 방식으로 사용할 수 있다는 것을 눈에 익혀두면 좋겠다
그리고 기억해야하는 것은 중간 연산은 지연 연산이라는 것이다.
즉, 최종 연산이 호출되기 전까지는 실제로 실행되지 않는다!
최종 연산은 스트림의 요소를 소모하며 결과를 산출한다.
최종 연산이 수행되면 스트림은 닫히고 더 이상 사용할 수 없다.
이전 코드에서 살펴본 .forEach()
도 사실 최종 연산에 포함되는 것이다.
예시 코드를 통해 정리해보자
Sample Code
// Sample Data List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 1. forEach: 각 요소에 대해 실행 numbers.stream() .forEach(System.out::println);
// 2. count: 요소 개수 반환 long count = numbers.stream().count();
// 3. collect: 결과를 컬렉션으로 변환 List<Integer> evenNumbers = numbers.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList());
// 4. reduce: 요소를 하나로 줄임 int sum = numbers.stream() .reduce(0, (a, b) -> a + b);
위 예시 코드에서는 4개의 최종 연산을 수행하는 메서드를 살펴봤다.
물론 이외에도 많은 스트림 API가 존재하기 때문에 많이 알수록 쉽게 코드를 작성할 수 있을 것이다!
실제로 개발을 하다 보면 스트림을 사용하는 경우가 정말 많다.
특히 필자는 데이터를 필터링하고 변환하는 작업에서 자주 사용한다.
Sample Code
private Map.Entry<Post, Integer> findPostStatistics(Post post) { return postStatistics.entrySet().stream() .filter(entry -> entry.getKey().equals(post)) .findFirst() .orElse(null); }
위 코드는 미니 프로젝트에서 필자가 사용한 게시물 통계 조회 기능의 일부분인데, 스트림을 사용하니 코드가 마치 하나의 문장처럼 읽히는 느낌이다.
이 코드를 처음 보는 개발자들도 해당 메서드의 기능이 "통계 데이터에서 해당 게시물을 찾아서 반환하고, 없으면 null을 반환한다"라고 직관적으로 이해할 수 있을 것이다.
이처럼 스트림은 코드의 가독성을 크게 향상시켜준다는 걸 이해할 수 있다.
처음에는 익숙하지 않을 수 있지만, 한번 익숙해지면 코드의 의도를 파악하기가 훨씬 쉬워진다!
이번 포스팅에서는 자바의 스트림에 대해 알아봤다.
스트림은 컬렉션을 함수형으로 처리할 수 있게 해주는 강력한 기능이라고 배웠다.
필자는 처음에 조금 생소했지만, 쓰다보니 직관적으로 코드를 이해할 수 있어서 좋은 기능인 것 같다.
(그런데 성능적 측면에서 스트림이 엄청 느리다고 한다)
아마 앞으로 자바로 개발을 하게되면 스트림을 적극적으로 활용할 것 같다 👊