ITEM 44 "표준 함수형 인터페이스를 활용하라"
자바8 부터 람다를 지원하면서 템플릿 메서드 패턴보다는 전략을 함수 객체 인자 형태로 전달하는 템플릿 콜백 패턴(전략 패턴) 이 모범이 되었다.
- 템플릿 메서드 패턴
abstract class Car {
// 공통 메서드
public void drive() {
System.out.println("운전 시작");
moving();
System.out.println("운전 완료");
}
// 변화하는 부분
abstract void moving();
}
class Hyundai extends Car {
@override
public void moving() {
...
}
}
상위 클래스의 변하는 부분들을 메서드로 분리해 하위 클래스에 재정의하는 기법을 템플릿 메서드 패턴이라고 한다. 상속을 사용한다.
- 템플릿 콜백 패턴 (전략 패턴)
// 선언부
class Car {
// 공통 메서드
public void drive(MovingStrategy movingStrategy) {
System.out.println("운전 시작");
movingStrategy.moving();
System.out.println("운전 완료");
}
}
// 사용부
// MovingStrategy 인터페이스에는 moving 이라는 추상 메서드 하나만 존재해야 한다.
Car myCar = new Car();
myCar.drive(() -> System.out.println('100km/h 주행 중'));
이와는 반대로 함수 객체를 사용하는 시점(moving 함수 호출)에 전략 함수 객체를 인자로 받아 실행하는 형태를 템플릿 콜백 패턴. 즉 전략패턴이라고 한다. 함수 생성 시점에 전략 객체를 같이 생성하여 강하게 결합하는 것이 아닌, 사용하는 시점에 전략 객체를 인자로 받아 사용하고 제거하는 형태로 결합해 결합도가 낮아 유연하다는 장점이 있다.
LinkedHashMap 를 커스터마이징해서 더 자세히 알아보자.
LinkedHashMap 의 removeEldestEntry() 함수는 맵에 새로운 키를 추가할 때 호출되는 put() 메서드에 의해 호출되는데, 해당 메서드가 true 를 반환하면 맵에서 가장 오래된 원소를 제거한다. 이 removeEldestEntry 함수를 오버라이드해서 가장 최신 5 개인 데이터만 유지하는 캐시를 만들어보자.
람다 이전에는 아래와 같이 템플릿 메서드 형태로 직접 하위 클래스에서 오버라이딩했었다.
protected boolean removeEldestEntry(Map.Entry<K,V> eldest){
return size() > 5;
}
removeEldestEntry 메서드는 인스턴스 메서드라서 바로 자신 Map 객체의 size() 호출을 통해 맵 안의 원소 수를 알아낼 수 있다. 이 부분을 함수형 인터페이스를 사용해서 전략 패턴으로 바꿔보면 다음과 같이 바꿀 수 있다.
@FunctionalInterface
interface EldestEntryRemovalFunction<K,V>{
boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}
일단, removeEldestEntry 메서드는 인스턴스 메서드이므로 자기 자신도 함수 객체로 건네주어야 한다. Map<K, V> map
그리고 해당 인터페이스가 함수형 인터페이스임을 알 수 있도록 @FunctionalInterface 어노테이션을 붙여준다.
FunctionalInterface 어노테이션을 붙여주면 아래와 같은 이점이 있다.
- 해당 클래스의 코드나 문서를 읽을 이에게 인터페이스가 람다용으로 설계된 것임을 알려준다.
- 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일 되게 해준다.
- 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.
하지만 위와 같이 두 개의 인자를 받고 boolean 값을 리턴하는 모양의 인터페이스가 이미 자바 표준 라이브러리에 존재한다. BiPredicate 인터페이스다.
// java.util.function 패키지에 선언되어 있는 BiPredicate 형태
@FunctionalInterface
public interface BiPredicate<T, U> {
boolean test(T t, U u);
}
// 별도로 인터페이스를 만들 필요 없다. BiPredicate 사용
BiPredicate<Map<String, String>, Map.Entry<String, String>> removalFunction
= (map, eldest) -> map.size() > 5;
별도로 인터페이스를 선언할 필요없이 바로 사용하면 된다. "두 개의 인자를 받아 검증한다" 이외에 다른 기능이 존재하지 않는다면 별도로 만들지 않고 표준 라이브러리 기능을 사용하는 것을 권장한다.
아래 코드는 위에서 언급한 3가지 경우의 수를 테스트해볼 수 있는 코드이다.
OverrideLinkedHashMap 이 직접 클래스에 전략을 override 를 한 케이스(템플릿 메서드 패턴)이고, FunctionalLinkedHashMap 과 BiPredicateLinkedHashMap 이 전략을 함수 인자 형태로 전달한 케이스(템플릿 전략 패턴) 이다.
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiPredicate;
public class MyLinkedHashMap{
@FunctionalInterface
public interface EldestEntryRemovalFunction<K, V> {
boolean remove(Map<K, V> map, Map.Entry<K, V> eldest);
}
public static void main(String[] args) {
// OverrideLinkedHashMap
Map<Integer, Integer> overrideMap = new OverrideLinkedHashMap<>();
for (int i = 0; i < 10; i++){
overrideMap.put(i, i);
}
System.out.println(overrideMap);
// FunctionalLinkedHashMap
Map<Integer, Integer> functionalMap = new FunctionalLinkedHashMap<>((map, eldest) -> map.size() > 5);
for (int i = 0; i < 10; i++){
functionalMap.put(i, i);
}
System.out.println(functionalMap);
// BiPredicateLinkedHashMap
Map<Integer, Integer> functional2Map = new BiPredicateLinkedHashMap<>((map, eldest) -> map.size() > 5);
for (int i = 0; i < 10; i++){
functional2Map.put(i, i);
}
System.out.println(functional2Map);
}
private static class OverrideLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > 5;
}
}
private static class FunctionalLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
private final EldestEntryRemovalFunction<K, V> eldestEntryRemovalFunction;
public FunctionalLinkedHashMap(EldestEntryRemovalFunction<K, V> function) {
this.eldestEntryRemovalFunction = function;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return eldestEntryRemovalFunction.remove(this, eldest);
}
}
private static class BiPredicateLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
private final BiPredicate<Map<K, V>, Map.Entry<K, V>> eldestEntryRemovalFunction;
public BiPredicateLinkedHashMap(BiPredicate<Map<K, V>, Map.Entry<K, V>> function) {
this.eldestEntryRemovalFunction = function;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return eldestEntryRemovalFunction.test(this, eldest);
}
}
}
java.util.function 패키지에 43개의 인스턴스가 포함되어 있으며 아래 6개의 인터페이스가 기본적인 인터페이스다. 나머지는 충분히 유추해낼 수 있다.
인터페이스 | 함수 시그니처 | 예 |
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 |
아래의 6개 인터페이스들은 모두 참조 타용이다. 참고로 기본 인터페이스는, 기본 타입인 int, long, double 용으로 각 3개씩 변형이 생겨난다. 나머지는 코드나 문서를 참고하자.
보통의 경우에는 직접 작성하지 않고 표준 함수형 인터페이스를 사용해야 하지만, 구조적으로 같아도 직접 작성해야 하는 경우가 존재한다. 예를 들어 Comparator<T> 인터페이스는 구조적으로 ToIntBiFunction<T,U> 와 동일하다. 하지만 Comparator 가 독자적인 인터페이스로 남아야 하는 이유는 다음과 같다.
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
- API에서 자주 사용되며, 이름 자체가 용도를 명확히 설명해준다.
- 구현하는 쪽에서 반드시 따라야 하는 규약이 있다.
- 유용한 디폴트 메서드를 여러 개 제공할 필요가 있다. (위 Comparator 예에서는 비교자들을 조합하고 변환하는 메서드를 제공)
마지막으로, 위와 같은 함수형 인터페이스도 주의사항이 있다. 같은 위치의 인수로 받는 메서드들을 다중 정의해서는 안된다. 클라이언트에게 모호함을 안겨줄 뿐만 아니라, 둘 중에 어떤 타입인지 알기 위해 사용하는 쪽에서 타입 형변환을 해야할 수도 있다. "다중정의는 주의해서 사용하라" 라는 아이템 52 조언을 한 번 더 강조한다.
public interface ExecutorService extends Executor {
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
}
Callable 과 Runnable 을 객체로 받는 두 가지 오버라이딩 형태로 함수 이름으로 한 번에 파악이 불가능하다.
"입력값과 반환값에 함수형 인터페이스 타입을 활용하라"
'독후감 > Effective JAVA' 카테고리의 다른 글
[Effective JAVA] 43 "람다보다는 메서드 참조를 사용하라" (0) | 2024.03.02 |
---|---|
[Effective JAVA] 42 "익명 클래스보다는 람다를 사용하라" (1) | 2024.03.02 |
[Effective JAVA] 41 "정의하려는 것이 타입이라면 마커 인터페이스를 사용하라" (0) | 2023.11.06 |
[Effective JAVA] 40 "@Override 애너테이션을 일관되게 사용하라" (1) | 2023.10.29 |
[Effective JAVA] 39 "명명 패턴보다 애너테이션을 사용하라" (1) | 2023.10.24 |