Java - 45) 스트림은 주의해서 사용하라

<이펙티브 자바>

Posted by Yan on June 15, 2022

이전에 코딩해놓은 서비스의 코드를 다시 보면서 stream으로 리팩토링하는 작업을 하고 있다. 놀랍도록 간결해지고 있다!

스트림은 주의해서 사용하라

스트림을 제대로 사용하면 프로그램이 짧고 깔끔해지지만, 잘못 사용하면 읽기 어렵고 유지보수도 힘들어진다.

Stream API

Stream API는 다량의 데이터 처리 작업을 돕고자 탄생했다.

  1. 스트림은 데이터 원소의 유한 혹은 무한 시퀀스Sequence를 뜻한다.
  2. 스트림 파이프라인은 원소들로 수행하는 연산단계를 표현하는 개념이다.

스트림 파이프라인

  • 스트림 원소들: 컬렉션, 배열, 파일, 정규표현식 패턴 matcher, 나수 생성기, 다른 스트림 등등.
  • 스트림 안의 데이터 원소들은 객체 참조나 기본 타입 값(int, long, double 세 가지 지원)이다.
  • 메소드 연쇄를 지원하는 Fluent API다. 파이프라인 하나를 구성하는 모든 호출을 연결하여 하나의 표현식으로 완성할 수 있다.
  • 기본적으로 순차적으로 수행된다. 병렬로 실행하려면 파이프라인을 구성하는 스트림 중 하나에서 parallel 메소드를 호출해주면 된다.

연산 방식

  • 소스 스트림에서 시작해, 종단 연산으로 끝나고, 그 사이에 하나 이상의 중간 연산이 있을 수 있다.
  • 중간 연산은 스트림을 어떠한 방식으로 변환한다. 중간 연산들은 모두 한 스트림을 다른 스트림으로 변환하는데, 변환된 스트림의 원소 타입은 변환 전 스트림의 원소 타입과 같을 수도 있고 다를 수도 있다.
  • 종단 연산이 없는 스트림 파이프라인은 아무 일도 하지 않는다. 빼먹지 말자.

지연평가

스트림 파이프라인은 지연 평가lazy evaluation된다.

  • 평가는 종단 연산이 호출될 때 이루어지고, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다.
  • 지연 평가는 무한 스트림을 다룰 수 있게 해주는 열쇠다.

아나그램 예제

스트림을 사용하지 않고 구현

✅ 사전 파일에서 단어를 읽어 사용자가 지정한 문턱값보다 원소 수가 많은 아나그램 그룹을 출력.
(아나그램 : 철자를 구성하는 알파벳이 같고 순서만 다른 단어)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Anagrams {
  public static void main(String[] args) throws IOException {
    File dictionary = new File(args[0]);
    int minGroupSize = Integer.parseInt(args[1]);

    Map<String, Set<String>> groups = new HashMap<>();

    //사용자가 명시한 사전 파일에서 각 단어를 읽어 맵에 저장한다.  
    try (Scanner s = new Scanner(dictionary)) {
      while (s.hasNext()) {
        String word = s.next();
        groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>())
          .add(word);
        // 사전 하나를 모두 처리하고 나면 각 집합은 사전에 등재된 아나그램을 모두 담은 상태가 된다.
      }
    }
    
    // 맵의 values() 메소드로 아나그램 집합들을 얻어 원소 수가 문턱값보다 많은 집합들을 출력한다.
    for (Set<String> group : groups.values()) {
      if (group.size() >= minGroupSize) {
        System.out.println(group.size() + ": " + group);
      }
    }
  }

  //맵의 키는 그 단어를 구성하는 철자들을 알파벳순으로 정리한 값이다. 맵의 값은 같은 키를 공유한 단어들을 담은 집합이다.  
  private static String alphabetize(String s) {
    char[] a = s.toCharArray();
    Arrays.sort(a);
    return new String(a);
  }
}
  • computeIfAbsent() : 맵 안에 키가 있는지 찾고
    • 키가 있으면 단순히 그 키에 매핑된 값을 반환
    • 키가 없으면 건네진 함수 객체를 키에 적용하여 값을 계산해낸 다음 그 키와 값을 매핑해놓고, 계산된 값을 반환
    • 각 키에 다수의 값을 매핑하는 맵을 쉽게 구현할 수 있다.
스트림을 적절히 활용하여 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Anagrams {
  public static void main(String[] args) throws IOException {
    Path dictionary = Paths.get(args[0]);
    int minGroupSize = Integer.parseInt(args[1]);

    try(Stream<String> words = Files.lines(dictonary)) {
      words.collect(groupingBy(word -> alphabetize(word)))
        .values().stream() // Stream<List<String>> 스트림 열기
        .filter(group -> group.size() >= minGroupSize)
        .forEach(group -> System.out.println(group.size() + ": " + group));
    }
  }

  private static String alphabetize(String s) {
    char[] a = s.toCharArray();
    Arrays.sort(a);
    return new String(a);
  }
}
  • 중간 연산 없는 스트림 파이프라인.
  • 종단 연산에서는 모든 단어를 수집해 맵으로 모은다.
  • forEach 종단 연산은 살아남은 리스트를 출력한다. 리스트 중 원소가 minGroupSize보다 적은 것은 필터링되어 무시된다.
reference

이펙티브 자바