DEV ℧ Developer Diary

[EffectiveJava] item44 - 표준 함수형 인터페이스를 사용하라

자바가 람다를 지원하면서 API를 작성하는 모범 사례도 템플릿 메서드 패턴에서 정적 팩터리나 생성자를 제공하는 형태로 많이 변화했다.

위의 내용을 풀어 말하자면, 함수 객체를 매개변수로 받는 생성자와 메서드를 더 많이 많들어야 한다는 것이다. 이때 함수형 매개변수 타입을 올바르게 선택하도록 해야한다.

함수형 매개변수 타입

예를 들어 보자.

LinkedHashMap은 내부 protected 메서드인 removeEldestEntry를 재정의 하면 캐시처럼 사용 할 수 있다.

기존엔 return false로 되어있어 false로 구현되어 있지만, 아래와 같이 재정의한다면 100개 까지만 저장하는 LinkedHashMap을 만들 수 있다.

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return size() > 100;
}

잘 동작하지만 람다를 사용하면 훨씬 잘 해낼 수 있다. LinkedHashMap을 오늘날 다시 구현한다면 함수 객체를 받는 정적 팩터리나 생성자를 제공했을 것이다. 또한 removeEldestEntry가 인스턴스 메서드라서 가능한 방식이다.

하지만 생성자나 팩터리를 호출해 넘길때는 맵의 인스턴스가 존재하지 않기 때문에 해당 인스턴스 메서드에 넘기는 함수 객체는 이 맵의 인스턴스 메서드가 아니다. 따라서 맵은 자기 자신도 함수 객체에 건네줘야 한다.

이를 함수형 인터페이스로 변경한다면 아래와 같이 볼 수 있을 것이다.

@FunctionalInterface interface EldestEntryRemovalFunction<K,V> {
    boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}

하지만 중요한 요지가 있다.

위와 같은 인터페이스는 잘 동작하지만, 이미 같은 모양의 인터페이스가 있기 때문에 굳이 사용할 필요가 없다.

표준 함수형 인터페이스

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

그러면 API가 다루는 개념의 수가 줄어들어 익히기 더 쉬워진다.

위 예시의 LinkedHashMapEldestEntryRemovalFunction 대신에 표준 인터페이스인 BiPredicate<Map<K,V>, Map.Entry<K,V>>를 사용할 수 있다.

대표적인 기본 인터페이스

java.util.function 패키지에는 총 43개의 인터페이스가 있지만, 기본 인터페이스 6개만 기억하면 나머지를 쉽게 유추 할 수 있다.

인터페이스 함수 시그니처
UnaryOperator T apply(T t) String::toLowerCase
BinaryOperator T apply(T t1, T t2) BigInteger::add
Predicate boolean test(T t) Collection::isEmpty
Function<T,R> R apply(T t) Arrays::asList
Supplier T get() Instant::now
Consumer void accept(T t) System.out::println

UnaryOperator, BinaryOperator

Operator 인터페이스는 인수가 1개인 UnaryOperator와 2개인 BinaryOperator로 나뉘며, 반환값과 인스의 타입이 같은 함수를 말한다.

Predicate

Predicate 인터페이스는 인수 하나를 받아 boolean을 반환하는 함수를 뜻한다.

Function

Function 인터페이스는 인수와 반환 타입이 다른 함수를 뜻한다.

Supplier

Supplier 인터페이스는 인수를 받지 않고 값을 반환(혹은 제공) 하는 함수를 뜻한다.

Consumer

Consumer 인터페이스는 인수를 하나 받고 반환값은 없는(특히 인수를 소비하는) 함수를 뜻한다.

기본 인터페이스의 명명 규칙

기본적인 명명 규칙

이제 기본 인터페이스들을 유추해본다면, 기본 인터페이스는 기본 탕비인 int, long, double용으로 각각 3개씩 변형이 생겨난다.

그 이름도 기본 인터페이스의 이름 앞에 해당 기본 타입 이름을 붙여 지었다. 예를 들어 int를 받는 Predicate는 IntPredicate가 되고 long 을 받아 long을 반환 하는 BinaryOperator는 LongBinaryOperator가 되는 식이다.

Function 인터페이스의 명명 규칙

이 변형중 Function의 변형만 매개변수화 됐다. 예를 들어 LongFunction<int[]>은 long 인수를 받아 int[]을 반환한다.

또한 Function 인터페이스에는 기본 타입을 반환하는 변형이 총 9개나 더있다.

SrcToResult

인수와 같은 타입을 반환하는 함수는 UnaryOperator 이므로, Function 인터페이스의 변형은 입력과 결과의 타입이 항상 다르다.

입력과 결과 타입이 모두 기본 타입이면 접두어로 SrcToResult의 규칙을 사용한다. 예를들어 long을 받아 int를 반환하면 LongToIntFunction이 되는 식이다.

ToResult

입력이 객체참조이고 결과가 int, long, double 인 경우 접두어로 ToResult를 사용하는 경우도 있다. ToLongFunction<int[]>은 int[] 인수를 받아 long을 반환한다.

Bi~ 함수형 인터페이스

BiPredicate<T,U>, BiFunction<T,U,R>, BiConsumer<T,U> 는 인수를 2개씩 받는 변형이다.

각각 BiFunction<T,U,R>에는 기본타입을 반환하는 ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U>

BiConsumer<T,U>에는 객체 참조와 기본타입하나의 변형인 ObjDoubleConsumer<T>, ObjIntConsumer<T>, ObjLongConsumer<T> 가 있다.

BooleanSupplier 인터페이스

마지막으로 BooleanSupplier는 boolean을 반환하도록한 Supplier의 변형이다. 이것이 표준 합수형 인터페이스중 boolean을 이름에 명시한 유일한 인터페이스이다.

주의할점

표준 함수형 인터페이스 대부분은 기본 타입만 지원한다. 그렇다고 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 말자. 동작은 하지만 특히 계산량이 많을 때는 성능이 처참히 느려질 수 있다.

무조건 표준 함수형 인터페이스를 사용할까?

그렇다면 무조건 표준 함수형 인터페이스만 사용해야 할까? 물론 아니다.

표준 인터페이스중 필요한 용도에 맞는게 없다면 직접 작성 해야한다. 하지만 Predicate같은 검사 예외를 던지는 경우 구조적으로 똑같은 표준 함수형 인터페이스가 있더라도 직접 작성해야 할 때가 있다.

Comparator

대표적인 예시로 Comparator를 들 수 있다. 구조적으로는 ToIntBiFunction<T,U>와 동일하지만 직접 구현되어있다.

그 이유를 간단히 정리하자면 아래와 같다.

직접 구현할 경우

  • 자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다.
  • 반드시 따라야 하는 규약이 있다.
  • 유용한 디폴트 메서드를 제공할 수 있다.

위와 같은 조건중 하나 이상을 만족한다면 전용 함수형 인터페이스를 구현해야 하는건 아닌지 진중히 고민이 필요하다.

전용 함수형 인터페이스를 작성할시 주의할점

자신이 작성하는게 ‘인터페이스’임을 명시하고 주의해서 설계를 해야한다.

@FunctionInterface

@FunctionInterface 애너테이션을 달아, 프로그램의 의도를 명시하고 아래의 세가지 목적을 준수하도록 하자.

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

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

마지막으로 함수형 인터페이스를 API에서 사용할때 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중 정의해서는 안된다. 이는 클라이언트에게 불필요한 모호함을 안겨줄 뿐더러, 문제가 일어날 수 있다.