본문 바로가기

개발/이펙티브 자바

Effective Java ( 이펙티브 자바 ) - 아이템 46

스트림에서는 부작용 없는 함수를 사용하라


  스트림은 처음 봐서는 이해하기 어려울 수 있다. 스트림은 그저 또 하나의 API가 아닌, 함수형 프로그래밍에 기초한 패러다임이다. 

  스트림 패러다임의 핵심은 계산을 일련의 변환으로 재구성 하는 부분이다. 이때 각 변화 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 순수 함수여야 한다. 

순수 함수란 오직 입력만이 결과에 영향을 주는 함수를 말한다. 다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않는다. 

이렇게 하려면 (중간 단계든 종단 단계든) 스트림 연산에 건네는 함수 객체는 모두 부작용(side effect)이 없어야 한다.

  다음은 주위에서 종종 볼 수 있는 스트림 코드로, 텍스트 파일에서 단어별 수를 세어 빈도표로 만드는 일을 한다.

Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
   words.forEach(word -> {
      freq.merge(word.toLowerCase(), 1L, Long::sum);
   });
}

스트림, 람다, 메서드 참조를 사용했고, 결과도 올바르지만, 절대 스트림 코드라 할 수 없다. 스트림 코드를 가장한 반복적 코드다. 스트림 API의 이점을 살리지 못하여 같은 기능의 반복적 코드보다 길고, 읽기 어렵고, 유지보수에도 좋지 않다. forEach에서 외부 상태를 수정하는 람다를 실행하면서 문제가 생긴다. 

Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
   freq = words
         .collect(groupingBy(String::toLowerCase, counting()));
}

앞서와 같은 일을 하지만, 이번엔 스트림API를 제대로 사용했다. 그뿐만 아니라 짧고 명확하다. 그뿐만 아니라 짧고 명확하다. forEach 연산은 종단 연산 중 기능이 가장 적고 가장 '덜' 스트림답다. 대놓고 반복적이라서 병렬화할 수도 없다. forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 말자. 

 

  collector는 스트림을 사용하려면 꼭 배워야하는 새로운 개념이다. 익숙해지기 전까지는 Collector 인터페이스를 잠시 잊고, 그저 축소 전략을 캡슐화한 블랙박스 객체라고 생각하자. 취합이다. 일반적으로 컬렉션을 생성한다. 

 

  지금까지 배운 지식을 활용해 빈도표에서 가장 흔한 단어 10개를 뽑아내는 스트림 파이프라인을 작성해보자.

List<String> topTen = freq.keySet().stream()
      .sorted(comparing(freq::get).reversed())
      .limit(10)
      .collect(toList());

 

Collectors의 나머지 36개 메서드들도 알아보자. 

가장 간단한 맵 수집기는 toMap(keyMapper, valueMapper)로, 스트림 원소를 키에 매핑하는 함수와 값에 매핑하는 함수를 인수로 받는다. 

private static final Map<String, Operation> stringToEnum = 
   Stream.of(values()).collect(
      toMap(Object::toString, e -> e));

  스트림 원소 다수가 같은 키를 사용한다면 파이프라인이 IllegalStateException을 던지며 종료될 것이다. 

  더 복잡한 형태의 toMap이나 groupingBy는 이런 충돌을 다루는 다양한 전략을 제공한다. 예컨대 toMap에 키 매퍼와 값 매퍼는 물론 병합 함수까지 제공할 수 있다. 병합 함수의 형태는 BinaryOperator<U>. 

 

그 외에도 다양한 스트림 수집기가 존재한다.

 

스트림 파이프라인 프로그래밍의 핵심은 부작용 없는 함수 객체에 있다. 스트림뿐 아니라 스트림 관련 객체에 건네지는 모든 함수 객체가 부작용이 없어야 한다. 종단 연산 중 forEach는 스트림이 수행한 계산 결과를 보고할 때만 이용해야 한다. 스트림을 올바로 사용하려면 수집기를 잘 알아둬야 한다. 가장 중요한 수집기 팩터리는 toList, toSet, toMap, groupingBy, joining이다.