diff --git a/developLog/programming-lanuage/java/effective-java/7/item-44.md b/developLog/programming-lanuage/java/effective-java/7/item-44.md index d60bae8..4bdca1e 100644 --- a/developLog/programming-lanuage/java/effective-java/7/item-44.md +++ b/developLog/programming-lanuage/java/effective-java/7/item-44.md @@ -4,6 +4,8 @@ > 이로 인해 함수 객체를 매개변수로 받는 생성자와 메서드를 더 많이 작성하게 되었으며, 올바른 **함수형 인터페이스 타입**을 선택하는 것이 중요해졌다. 즉, 함수형 매개변수 타입을 올바르게 선택해야 한다. +자바의 표준 라이브러리에서는 이미 다양한 용도의 **표준 함수형 인터페이스**가 제공되고 있으며, 이들을 활용하면 불필요하게 새로운 인터페이스를 작성하지 않고도 필요한 기능을 구현할 수 있다. 특히 `java.util.function` 패키지에는 함수형 프로그래밍을 지원하기 위해 총 43개의 함수형 인터페이스가 준비되어 있다. + ## 1. 템플릿 메서드 패턴에서 함수형 인터페이스로 전환 과거에는 상위 클래스의 기본 메서드를 재정의하여 동작을 맞춤 설정하는 **템플릿 메서드 패턴**을 많이 사용했다. 하지만 함수형 인터페이스와 람다를 이용하면 이보다 간결하고 유지보수하기 좋은 API를 만들 수 있다. @@ -81,14 +83,44 @@ CustomCache cache = new CustomCache<>((map, entry) -> map.size() 표준 함수형 인터페이스는 크게 다음과 같은 유형이 있다. -| 인터페이스 | 함수 시그니처 | 설명 | -| ------------------- | --------------------- | --------------------- | -| `UnaryOperator` | `T apply(T t)` | 인수를 하나 받아 같은 타입을 반환 | -| `BinaryOperator` | `T apply(T t1, T t2)` | 인수를 두 개 받아 같은 타입을 반환 | -| `Predicate` | `boolean test(T t)` | 인수를 하나 받아 boolean을 반환 | -| `Function` | `R apply(T t)` | 인수와 반환 타입이 다른 함수 | -| `Supplier` | `T get()` | 인수를 받지 않고 값을 반환 | -| `Consumer` | `void accept(T t)` | 인수를 하나 받고 값을 반환하지 않음 | +기본적으로 자바의 함수형 인터페이스는 다음과 같은 유형으로 나뉩니다. 이 인터페이스들은 **객체 참조 타입**을 위한 기본 인터페이스들로, 실무에서 자주 활용됩니다. + +| 인터페이스 | 함수 시그니처 | 설명 | 사용 예시 | +| ------------------- | --------------------- | --------------------------- | --------------------- | +| `UnaryOperator` | `T apply(T t)` | 인수를 하나 받아 같은 타입을 반환합니다. | `String::toLowerCase` | +| `BinaryOperator` | `T apply(T t1, T t2)` | 인수를 두 개 받아 같은 타입을 반환합니다. | `BigInteger::add` | +| `Predicate` | `boolean test(T t)` | 인수를 하나 받아 `boolean`을 반환합니다. | `Collection::isEmpty` | +| `Function` | `R apply(T t)` | 인수와 반환 타입이 다를 때 사용합니다. | `Arrays::asList` | +| `Supplier` | `T get()` | 인수를 받지 않고 값을 반환합니다. | `Instant::now` | +| `Consumer` | `void accept(T t)` | 인수를 하나 받고 반환값이 없습니다. | `System.out::println` | + +**예시 - `Predicate`와 `BiPredicate` 활용** + +`Predicate`와 `BiPredicate`는 조건을 표현하는 데 사용되며, 각각 하나 또는 두 개의 인수를 받아 `boolean`을 반환한다. 예를 들어, 특정 크기 이상일 때 캐시 항목을 삭제하도록 조건을 설정할 수 있다. + +```java +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.BiPredicate; + +class CustomCache extends LinkedHashMap { + private final BiPredicate, Map.Entry> removalCriteria; + + public CustomCache(BiPredicate, Map.Entry> removalCriteria) { + this.removalCriteria = removalCriteria; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return removalCriteria.test(this, eldest); + } +} + +// 사용 예: 캐시 크기가 100을 초과하면 오래된 항목 제거 +CustomCache cache = new CustomCache<>((map, entry) -> map.size() > 100); +``` + +위 코드는 `BiPredicate`를 사용해 특정 크기를 넘을 때 캐시 항목을 제거하도록 설정한 예시이다. **예제** @@ -124,9 +156,19 @@ CustomCache cache = new CustomCache<>((map, entry) -> map.size() System.out.println(toLowerCase.apply("HELLO")); // hello ``` +### 2) 표준 함수형 인터페이스 활용의 이점 + +기존에는 커스텀 인터페이스를 작성하여 기능을 구현하는 경우가 많았지만, 표준 함수형 인터페이스를 사용하면 다음과 같은 이점을 누릴 수 있다: + +* **코드의 간결성**: 중복되는 인터페이스 생성을 피할 수 있다. +* **API의 일관성**: 자바 개발자들이 이미 익숙한 표준 인터페이스를 사용하므로, 새로운 개념을 익힐 필요가 줄어든다. +* **디폴트 메서드의 활용**: 표준 인터페이스는 필요한 경우 메서드를 조합하거나 변형할 수 있는 유용한 디폴트 메서드를 제공하여 다른 코드와 쉽게 상호 운용이 가능하다. + +예를 들어, `Predicate` 인터페이스는 조건을 조합할 수 있는 `and`, `or`, `negate` 메서드를 제공하여 복잡한 조건을 간편하게 표현할 수 있다. + ## 3. 기본 타입용 함수형 인터페이스 -기본 타입을 위해 `java.util.function` 패키지에서는 기본 타입용 함수형 인터페이스도 제공한다. 예를 들어, `IntPredicate`, `LongFunction`, `DoubleConsumer` 등이다. 기본 타입을 지원하는 인터페이스를 사용하면 **박싱 오버헤드**를 피할 수 있어 성능이 개선된다. +기본 타입을 위해 `java.util.function` 패키지에서는 기본 타입용 함수형 인터페이스도 제공한다. 박싱을 피하고 성능을 높이기 위해 기본 타입을 위한 함수형 인터페이스 변형도 제공한다. 예를 들어, `IntPredicate`, `LongFunction`, `DoubleConsumer` 등이다. 기본 타입을 지원하는 인터페이스를 사용하면 **박싱 오버헤드**를 피할 수 있어 성능이 개선된다. **예제** @@ -136,6 +178,33 @@ System.out.println(isEven.test(4)); // true System.out.println(isEven.test(5)); // false ``` +자바 기본 타입을 위한 변형은 다음과 같은 규칙에 따라 네이밍된다. + +1. **타입 접두어**: `Int`, `Long`, `Double`을 사용하여 기본 타입을 명시 +2. **기능**: `Predicate`, `Function`, `Consumer` 등 주요 기능을 반영 +3. **변형 예시**: + * `IntPredicate`: `boolean test(int value)` 형태로 `int` 타입을 받는다. + * `LongToIntFunction`: `int applyAsInt(long value)` 형태로 `long` 타입의 인수를 받고 `int`를 반환한다. + +**예시 - 기본 타입용 함수형 인터페이스** + +기본 타입을 사용하여 특정 조건을 검사하는 예제 + +```java +import java.util.function.IntPredicate; + +public class BasicTypePredicateExample { + public static void main(String[] args) { + IntPredicate isEven = value -> value % 2 == 0; + + System.out.println(isEven.test(4)); // true + System.out.println(isEven.test(5)); // false + } +} +``` + +위 예제에서는 `IntPredicate`를 사용하여 숫자가 짝수인지 여부를 검사한다. + ## 4. 직접 작성해야 하는 경우 표준 함수형 인터페이스가 거의 대부분의 경우를 커버하지만, **다음과 같은 경우**에는 직접 작성하는 것이 더 나을 수 있다. @@ -144,7 +213,9 @@ System.out.println(isEven.test(5)); // false * **자주 사용되며 이름만으로 용도가 명확한 경우**: 자주 사용되며 명확한 이름을 가지면 코드 가독성이 높아진다. * **추가적인 디폴트 메서드가 필요한 경우**: 인터페이스 내에서 메서드를 조합하거나 변형하는 디폴트 메서드가 필요한 경우가 있다. -**예제 - 커스텀 함수형 인터페이스** +**예시 - 3개의 매개변수를 받는 함수형 인터페이스** + +`java.util.function` 패키지에는 매개변수 3개를 받는 함수형 인터페이스가 없으므로, 직접 작성할 수 있다. ```java @FunctionalInterface @@ -152,6 +223,7 @@ interface TriFunction { R apply(T t, U u, V v); } +// 사용 예시 TriFunction sumThree = (a, b, c) -> a + b + c; System.out.println(sumThree.apply(1, 2, 3)); // 6 ``` @@ -160,13 +232,126 @@ System.out.println(sumThree.apply(1, 2, 3)); // 6 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드를 다중 정의하는 것은 피해야 한다. 예를 들어, `ExecutorService`의 `submit` 메서드는 `Callable`과 `Runnable`을 다중 정의하는데, 이를 사용할 때 형변환이 필요한 경우가 생긴다. 이는 불필요한 혼란을 초래할 수 있다. -따라서, **같은 위치의 인수로 서로 다른 함수형 인터페이스를 받지 않도록 주의**하는 것이 좋다. +> 따라서, **같은 위치의 인수로 서로 다른 함수형 인터페이스를 받지 않도록 주의**하는 것이 좋다. + +이는 API 설계에서 모호함을 줄이고, 사용자가 코드의 의도를 명확하게 이해할 수 있도록 도와준다. + +{% hint style="info" %} +안 좋은 예시 +{% endhint %} + +아래는 서로 다른 함수형 인터페이스(`Callable`과 `Runnable`)를 같은 위치에서 받도록 다중 정의된 메서드 + +```java +import java.util.concurrent.Callable; + +public class ExecutorServiceExample { + // Runnable을 인수로 받는 메서드 + public void submit(Runnable task) { + System.out.println("Runnable version called"); + task.run(); + } + + // Callable을 인수로 받는 메서드 + public T submit(Callable task) throws Exception { + System.out.println("Callable version called"); + return task.call(); + } + + public static void main(String[] args) throws Exception { + ExecutorServiceExample service = new ExecutorServiceExample(); + + // Runnable을 사용한 경우 + service.submit(() -> System.out.println("Running task...")); + + // Callable을 사용한 경우 + String result = service.submit(() -> { + System.out.println("Calling task..."); + return "Task result"; + }); + System.out.println("Result: " + result); + + // 혼란을 일으킬 수 있는 모호한 상황 + service.submit((Callable) () -> { + System.out.println("Ambiguous task..."); + return null; + }); + } +} +``` + +* `submit(Runnable task)`: `Runnable` 인터페이스를 받는 `submit` 메서드이다. +* `submit(Callable task)`: `Callable` 인터페이스를 받는 제네릭 `submit` 메서드이다. +* 두 메서드는 모두 같은 이름의 `submit` 메서드지만, 인수의 타입만 다르게 정의되어 있다. + +> 문제점 + +이 다중 정의된 메서드는 `Runnable`과 `Callable` 중 어떤 것을 호출할지 모호한 경우가 발생할 수 있다. 특히 **람다 표현식**으로 전달할 때 문제를 일으킬 수 있습니다. `Runnable`과 `Callable`은 둘 다 인수가 없으므로 람다식으로는 명확히 구분되지 않는다. 예를 들어, 다음과 같이 호출할 때 문제가 된다. + +```java +service.submit(() -> { + System.out.println("Ambiguous task..."); + return null; +}); +``` + +위 코드는 `Runnable`인지 `Callable`인지 컴파일러가 구분하지 못하여 오류를 발생시킨다. 이를 해결하려면 **명시적인 형변환**이 필요하다. + +```java +service.submit((Callable) () -> { + System.out.println("Callable task..."); + return null; +}); +``` + +명시적 형변환을 통해 `Callable`이라는 의도를 명확히 했지만, 이는 코드를 더 복잡하게 만들고 실수할 가능성을 높인다. + +#### 해결 방법: 함수형 인터페이스 다중 정의를 피하기 + +이 문제를 피하려면 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 **다중 정의를 피하는 것**이 좋다. 예를 들어, `submit` 메서드를 하나로 통일하고, `Runnable`이나 `Callable`이 필요할 경우 래퍼 메서드로 구분하는 방식이 가능하다. + +```java +public class ExecutorServiceExample { + public T submitTask(Callable task) throws Exception { + System.out.println("Callable version called"); + return task.call(); + } + + public void submitTask(Runnable task) { + System.out.println("Runnable version called"); + task.run(); + } + + public static void main(String[] args) throws Exception { + ExecutorServiceExample service = new ExecutorServiceExample(); + + // Runnable로 전달 + service.submitTask(() -> System.out.println("Running task...")); + + // Callable로 전달 + String result = service.submitTask(() -> { + System.out.println("Calling task..."); + return "Task result"; + }); + System.out.println("Result: " + result); + } +} +``` + +이렇게 메서드 이름을 명확히 나누어 **각 메서드가 어떤 인터페이스를 받는지 명확히** 하면 혼란을 줄일 수 있다. + + ## 핵심 요약 1. **함수형 인터페이스**를 통해 람다 표현식을 활용하면 코드가 간결해지고 유연성이 높아진다. 2. **표준 함수형 인터페이스**를 적극적으로 활용하고, 필요할 때만 커스텀 인터페이스를 정의하는 것이 좋다. + +> `Predicate`, `Function`, `Supplier`, `Consumer` 등의 주요 함수형 인터페이스는 대부분의 경우를 커버할 수 있어, **표준 인터페이스** 사용을 우선적으로 고려해야 한다. + 3. 커스텀 함수형 인터페이스를 정의할 때는 **반드시 따라야 할 규약**이나 **유용한 디폴트 메서드**가 필요한지 고민해보아야 한다. 4. 함수형 인터페이스를 다중 정의하는 것을 피하여 **모호함**을 방지한다. +5. 자바에서 **표준 함수형 인터페이스**를 적극 활용하면 불필요한 인터페이스 작성을 줄일 수 있으며, API가 더 일관성 있고 간결해진다. +6. **기본 타입용 인터페이스**를 사용하여 성능을 최적화할 수 있습니다. 박싱된 타입 대신 `IntPredicate`, `LongFunction` 등 기본 타입을 위한 인터페이스를 사용하는 것이 좋다. 자바에서도 함수형 프로그래밍의 장점을 활용할 수 있으므로, API 설계 시 함수형 인터페이스의 활용을 염두에 두는 것이 좋다.