Effective Java : 아이템 46. 스트림에는 부작용 없는 함수를 사용하라.

    아이템 46. 스트림에는 부작용 없는 함수를 사용하라.

    • 스트림은 함수형 프로그래밍의 패러다임을 의미한다. 이 패러다임을 받아들여야 Stream의 이점을 가져갈 수 있음.
    • 스트림은 일련의 계산을 변환(Transforming)하며 순차적으로 처리한다. 일련의 계산들은 순수함수들에 의해서 처리되어야 함. 
    • forEach()를 이용해 외부 객체의 상태를 업데이트 하는 작업을 하지 마라. (순수함수가 아니게 됨)
      • 대신, Stream Collectors가 제공하는 함수 객체들을 이용해 업데이트 대신 새로운 Collection을 만들어라.
    • Stream을 사용할 때는, Collectors가 제공하는 함수 객체를 잘 사용하자.
      • toMap()
      • toSet()
      • toCollection()
      • groupingBy()
      • groupingByConcurrentHashMap()
      • partitioningBy()
      • minBy(), maxBy()
      • counting()
      • joining()

     


    스트림이란?

    • 스트림은 함수형 프로그래밍에 기초한 패러다임이다. 이 패러다임을 받아들여야 스트림의 표현력, 속도에 대한 이점을 가져갈 수 있음.
    • 스트림 패러다임의 핵심은 계산을 일련의 변환(transformation)으로 재구성하는 방식으로 동작함.

    스트림 패러다임의 핵심은 계산을 일련의 변환으로 재구성하는 부분이다. 이것은 스트림의 각 단계들이 계산을 수행하며, 계산의 결과를 집계해야한다는 것을 의미한다. 일반적인 함수형 프로그래밍이 그렇듯, 스트림 패러다임에서 사용되는 함수 객체들은 '순수 함수'여야만 한다. 

    순수함수는 오직 입력만이 결과에 영향을 주는 함수를 말한다. 즉, 다른 가변 상태를 참조하지 않고, 함수 객체 스스로도 다른 상태를 변경하지 않는다. 스트림에 전달되는 함수 객체가 모두 순수함수여야 한다는 것은 스트림은 '사이드 이펙트'가 없는 처리 결과를 보여준다는 것이다. 스트림의 각 단계에 입력으로 전달되지 않은 모든 객체들의 어떠한 상태도 스트림은 변경하지 않는 것을 의미한다. 

     


    순수함수가 아닌 스트림 코드, 그리고 수정

    앞서서 스트림에 전달되는 함수 객체는 모두 순수함수여야 한다고 했다. 그런데 그렇지 않은 경우를 쉽게 찾아볼 수 있다. 아래 코드를 보자.

    // Stream을 사용했으나 순수 함수가 아닌 경우.
    public static void main(String[] args) {
        final String file = "data data2 data3 data4 data";
    
        final Map<String, Long> freq = new HashMap<>();
        try (Stream<String> words = new Scanner(file).tokens()) {
            words.forEach(word ->
                freq.merge(word.toLowerCase(), 1L, Long::sum)); // stream에서 외부 객체(freq)의 상태를 변경함.
        }
        System.out.println(freq);
    }
    
    >>>
    {data=2, data4=1, data3=1, data2=1}

    이 코드는 Stream을 사용했지만, Stream 내부에서 외부 객체 freq를 참조해서 상태를 수정하고 있다. Stream은 순수함수만을 사용해야하는데, 그렇지 않았기 때문에 Stream이 잘못 사용된 케이스로 볼 수 있다. 이것을 순수함수만 사용한 Stream으로 수정하면 다음과 같다.

    // 순수함수만으로 Stream을 사용한 경우.
    public static void main(String[] args) {
        final String file = "data data2 data3 data4 data";
    
        final Map<String, Long> freq;
        try (Stream<String> words = new Scanner(file).tokens()) {
            freq = words.collect(groupingBy(s -> s, counting())); // stream에서 외부 객체(freq)의 상태를 변경하지 않음..
        }
        System.out.println(freq);
    }
    
    >>>
    {data=2, data4=1, data3=1, data2=1}

    기존에는 forEach() 내부에서 freq.merge()를 호출하며 외부 객체 freq의 내부 상태를 수정하는 작업을 수행했다. 이 부분이 순수함수를 사용해야 한다는 관점에서 잘못되었기 때문에 Stream의 Collectors에서 제공하는 함수 객체들만 이용해서 외부 객체의 참조 없이 동일한 결과를 가져오도록 했다.

     


    forEach()의 쓰임. 

    forEach()를 이용해서 외부 객체들을 참조하고 외부 객체들의 상태를 업데이트 하고 있는 코드가 있다면 그 즉시 수정이 필요하다. forEach()로 외부 Collection을 업데이트 하기 보다는, Stream API가 제공하는 Collectors 함수 객체들을 이용해 수정이 필요하다. 

    public void forEachShouldbeUsedLikedThis() {
        final String file = "data data2 data3 data4 data";
        try (Stream<String> words = new Scanner(file).tokens()) {
            words.collect(groupingBy(s -> s, counting()))
                    .forEach((key, value) -> publisher.publishEvent(new MyEvent(key)));
                    // publishEvent()를 통해 계산 결과를 보고만 함.
        }
    }

    forEach()는 계산의 결과를 보고하는 형태로만 사용해야 한다. 바꿔이야기하면 forEach()는 객체들의 계산을 위해서 사용되어서는 안된다. 위의 코드에서는 Stream을 통해 계산이 완료되면, publisher.publishEvent() 메서드를 호출해서 계산 결과를 다른 인스턴스들에게 보고하는 역할로 사용했다. 

     


    Collectors의 다양한 사용 방법

    collectors의 다양한 사용방법은 이곳에 따로 정리해두었다. 

    댓글

    Designed by JB FACTORY