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

스트림은 그저 또 하나의 API가 아닌, 함수형 프로그래밍에 기초한 패러다임이기 대무이다. 스트림이 제공하는 표현력, 속도를 얻으려면 API와 이 패러다임까지 함께 받아 드려야 한다.

계산을 일련의 변환으로 재구성

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

순수 함수

순수함수란 오직 입력만이 결과에 영향을 주는 함수를 말한다. 다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않는다. 이렇게 하려면 스트림 연산에 건네는 함수 객체는 모두 부작용이 없어야 한다.

순수하지 않은 함수

public class Main {

    private static int factor = 2; // 외부 상태

    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        // 순수 함수가 아닌 예: 외부 상태에 의존
        List<Integer> multipliedNumbers = numbers.stream()
                .map(n -> n * factor) // factor에 의존
                .collect(Collectors.toList());

        System.out.println(multipliedNumbers); // [2, 4, 6, 8, 10]

        factor = 3; // 외부 상태 변경

        multipliedNumbers = numbers.stream()
                .map(n -> n * factor) // 변경된 factor에 의존
                .collect(Collectors.toList());

        System.out.println(multipliedNumbers); // [3, 6, 9, 12, 15]
    }
}

위 함수는 순수하지 않다. 외부 상태에 의존하기 때문이다. 외부 상태가 변경되면 결과가 바뀐다는 불확실성을 만든다. 순수하지 않은 함수의 단점은 아래와 같다.

  1. 병렬 처리의 어려움: 순수 함수가 아닌 함수는 외부 상태에 의존하거나 외부 상태를 변경하기 때문에 병렬 스트림에서 사용할 경우 동시성 문제가 발생할 수 있다.
  2. 예측 불가능성: 함수의 출력이 외부 상태에 따라 달라질 수 있어 코드의 동작을 예측하기 어렵다.
  3. 테스트의 어려움: 함수가 외부 상태를 변경하거나 외부 상태에 의존하므로 단위 테스트를 작성할 때 함수의 입력뿐만 아니라 외부 상태도 고려해야 한다.

순수함수

public class PureFunctionExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        // 순수 함수 사용
        List<Integer> squaredNumbers = numbers.stream()
                                              .map(n -> n * n) // map() 메서드에 전달된 람다식은 순수 함수
                                              .collect(Collectors.toList());

        System.out.println(squaredNumbers); // [1, 4, 9, 16, 25]
    }
}

위 함수는 순수하다. 결과에 영향을 미치는 값은 입력 값이 유일하다. 순수함수의 장점은 아래와 같다.