[Effective Java] 스트림은 주의해서 사용하라

들어가며

다량의 데이터 처리를 돕기 위해 스트림 API가 자바8 부터 추가되었다. 이 API가 제공하는 추상 개념중 핵심은 두 가지다. 첫째는 스트림은 데이터 원소의 무한 혹은 유한 시퀀스를 뜻한다. 둘째는 스트림 파이프라인은 이 원소들로 수행하는 연산 단계를 표현한다. 스트림 안의 데이터 원소들은 객체 참조나 기본 타입 값이며 기본 타입 값은 int, long, double 세 가지를 지원한다.

스트림 파이프라인은 소스 스트림에서 시작해 종단 연산으로 끝나며 그 사이에 하나 이상의 중간 연산이 있을 수 있다. 각 중간 연산은 스트림을 어떠한 방식으로 변환한다. 중간 연산들은 모두 한 스트림을 다른 스트림으로 변환하는데 변환된 스트림의 원소 타입은 변환 전 스트림의 원소 타입과 같을 수도, 다를 수도 있다. 종단 연산은 마지막 중간 연산이 내놓은 스트림에 최후 연산을 가한다.

스트림 파이프라인은 지연 평가(lazy evaluation)된다. 평가는 종단 연산이 호출될 때 이뤄지며 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다. 종단 연산이 없는 스트림 파이프라인은 아무 일도 하지 않는 명령어인 no-op와 같다. 스트림 API는 메소드 연쇄를 지원하는 플루언트 API다. 기본적으로 스트림은 순차적으로 수행되나 병렬로 수행을 원할 시 parallel 메소드를 호출해주면 된다. 다음은 스트림을 사용하는 예이다.

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
Stream<String> words = Arrays.stream(new String[]{"abc", "bca" , "bbb", "bvc"});
words.collect(groupingBy(word -> alphabetize(word))).values().stream().forEach(g -> System.out.println(g.size() + " : " + g));
}

private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}

위 코드는 스트림 변수를 알파벳 아나그램으로 만들어 그룹을 지어 출력하는 열할을 수행한다. 스트림 파이프라인은 되풀이되는 계산을 함수 객체로 표현한다. 반면 반복 코드에서는 코드 블록을 사용해 표현한다. 그런데 함수 객체로는 할 수 없지만 코드 블록으로는 할 수 있는 일이 있다. 다음이 그 예다.

  • 코드 블록에서는 범위 안의 지역변수를 읽고 수정할 수 있지만 람다에서는 final이거나 사실상 final인 변수만 읽을 수 있고 지역변수의 수정은 불가하다.
  • 코드 블록에서는 return, break, continue를 통해 반복문의 제어가 가능하지만 람다는 불가능하다.
  • 코드 블록에서는 메소드 선언에 명시된 검사 예외를 던질 수 있지만 람다는 불가능하다.

반대로 다음은 스트림을 적용하기 적합한 예이다.

  • 원소들의 시퀀스를 일관되게 반환한다.
  • 원소들의 시퀀스를 필터링한다.
  • 원소들의 시퀀스를 하나의 연산을 사용해 결합한다.
  • 원소들의 시퀀스를 컬렉션에 모은다.
  • 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다.

한편 스트림으로 처리하기 어려운 일도 있다. 예를 들어 한 데이터가 파이프라인의 여러 단계를 통과할 때 이 데이터의 각 단계에서의 값들에 동시 접근하기는 어렵다.

Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/16/Languages/Effective%20JAVA/item45/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.