[Effective Java] 7장 람다와 스트림 (2부)
정리
아이템 45. 스트림을 사용해야 처리할 수 있는 일이 있고, 반복 방식이 더 알맞은 일도 있다. 그리고 수많은 작업이 이 둘은 조합했을 때 가장 효율적으로 해결된다. 어느 쪽을 선택하는 확고부동한 규칙은 없지만 참고할 만한 지침 정도는 있다. 어느 쪽이 나은지가 확연히 드러나는 경우가 많겠지만, 아니더라도 방법은 있다. 스트림과 반복 중 어느 쪽이 더 나은지 확신하기 어렵다면 둘다 써보고 나은 쪽을 선택하는 것이 좋다.
아이템 46. 스트림 파이프라인 프로그래밍의 핵심은 스트림과 스트림 관련 객체에 건네지는모든 함수 객체가 부작용 없어야한다. 종단 연산 중 forEach는 계산 할때가 아닌 스트림이 수행한 계산 결과를 비교할 때만 이용해야한다. 스트림을 올바르 사용하려면 수집기를 알아야한다. 예로는 toList, toSet, toMap, groupingBy, joining 이 있다.
아이템 47. 원소 시퀀스를 반환하는 메서드를 작성할 때, 이를 스트림으로 처리하기를 원하는 사용자와 박복으로 처리하길 원하는 사용자가 있을 수도 있기 때문에 양쪽을 만족시킬 수 있어야한다. 되도록이면 컬렉션을 반환하는 것이 좋다. 반환하는 원소양이 적다면 ArrayList 같은 표준 컬렉션에 담아 반환하거나 전용 컬렉션을 구현하는 것이 좋다.
아이템 48. 계산도 올바로 수행하고 성능도 빨라질 거라는 확신 없이는 스트리 파이프라인 병렬화는 시도하지 않는 것이 좋다. 잘못 사용할 경우 프로그램 오동작하거나 성능에 악영향이 생긴다. 병렬화를 했다면 선응지표를 유심히 관찰하고, 성능이 나아졌는지 확인 후 사용하는 것이 좋다.
내용
아이템 45 스트림은 주의해서 사용하라
● 스트림은함수형 프로그래밍에 기초한 패더라임이다.
● 스트림 API는 대량의 데이터 처리하(순차던 병렬이던)을 돕고자 자바 8에서 새롭게 추가되었다.
● API가 제공하는 추상 개념 중 핵심은 두 가지가 있다.
1. 스트림(stream)은 데이터 원소의 유한 혹은 무한 시퀀스(squence)를 뜻한다.
2. 스트림 파이트라인(stream pipeline)은 이 원소들로 수행하는 연산 단계를 표현하는 개념이다.
|
● 스트림의 원소들은 어디로부터든 올 수 있다. 컬렉션, 배열, 파일, 정규표현식 패턴 매처(Matcher), 난수 생성기, 혹은 다른 스트림이 있다.
스트림 안의 데이터 원소들은 객체 참조나 기본 타입 값이다. 기본 타입 값으로 int, long, double 3가지 지원가능하다.
● 스트림 파이프라인은 소스 스트림에서 시작해 종단 연산(terminal operation)으로 끝난다. 사이에는 중간 연산(intermediate operation)이 있을 수도 있다.
각 중간 연산은 어떠한 방식으로 변환(transform)한다.
● 스트림 파이프라인은 지연 평자(lazy evaluation)된다. 평가는 종단 연산이 호출될 때 이뤄지며, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다.
이런 지연 평가가 무한 스트림을 다룰 수 있게 한다. 종단 연산이 없는 스트림 파이프라인은 아무 일도 하지 않는 명령어인 no-op과 같으므로, 종단 연산을 빼먹지말자.
● 스트림 API는 메서드 연쇄를 지원하는 플루언트 API다. 즉, 파이프라인 하나를 구성하는 모든 호출을 연결하여 단 하나의 표현식으로 완성할 수 있다.
기본적으로 스트림 파이프라인은 순차적으로 수행된다. 하지만 파이프라인을 구성하는 스트림 중 하나에서 parallel 메서드를 호출하면 병렬로 사용할 수 있지만 효과를 볼 수 있는 상황이 많지는 않다.
● 스트림은 다재다능하지만 잘못 사용할 경우 가독성이 떨어지고 유지보수도 힘들다.
● 람다 매개변수의 이름은 주의해서 정해야한다. 람다에서는 타입 이름을 자주 생갹하므로 매개변수 이름을 잘 지어야 스트림 파이프라인의 가독성이 유지된다.
도우미 매서드를 적절히 활용하는 일의 중요성은 일반 반복 코드에서보다는 스트림 파이프라인에서 훨씬 크다.
● char용 스트림은 자바에서 지원하지 않기 때문에 사용하게 되면 느려진다. char값을 처리할 때는 스트림을 삼가하는 편이 낫다.
● 다음 경우에는 스트림를 사용하는 것이 좋다.
1. 원소들의 시퀀스를 일관되게 변환한다.
2. 원소들의 시퀀스를 필터링한다.
3. 원소들의 시퀀스를 하나의 연산을 사용해 결합한다.(더하기, 연결하기, 최솟값 구하기등)
4. 원소들의 시퀀스를 컬렉션에 모은다. (아마도 공통된 속성을 기준으로 묶음)
5. 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다.
|
● 한 데이터가 파이프라인의 여러 단계를 통과할 때 이 데이터의 각 단계에서의 값들을 동시에 접근하기 어려운 경우, 스트림으로 처리하기 어렵다.
※ 질문
스트림을 사용하면 반복문보다 시간복잡도를 줄일 수 있을까??
아이템 46 스트림에서는 부작용 없는 함수를 사용하라
● 스트림 패러다임의 핵심은 계산을 일련의 변환으로 재구성하는 부분이다. 이때 각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 순수 함수여야한다.
● 다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않는다. 이렇게 하려면 스트림 연산에서 건네는 함수 객체는 모두 부작용(side effect)이 없어야한다.
● 스트림의 종단 연산인 forEach 연산은 스트림 계산 결과를 보고할 때문 사용하고 계산하는데 사용하지말자.
● 수집기를 사용하면 스트림의 원소를 손쉽게 컬렉션으로 모을 수 있다. toList(), toSet(), toCollection(collectionFactory)
예
<toList()>
List<String>topTen = freq.ketSet().stream().sorted(comparing(freq::get).reversed()).limit(10).collect(toList());
해설 : sorted에 넘긴 비교사, 즉 comparing(freq::get).reversed()는 comparing 메서드는 키 추출 함수를 받는 비교사 생성 메서드다. 그리고 키 추출 함수로 쓰인 freq::get(한정적 메서드 참조)은 입력받은 단어를 빈도표에 찾아서 빈도를 반환한다.
<toMap()>
// 스트림의 각 원소가 고유한 키에 매핑되어 있을 때 적합
private static final Map<String, Operation> stringToEnum = Stream.of(values()).collect(toMap(Object::toString, e -> e));
// 각 키와 해당 키의 특정 원소를 연관짓는 맵 생성하는 수집기
Map<Artist, Album> topHits = albums.collect(toMap(Albums::artist, a -> a, maxBy(comparing(Album::sales))));
// 마지막에 쓴 값을 취하는 수집기
toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
<groupingBy>
// 입력으로 분류 함수를 받고 출력으로는 원소들을 카테고리별로 모아 놓은 맵을담은 수집기를 반환한다.
word.collect(groupingBy(word -> alphabetize(word)))
// 다운스트림 수집기에 더해 맵 팩터리도 지정할 수 있게 한다. Map과 안에 담긴 컬렉션의 타입을 모두 지정할 수 있다.
Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));
<maxBy>, <minBy>
// 인수로 받은 비교자를 이용해 스트림에 존재하는 최대 또는 최소 값을 반환한다.
<joining>
// 문자열 등 CharSquence 인스턴스의 스트림에만 적용할 수 있다. 매개변수가 없는 joining은 단순히 원소를 연결하는 수집기를 반환한다.
// 인수가 한개인 경우, 구분자를 매개변수로 받는다.
// 인수가 3개인 경우, 접두, 접미까지 매개변수로 받는다.
TIP : Collectors의 멤버를 정적 임포트하여 쓰면 스트림 파이프라인 가독성이 좋아진다.
|
● 간단한 맵수집기는 toMap(keyMapper, valueMapper)로 스트림 원소를 키에 매핑하는 함수와 값에 매칭하는 함수를 인수로 받는다.
● collect(counting()) 형태로 사용할 일은 전혀 없다.
아이템 47 반환 타입으로는 스트림보다 컬렉션이 낫다
· Collection 인터페이스는 Iterable의 하위 타입이고, stream 메서드도 제공함으로 stream을 사용하는 유저와 for-each로 반복하기를 원하는 유저를 동시에 지원할 수 있다. 원소 시퀀스를 반환한느 공개API의 반환 타입은 Collection이나 그 하위타입을 쓰는 것이 좋다.
· 시퀀스를 반환할 때, 시퀀스 크기가 메모리에 올려도 될 정도로 안전한지 확인하고 작다면, ArrayList나 HashSet 같은 표준 컬렉션 구현체를 반환하는 것이 좋다.
· 스트림은 반복을 지원하지 않는다. Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함할 뿐만 아니라, 정의한 방식대로 동작한다. 하지만 지원하지 않는 이유는 Stream이 Iterable을 확장하지 않기 때문이다.
· 스트림으로 반복을 사용하기 위해서는 어댑터 메서드를 사용하면 된다.
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
// 처리
}
|
아이템 48 스트림 병렬화는 주의해서 적용하라
· 스트림 병렬화를 사용하게 되면 느려지는 경우가 있는데, 스트림 라이브러리가 파이프라인을 병렬화하는 방법을 찾아내지 못했기 때문이다. 데이터 소스가 Stream.iterate거나 중간 연산으로 limit를 쓰면 파이프라인 병렬화로는 성능 개선을 기대할 수 없다.
· 병렬화의 효과가 가장 좋은 경우는 스트림 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스거나 배열, int 범위, long 범위일 때이다. 이유는 데이터를 원하는 크기로 쉽게 나눌수 있어서 스레드에 분배하기 좋고 순차적으로 실행할 때의 참조 지역성이 뛰어나기 때문이다.
· 참조 지역성이 낮으면 스레드는 데이터가 주 메모리에서 캐시 메모리로 전송되어 오기를 기다리게 된다.
· 종단 연산 중 병렬화에 가장 적합한 것은 축소(reduction)이다. 축소는 파이프라인에서 만들어진 모든 원소를 하나로 합치는 작업이다. 단 가변 축소는 적합하지 않다.
· Stream의 reduce 메서드 중 하나, 혹은 min, max, count, sum 같이 완성된 형태로 제공되는 메서드 중 하나를 선택해 수행한다. anyMatch, allMatch, noneMatch처럼 조건에 맞으면 반환되는 메서드도 병렬화에 적합하다.
· 스트림을 잘못 병렬화하면 응답 불가를 포함해 성능이 나빠질 수 있고 결과 자체가 잘못되거나 예상 못한 동작이 발생할 수 있다. 이런 것을 안전 실패(save failure)라 한다.
· 스트림 병렬화는 오직 성능 최적화 수단임을 기억해야 한다. 변경 전후로 반드시 성능을 테스트하여 병렬화를 사용할 가치가 있는지 확인해야한다. 운영시스템과 흡사한 환경에서 테스트하는 것이 좋다. 보통은 별렬 스트림 파이프라인도 공통의 포크-조인풀에서 수행되므로, 잘못된 파이프라인 하나가 시스템의 다른 부분의 성능에까지 악영향을 줄 수 있다.
· 조건이 잘 갖춰지면 parallel 메서드 호출 하나로 거의 프롯게서 코어 수에 비례하는 성능 향상을 얻을 수 있다. 머신러닝과 데이터 처리 같은 특정 분야에서 사용이 가능하다.
참고
● computeIfAbsent 메서드는 Map 안에 키가 있는지 찾은 다음, 있으면 단순히 그 키에 매핑된 값을 반환한다. 키가 없으면 건네진 함수 객체를 키에 적용하여 값을 계산해낸 다음 그 키와 값을 매핑해놓고 계산된 값을 반환한다.
● 순수 함수 : 오직 입력만이 결과에 영향을 주는 함수
● 참조 지역성이 뛰어나다는 의미는 원소의 참조들이 메모리에 연속해서 저장되어 있다는 뜻이다.
● 안전 실패(save failure) : 병렬화한 파이프라인이 사용하는 mappers, filters, 혹은 프로그래머가 제공한 다른 함수 객체가 명세대로 동작하지 않을 때 발생한다.
스트림을 사용하게 되면 여러줄로 써야하는 코드가 한줄로 끝나는 것을 자주 보게 된다. 하지만 이를 너무 길게 나열하게 되면 나중에 알아보기 굉장히 힘들때가 종종 있었다.
스트림 사용시 아이템 46에 나오는 내용들은 자주 사용하니깐 외워두는 것으 좋을 것 같다.
스트림 사용시 아이템 46에 나오는 내용들은 자주 사용하니깐 외워두는 것으 좋을 것 같다.
댓글
댓글 쓰기