Home [Effective Java] Item46. 스트림에서는 부작용 없는 함수를 사용하라!
Post
Cancel

[Effective Java] Item46. 스트림에서는 부작용 없는 함수를 사용하라!

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

Intro

  • 스트림은 처음 봐서는 이해하기 어려울 수 있다. 원하는 작업을 스트림 파이프라인으로 표현하는 것조차 어려울지 모른다.
  • 스트림은 그저 또 하나의 API가 아닌, 함수형 프로그래밍에 기초한 패러다임이다.
    • 스트림이 제공하는 표현력, 속도, 병렬성을 얻으려면 API는 말할 것도 없고 이 패러다임까지 받아들여야 한다.

스트림

  • 스트림 패러다임의 핵심은 계산을 일련의 변환으로 재구성하는 부분이다.

  • 각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 순수 함수여야 한다.

    순수함수 : 오직 입력만이 결과에 영향을 주는 함수

  • 이렇게 구성하기 위해서는 (중간 단계든 종단 단계든) 스트림 연산에 건네는 함수 객체는 모두 부작용이 없어야 한다.

스트림 패러다임을 이해하지 못한 예

1
2
3
4
5
6
Map<String, String> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
  	words.forEach(word -> {
      	freq.merge(word.toLowerCase(), 1L, Long::sum);
    });
}
  • 스트림, 람다, 메서드 참조를 사용했다. 결과도 올바르다. 하지만 절대 스트림 코드라 할 수 없다.
    • 스트림을 가장한 반복적 코드이기 때문이다.
    • 종단 연산인 forEach에서 그저 스트림이 수행한 연산 결과를 보여주는 일 이상을 실행한다.
1
2
3
4
Map<String, String> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
  	freq = words.collect(groupingBy(String::toLowerCase, counting()));
}
  • 스트림 API를 제대로 사용하는 형태로 변환했다.
  • 자바프로그래머라면 for-each 반복문 사용에 익숙할 것이라 스트림의 forEach 종단 연산을 잘못 사용하는 경우가 더러 있을 것이다.
    • forEach 종단 연산은 종단 연산 중 기능이 가장 적고 덜 스트림스럽다. 대놓고 반복적이라 병렬화도 할 수 없다.
      • forEach 연산은 스트림 계산 결과를 보고할 때만 사용하자. 계산에는 사용하지 말자.

수집기(Collector)

  • 스트림을 사용하려면 꼭 배워야 하는 새로운 개념이다.
  • java.util.stream.Collectors 클래스는 메서드를 무려 39개나 가지고 있고, 그 중 타입 매개변수가 5개나 되는 것도 있다.
  • 익숙해 지기 전까지는 Collector 인터페이스를 잠시 잊자. 그저 축소(reduction) 전략을 캡슐화한 블랙박스 객체라고 생각하자.
  • 수집기가 생성하는 객체는 일반적으로 컬렉션이라 collector라는 이름을 쓴다.

개념

  • 수집기를 사용하면 스트림의 원소를 손쉽게 컬렉션으로 모을 수 있다.
  • 수집기는 총 세가지다.
    • toList() : 리스트 반환
    • toSet() : 집합 반환
    • toCollection(collectionFactory) : 프로그래머가 지정한 컬렉션 타입 반환

예시

1
2
3
4
List<String> topTen = freq.keySet().stream()
    .sorted(comparing(freq::get).reversed())
    .limit(10)
    .collect(toList());
  • comparing(freq::get).reversed()
    • comparing 메서드는 키 추출 함수를 받는 비교자 생성 메서드다.
    • 한정적 메서드 참조인 freq::get은 입력받은 단어(키)를 빈도효에서 찾아(추출) 그 빈도를 반환한다.
    • reversed 메서드는 가장 흔한 단어가 위로 오도록 역순으로 정렬한다.

Collections의 여러 메서드 소개

  1. toMap

    • 스트림 원소를 키에 매핑하는 함수와 같에 매핑하는 함수를 인수로 받는다.
    1
    2
    
    private static final Map<String, Operation> stringToEnum =
            Stream.of(values()).collect(toMap(Object::toString, e -> e));
    
    • toMap 수집기를 사용해 문자열을 열거 타입 상수에 매핑한 예이다.
      • toMap 형태는 스트림의 각 원소가 고유한 키에 매핑되어 있을 때 적합하다.
      • 스트림 원소 다수가 같은 키를 사용한다면 파이프라인이 IllegalStateException을 던진다.
    1
    2
    
    Map<Artist, Album> topHits = albums.collect(
        toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
    
    • 각 키와 해당 키의 특정 원소를 연관 짓는 맵을 생성하는 수집기 예제다.
      • 예를 들어, 다양한 음악가의 앨범들을 담은 스트림에서, 음악가와 그 음악가의 베스트 앨범을 연관짓고 싶은 경우
    1
    
    toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
    
    • 마지막에 쓴 값을 취하는 수집기 예제다.
      • 인수가 3개인 toMap은 충돌이 나면 마지막 값을 취하는 Collector를 만들 때 유용하다.
  2. groupingBy

    • 입력으로 분류 함수를 받고 출력으로는 원소들을 카테고리별로 모아 놓은 맵을 담은 수집기를 반환한다.
      • 분류함수는 입력받은 원소가 속하는 카테고리를 반환한다. 이 카테고리는 맵의 키로 쓰인다.
    1
    
    words.collect(groupingBy(word -> alphaetize(word))); // 아이템 45에서 다룬 예제
    
    • groupingBy가 반환하는 수집기가 리스트 외의 값을 같는 맵을 생성하게 하려면, 분류 함수와 함께 다운스트림 수집기도 명시해야 한다.

      • 다운스트림 수집기의 역할은 해당 카테고리의 모든 원소를 담은 스트림으로부터 값을 생성하는 일이다.
      1
      
      Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting())); // 다운스트림 예제
      
      • 다운스트림 수집기로 counting()을 건네는 예제다. 이렇게 하면 각 카테고리(키)를 해당 카테고리의 원소 개수와 매핑한 맵을 얻는다.
      • counting 메서드가 반환하는 수집기는 다운스트림 전용이다. Stream의 count 메서드를 직접 사용해 같은 기능을 수행할 수 있으니 collect(counting()) 형태를 사용할 일은 전혀 없다.
  3. minBy, maxBy

    • Collectors에 정의되어 있지만 ‘수집’과는 전혀 상관 없다.
    • 인수로 받은 비교자를 이용해 스트림에서 값이 가장 작거나 큰 원소를 찾아 반환한다.
  4. joining

    • 이 메서드는 CharSequence 인스턴스의 스트림에만 적용할 수 있다.
    • 매개변수가 없는 joining은 단순히 원소들을 연결(concatenate)하는 수집기를 반환한다.
    • 인수 하나짜리 joining은 구분문자(delimiter)를 매개변수로 받는다.
    • 인수 세개짜리 joining은 구분문자, 접두문자(prefix), 접미문자(suffix)를 받는다.
      • 예를 들어 구분을 ‘,’로, 접두를 ‘[‘로, 접미를 ‘]’로 지정하면, ‘[사과, 배, 포도]’와 같이 마치 컬렉션을 보는 듯한 문자열 생성도 가능하다.

핵심 정리

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

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

[Effective Java] Item47. 반환 타입으로는 스트림보다 컬렉션이 낫다!

Comments powered by Disqus.