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

자바가 람다를 지원하면서 템플릿 메서드 패턴의 매력이 크게 줄었다. 이를 대체하는 현대적인 해법은 같은 효과의 함수 객체를 받는 정적 팩터리나 생성자를 제공하는 것이다. 이 내용을 일반화해서 말하면 함수 객체를 매개변수로 받는 생성자와 메서드를 더 많이 만들어야 한다. 이때 함수형 매개변수 타입을 올바르게 선택해야 한다.

LinkedHashMap의 removeEldestEntry 메서드

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

LinkedHashMap을 오늘날 다시 구현한다면 함수 객체를 받는 정적 팩터리나 생성자를 제공했을 것이다.

LinkedHashMap에 필요한 함수형 인터페이스 선언

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

이 인터페이스는 잘 동작하지만 굳이 사용할 이유는 없다. 자바 표준 라이브러리에 이미 같은 모양의 인터페이스가 준비되어 있기 때문이다. java.util.function 패키지를 보면 다양한 용도의 표준 함수형 인터페이스가 담겨있다. 필요한 용도에 맞는 게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하라.
위의 EldestEntryRemovalFunction 대신 표준 인터페이스인 BiPredicate<Map<K,V>, Map.Entry<K,V>>를 사용할 수 있다.

표준 함수형 인터페이스

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

기본 인터페이스는 기본 타입인 int, long, double용으로 각 3개씩 변형이 생긴다. int를 받는 Predicate는 IntPredicate가 되고 long을 받아 long을 반환하는 BinaryOperator는 LongBinaryOperator가 되는 식이다. 이 변형들 중 유일하게 Function의 변형만 매개변수화됐다. 예를 들어 LongFunction<int[]>은 long 인수를 받아 int[]을 반환한다.

Function 인터페이스

Function 인터페이스에는 기본 타입을 반환하는 변형이 총 9개 더 있다. Function 인터페이스의 변형은 입력과 결과의 타입이 항상 다르다. 입력과 결과 타입이 모두 기본 타입이면 접두어로 SrcToResult를 사용한다. 예컨대 long을 받아 int를 반환하면 LongToIntFunction이 되는 식이다.
입력이 객체 참조이고 결과가 int, long, double인 변형들은, 입력을 매개변수화하고 접두어로 ToResult를 사용한다. ToLongFunction<int[]>은 int[] 인수를 받아 long을 반환한다.

인수 2개를 받는 변형

BiPredicate<T, U>, BiFunction<T,U,R>, BiConsumer<T,U>

  • BiFunction에는 기본 타입을 반환하는 세 변형 ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U>가 존재한다.

  • Consumer에는 객체 참조와 기본 타입 하나, 즉 인수를 2개 받는 변형인 ObjDoubleConsumer<T>, ObjIntConsumer<T>, ObjLongConsumer<T>가 존재한다.

BooleanSupplier

boolean을 반환하도록 한 Supplier의 변형이다. 표준 함수형 인터페이스 중 boolean을 이름에 명시한 유일한 인터페이스지만, Predicate와 그 변형 4개도 boolean값을 반환할 수 있다.

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

전용 함수형 인터페이스

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

전용 함수형 인터페이스를 구현해야 할 때

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

위 특성 중 하나 이상을 만족하면 전용 함수형 인터페이스를 구현해야 하는지 고민해봐야 한다. 전용 함수형 인터페이스를 작성하기로 했다면, 자신이 작성하는 게 다른 것도 아닌 인터페이스 임을 명심해야 한다. 아주 주의해서 설계해야 한다는 뜻이다.

Comparator<T> 인터페이스는 구조적으로 ToIntBiFunction<T,U>와 동일하다. 심지어 자바 라이브러리에 Comparator<T>를 추가할 당시 ToIntBiFunction<T,U>가 이미 존재했더라도 사용하면 안 됐다. Comparator가 독자적인 인터페이스로 살아남은 이유는 위 특성들을 가지고 있기 때문이다.

@FunctionalInterface 애너테이션 목적

이 애너테이션을 사용하는 이유는 @Override를 사용하는 이유와 비슷하다. 프로그래머의 의도를 명시하는 것이다. 직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface 애너테이션을 사용하자.

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

함수형 인터페이스 사용시 주의사항

서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중 정의해서는 안 된다. 클라이언트에게 불필요한 모호함으로 인해 문제가 일어나기도 한다.

java.util.concurrent.ExecutorService의 submit() 메서드 잘못된 예

<T> Future<T> submit(Callable<T> task);

Future<?> submit(Runnable task);

ExecutorService의 submit 메서드는 Callable<T>과 Runnable을 받는 것을 다중정의했다. 그래서 올바른 메서드를 알려주기 위해 형변환해야 할 때가 생긴다.

자바가 람다를 지원하면서 API를 설계할 때 람다도 염두에 두어야 한다. 입력값과 반환값에 함수형 인터페이스 타입을 활용하라. 보통은 표준 함수형 인터페이스를 사용하는 것이 가장 좋은 선택이다. 단, 직접 새로운 함수형 인터페이스를 만들어 쓰는 편이 나을 때도 있다.

Comments