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

📌 표준 함수형 인터페이스를 사용하면 코드가 더 간결하고 이해하기 쉬워지며 재사용성도 높아진다.


📝 핵심 요약 (Overview)

Java 8부터는 람다 표현식과 메서드 참조를 지원하여, 함수를 값처럼 다룰 수 있습니다. 이때 직접 함수형 인터페이스를 정의하기보단, 이미 자바가 제공하는 표준 함수형 인터페이스(java.util.function) 를 사용하는 것이 권장됩니다.

표준 함수형 인터페이스의 중요한 점

  • 코드 가독성 향상: 표준 함수형 인터페이스는 이미 많은 개발자들에게 익숙하므로, 코드를 더 쉽게 이해할 수 있도록 도와줍니다. 예를 들어, Predicate<T>를 보면 'T 타입의 객체를 받아서 boolean 값을 반환하는 함수'임을 직관적으로 알 수 있습니다.

  • API 일관성 유지: 자바 API 및 많은 라이브러리에서 표준 함수형 인터페이스를 광범위하게 사용하고 있으므로, 여러분의 코드에서도 이를 활용하면 API의 일관성을 유지하는 데 도움이 됩니다.

  • 불필요한 코드 감소: 필요한 용도에 맞는 표준 함수형 인터페이스가 이미 존재한다면, 굳이 똑같은 기능을 하는 자신만의 함수형 인터페이스를 만들 필요가 없어 코드 양을 줄일 수 있습니다.

  • Comparator의 예외: Comparator 인터페이스는 API에서 매우 자주 사용되고, 구현 시 지켜야 할 규약이 명확하며, 비교자를 조합하고 변환하는 유용한 디폴트 메서드들을 포함하고 있기 때문에 독자적인 인터페이스로 유지되는 것이 타당합니다.

  • 커스텀 함수형 인터페이스 작성 시: 만약 표준 함수형 인터페이스 중에서 원하는 기능을 제공하는 것이 없다면, 직접 함수형 인터페이스를 작성해야 합니다. 이때는 @FunctionalInterface 애너테이션을 사용하여 해당 인터페이스가 함수형 인터페이스임을 명시하고, 실수로 추상 메서드가 여러 개 추가되는 것을 방지하는 것이 좋습니다.


📚 미리 알아야 할 배경지식 (Prerequisites)

함수형 인터페이스(Functional Interface)는 정확히 하나의 추상 메서드를 가진 인터페이스입니다.

자바의 표준 함수형 인터페이스는 주로 다음과 같습니다.

분류
인터페이스
설명
주요 메서드

공급

Supplier

매개변수 없이 값을 제공

T get()

소비

Consumer

값을 소비하고 반환 없음

void accept(T t)

변환

Function<T,R>

값을 다른 값으로 변환

R apply(T t)

조건

Predicate

조건을 검사하여 boolean 반환

boolean test(T t)

단항 연산

UnaryOperator

같은 타입의 입력·출력 연산

T apply(T t)

이항 연산

BinaryOperator

두 개의 입력값을 받아 같은 타입으로 반환

T apply(T t1, T t2)


⚠️ 주의사항 (Cautions)

  • 기본 타입 사용 권장 박싱된 타입(Integer, Double 등)을 사용하면 성능 저하 가능성 존재(아이템 61 참조)

  • 오버로딩 지양 같은 위치의 매개변수로 서로 다른 함수형 인터페이스를 다중 정의하면, 사용자가 형변환을 강요받을 수 있음(아이템 52 참조)

  • 사용자 정의 시 @FunctionalInterface 애너테이션 사용 필수 직접 함수형 인터페이스를 정의할 경우 실수를 방지하고 명시적으로 표현하는 것이 좋음


❌ 잘못된 예제

// 사용자 정의 함수형 인터페이스 (java.util.function.Predicate와 동일한 기능)
interface MyCondition<T> {
    boolean check(T t);
}

public static <T> List<T> filter(List<T> list, MyCondition<T> condition) {
    List<T> result = new ArrayList<>();
    for (T item : list) {
        if (condition.check(item)) {
            result.add(item);
        }
    }
    return result;
}

List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// 굳이 MyCondition을 사용할 필요 없이 Predicate를 사용하는 것이 더 좋습니다.
// List<Integer> evenNumbers = filter(numbers, n -> n % 2 == 0);

✅ 좋은 예시 코드

import java.util.List;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.function.BinaryOperator;
import java.util.stream.Collectors;

public class StandardFunctionalInterfaceExample {

    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5);

        // Predicate: 조건을 검사하여 boolean 값을 반환
        Predicate<Integer> isEven = number -> number % 2 == 0;
        List<Integer> evenNumbers = numbers.stream().filter(isEven).collect(Collectors.toList());
        System.out.println("짝수: " + evenNumbers); // 출력:

        // UnaryOperator: 하나의 인자를 받아서 같은 타입의 결과를 반환
        UnaryOperator<Integer> increment = number -> number + 1;
        List<Integer> incrementedNumbers = numbers.stream().map(increment).collect(Collectors.toList());
        System.out.println("각 숫자에 1 더하기: " + incrementedNumbers); // 출력:

        // BinaryOperator: 두 개의 같은 타입 인자를 받아서 같은 타입의 결과를 반환
        BinaryOperator<Integer> sum = (a, b) -> a + b;
        int totalSum = numbers.stream().reduce(0, sum);
        System.out.println("숫자들의 합: " + totalSum); // 출력: 15
    }
}

🔖 기억해야 할 중요한 점 (Important Tips)

  • 이미 존재하는 표준 함수형 인터페이스를 우선적으로 활용

  • 직접 구현 시 @FunctionalInterface 반드시 명시

  • 박싱된 타입 사용 자제하고, 기본 타입 사용 고려

  • 불필요한 오버로딩(다중 정의) 피하기


🚧 어려웠던 점과 해결법 (Challenges & Solutions)

  • 다양한 인터페이스의 역할이 헷갈린다? → 인터페이스의 이름을 통해 기억:

    • Supplier: "공급자"

    • Consumer: "소비자"

    • Predicate: "판정자(조건 검사)"

    • Function: "변환기(입력→출력 변환)"

    • Operator: "연산자(동일 타입 연산)"

  • 메서드 참조의 개념이 어려웠다면? → "클래스이름::메서드이름" 또는 "객체이름::메서드이름"의 형태로 간단히 생각합시다.


🗣️ 느낀점 (Reflection)

표준 함수형 인터페이스는 처음엔 낯설지만, 익숙해지면 코드를 매우 간결하고 효율적으로 만들어줍니다. 특히 라이브러리와의 호환성이 높아지고, API의 재사용성을 크게 증가시켜주는 것이 가장 큰 장점입니다. 앞으로 적극적으로 활용할 예정입니다.

Last updated