Link
Today
Total
10-17 00:15
Archives
관리 메뉴

초보개발자 긍.응.성

(이펙티브 자바 3) 7장 람다와 스트림 정리 본문

책 정리/이펙티브 자바 3

(이펙티브 자바 3) 7장 람다와 스트림 정리

긍.응.성 2020. 11. 30. 00:59
반응형

익명 클래스보다는 람다를 사용하라

익명 클래스 방식은 너무 코드가 길다. 단 하나의 추상 메서드를 갖는 함수형 인터페이스는 람다식으로 표현하자. 람다식의 경우 타입을 명시해야 코드가 더 명확할 때만 제외하고는, 람다의 모든 매개변수 타입은 생략하자. 익명 클래스는 함수형 인터페이스가 아닌 타입의 인스턴스를 만들 때만 사용하라

대신 람다는 이름이 없고 문서화도 못한다. 따라서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 쓰지 말아야 한다.

람다보다는 메서드 참조를 사용하라

람다가 익명 클래스보다 나은 점 중에서 가장 큰 특징은 간결함이다. 하지만 더 간결한 방법은 메서드 참조이다. 미리 정의된 메서드 참조를 사용해도 되고, 람다로 작성할 코드를 메서드에 담은 다음 람다 대신 그 메서드 참조를 사용해도 된다. 이때의 장점은 메서드에 이름을 지어줄 수 있고 친절한 설명을 문서로 남길 수도 있다.

Name 같은 기능을 하는 람다
정적 Integer::parseInt str → Integer.parseInt(str)
한정적 (인스턴스) Instant.now()::isAfter Instant then = Instant.now();
t → then.isAfter(t)
비한정적 (인스턴스) String::toLowerCase str → str.toLowerCase()
클래스 생성자 TreeMap<K,V>::new () → new TreeMap<K,V>()
배열 생성자 Int[]::new len → new int[len]

표준 함수형 인터페이스를 사용하라

함수형 인터페이스를 직접 만드는것도 좋지만 필요한 용도에 맞는 것이 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하자. java.util.function 패키지를 보면 다양한 용도의 표준 함수형 인터페이스가 존재한다.

표준 함수형 인터페이스 대부분은 기본 타입만 지원하는데 그렇다고 박싱 된 기본 타입을 넣어 사용하지 말자. NPE를 뱉을 수 있으며 계산량이 많을 때 좋은 성능을 기대하기 어렵다.

Comparator<T> 인터페이스는 ToIntFunction<T,U>와 동일한데 독자적인 인터페이스로 살아남아야 하는 이유가 몇 개 있다.

  1. API가 굉장히 자주 사용되며 이름이 그 용도를 아주 훌륭히 설명해준다.
  2. 구현하는 쪽에서 반드시 지켜야 할 규약을 담고있다.
  3. (비교자들을 변환하고 조합해주는) 유용한 디폴트 메서드들을 많이 담고 있다.

직접 만든 함수형 인터페이스에는 항상 @FunctionalInteface 애너테이션을 사용하자.

  1. 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려준다.
  2. 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해준다.
  3. 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.

스트림은 주의해서 사용하라

스트림 API는 다량의 데이터 처리 작업을 돕고자 자바 8에 추가되었다. 스트림은 소스 스트림에서 시작해 하나 이상의 중간 연산(intermediate operation)을 거쳐 종단 연산(terminal operation)으로 끝나게 된다. 스트림 API는 메서드 연쇄를 지원하는 플루언트(Fluent) API이다. 기본적으로 순차적으로 수행되나 parallel메서드를 사용하여 병렬 파이프라인으로 만들어 사용할 수 있다.

스트림을 과용하며 ㅁ프로그램이 읽거나 유지보수하기 어려워진다. 람다에서는 타입 이름을 자주 생략하므로 매개변수 이름을 잘 지어주도록 하며, 도우미 메서드를 적절히 활용하도록 하자.

반복 코드 블록 대신 스트림을 사용할 수 있지만 스트림은 아래의 행동은 하지 못하니 아래와 같은 일을 수행해야 한다면 스트림 대신 반복 코드 블록을 사용하자.

  1. 코드 블록에서는 범위 안의 지역변수를 읽고 수정할 수 있다. 람다는 final이거나 사실상 final인 변수만 읽을 수 있으며 지역변수의 수정은 불가능하다.
  2. 코드 블록에서는 return, break, continue를 이용하여 반복문 중간에서 진행을 제어할 수 있다. 하지만 람다로는 이러한 것들을 할 수 없다.

스트림과 반복 중 어느 쪽이 나은지는 스스로 잘 판단하여 사용하도록 하자.

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

스트림 패러다임의 핵심은 계산을 일련의 변환(transformation)으로 재구성하는 부분이다. 하지만 외부 상태를 수정하는 람다 코드를 사용하면 나쁜 코드 냄새가 나며 이는 스트림 API를 제대로 사용한 모습이 아니다. forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 말자. 또한, 안전하고 나쁜 코드 냄새가 나지 않도록 하기 위해 java.util.stream.Collectors 클래스의 메서드를 적극적으로 사용할 수 있도록 잘 익혀두자.

반환 타입으로는 스트림보다 컬렉션이 낫다

원소 시퀀스를 반환할 때는 사용자가 Stream을 사용할지 Iterator를 사용할지 알 수 없기다. 하지만 그 둘은 호환되지 않고 굳이 Iterator를 Stream으로, Stream을 Iterator로 사용하고 싶다면 어댑터를 만들어 쓸 수 있지만 좋은 성능을 기대하긴 어렵다.

  • Stream<E>를 Iterable<E>로 중개해주는 어댑터
public static <E> Iterable<E> iterableOf(Stream<E> stream) { 
	return stream::iterator; 
}
  • Iterable<E>를 Stream<E>로 중개해주는 어댑터
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
	return StreamSupport.stream(iterable.spliterator(), false); 
}

웬만하면, 둘 다 반환할 수 있는 Collection 타입으로 반환하자. Collection 인터페이스는 Iterable의 하위 타입이고 stream 메서드도 제공하니 반복과 스트림을 동시에 지원한다. 따라서 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 양쪽을 다 만족시키는 Collection이나 그 하위 타입을 쓰는 게 일반적으로 최선이다.

스트림 병렬화는 주의해서 적용하라

자바의 스트림에서는 parallel 메서드를 호출하여 쉽게 병렬 스트림을 사용할 수 있다. 하지만 환경이 아무리 좋더라도 데이터 소스가 Stream.iterate거나 중간 연산으로 limit을 쓰면 파이프라인 병렬화로는 성능 개선을 기대할 수 없다.

대체로 스트림의 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스이거나 배열, int 범위, long 범위일 때 병렬화의 효과가 가장 좋다. 종단 연산 중 병렬화에 가장 적합한 것은 축소(reduction)다. 축소는 파이프라인에서 만들어진 모든 원소를 하나로 합치는 작업으로, Stream의 reduce 메서드 중 하나, 혹은 min, max, count, sum 같이 완성된 형태로 제공되는 메서드이다. anyMatch, allMatch, nonMatch처럼 조건에 맞으면 바로 반환되는 메서드(short-circuiting terminal opertaion)도 병렬화에 적합하다. Stream의 collect 메서드는 컬력션들을 합치는 부담이 크기 때문에 병렬화에 적합하지 않다.

스트림을 잘못 병렬화하면 성능이 나빠질 뿐만 아니라 결과 자체가 잘못되거나 예상 못한 동작이 발생할 수 있다. 스트림 병렬화는 오직 성능 최적화 수단이다. 다른 최적화와 마찬가지로 변경 전후로 반드시 성능을 테스트하여 병렬화를 사용할 가치가 있는지 확인해야 한다. 계산이 정확하고 성능이 좋아지는 것이 확실하다면 parallel 메서드 호출 하나로 거의 프로세서 코어 수에 비례하는 성능 향상을 만끽할 수 있다.

반응형
Comments