스트림은 처음 봐서는 이해하기 어려울 수 있다.

원하는 작업을 스트림 파이프 라인으로 표현하는 것조차 어려울지 모른다.

성공하여  프로그램이 동작하더라도 장점이 무엇인지 쉽게 와닿지 않을 수도 있다.

스트림은 그저 또하나의 API가 아닌 함수형 프로그래밍에 기초한 패러다임이기 때문이다.

스트림이 제공하는 표현력, 속도, (상화에 따라서는) 병렬성을 얻으려면 API는 말할 것도 없고 이 패러다임까지 함께 받아들여야 한다.

 

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

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

순수 함수란 오직 입력만이 결과에 영향을 주는 함수를 말한다.

다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않는다.

 

이렇게 하려면 스트림 연산에 건네는 함수 객체는 모두 부작용이 없어야 한다.

 

다음코드는 텍스트 파일에서 단어별 수를 세어 빈도표로 만드는 일을 한다.

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

위 코드는 스트림 코드라 할 수 없다. 스트림 코드를 가장한 반복적 코드이다. 스트림 API의 이점을 살리지 못하여 같은 기능의 반복적 코드보다 길고, 가독성이 낮아 유지보수에 좋지 못하다.

 

이 코드의 모든 작업이 종단 연산인 forEach에서 일어나는데, 이때 외부 상태(빈도표)를 수정하는 람다를 실행하면서 문제가 생긴다. 

forEach가 그저 스트림이 수행한 연산 결과를 보여주는 일 이상을 하는 것을 보니 나쁜 코드일 것 같은 냄새가 난다.

 

오히려 아래와 같이 수정해주면 보기 깔끔하다.

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

 

앞서와 같은 일을 하지만 이번엔 스트림 API를 제대로 사용했다.

forEach 연산은 종단 연산 중 기능이 가장 적고 가장 '덜' 스트림답다. 대놓고 반복적이라서 병렬화를 할 수도 없다.

forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 말자.

 

이 코드는 컬렉터를 사용하는데, 스트림을 사용하려면 꼭 배워야 하는 새로운 개념이다.

java.util.Collectors 클래스는 메서드를 무려 39개나 가지고 있고, 그중에는 타입 매개변수가 5개나 되는 것도 있다.

 

컬렉터가 생성하는 객체는 일반적으로 컬렉션이며, 그래서 collector라는 이름을 쓴다.

 

컬렉터를 사용하면 스트림의 원소를 손쉽게 컬렉션으로 모을 수 있다.

 

컬렉션은 총 3가지로 toList(), toSet(), toCollection(collectionFactory)가 그 주인공이다.

이들은 차례로 리스트, 집합, 프로그래머가 지정한 컬렉션 타입을 반환한다.

 

다음 코드는 빈도표에서 가장 흔한 단어 10개를 뽑아내는 스트림 파이프라인을 작성한 코드이다.

List<String> topTen = freq.keySet().stream()
	.sorted(comparing(freq::get).reversed())
    .limit(10)
    .collect(toList());
마지막 toList는 Collectors의 메서드이다. 이처럼 Collectors의 멤버를 정적 임포트하여 쓰면 스트림 파이프라인 가독성이 좋아져, 흔히들 이렇게 사용한다.

 

comparing 메서드는 키 추출 함수를 받는 비교자 생성 메서드이다. 키 추출 함수로 쓰인 freq::get은 입력받은 단어(키)를 빈도표에서 찾아(추출) 그 빈도를 반환한다.

그런 다음 가장 흔한 단어가 위로 오도록 비교자(comparing)을 역순(reversed)으로 정렬한다.(sorted)

 

스트림의 각 원소는 키 하나와 값 하나에 연관되어 있다. 그리고 다수의 스트림 원소가 같은 키에 연관될 수 있다.

자세한 내용은 java.util.stream.Collectors 의 API문서를 펼쳐놓고 확인해보길 바란다. (http://bit.ly/2MvTOAR)

 

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

+ Recent posts