[Effective Java] 아이템 44 : 표준 함수형 인터페이스를 사용하라

Loopy·2022년 9월 9일
0

이펙티브 자바

목록 보기
43/76
post-thumbnail

☁️ 개요

자바8 부터 람다를 지원하면서, API를 작성하는 모범 사례들도 크게 바뀌었다. 예시로, 상위 클래스의 기본 메서드를 재정의해 원하는 동작을 구현하는 템플릿 메서드 패턴을 보자.

템플릿 메서드 패턴

템플릿 메서드 패턴은 추상 클래스의 상속을 통해 이루어진다.

abstract class Cook {
	// 공통 메서드
    public void cooking() {
        System.out.println("음식 조리 시작");
        variant();
        System.out.println("음식 조리 완료");
    }
    
    // 변화하는 부분
    abstract void variant();
}

class Baking extends Cook {
	@override
    public void variant() {
     	...
    }
}

람다 사용

람다를 통해, 변하는 부분을 함수 객체를 캡슐화하고 이를 매개변수로 받는 정적 팩터리나 생성자를 제공할 수 있다. 전략 패턴(템플릿 콜백 패턴)이라고도 한다.

public class Cook {
     public void cooking(Strategy strategy) {
        System.out.println("음식 조리 시작");
        strategy.variant();
        System.out.println("음식 조리 완료");
    }
}
Cook.cooking(() -> (System.out.println("라면 끓이는중");));

함수형 매개변수 타입은 직접 구현할 수도 있지만, 뒤에 나오듯이 이미 만들어져 있는 표준 함수형 인터페이스를 사용하는 것이 좋다.

☁️ 함수형 인터페이스 예시

LinkedHashMap 을 예로 들어보자.
removeEldestEntry() 함수는 맵에 새로운 키를 추가할때인 put() 메서드에 의해 호출되는데, 해당 메서드가 true 를 반환하면 맵에서 가장 오래된 원소를 제거한다.

람다를 사용하지 않은 경우

protected boolean removeEldestEntry(Map.Entry<K,V> eldest){
	return size() > 100;  //size : 호출된 맵 안의 원소 수를 알아냄
}

removeEldestEntry() 는 인스턴스 메서드라서 바로 size() 호출을 통해 맵 안의 원소 수를 알아낼 수 있다.

하지만 만약 함수 객체로 구현한다면, 생성자에 넘기는 함수 객체는 맵의 인스턴스 메서드가 아니기 때문에 자기 자신도 함수 객체에 건네줘야 한다.

이를 반영하여 함수 객체를 받는 정적 팩터리나 생성자로 구현한다면, 다음과 같다.

람다를 사용한 경우 : 함수형 인터페이스

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

하지만 이미 자바 표준 라이브러리에 같은 모양의 인터페이스가 존재한다.

🔖 @FunctionalInterface
직접 만든 함수형 인터페이스는 해당 어노테이션이 반드시 존재해야 한다.

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

표준 함수형 인터페이스를 사용한 경우

@FunctionalInterface
public interface BiPredicate<T, U> {
    boolean test(T t, U u);
}
BiPredicate<Map<String, String>, Map.Entry<String, String>> predicate
     = (k, v) -> k.size() > 100;

필요한 용도에 맞는게 있다면 직접 구현하지 말고 표준 함수형 인터페이스를 활용하자.

☁️ 표준 함수형 인터페이스 종류

아래의 6개 인터페이스들은 모두 참조 타용이다. 참고로 기본 인터페이스는, 기본 타입인 int, long, double 용으로 각 3개씩 변형이 생겨난다.

1. Operator 인터페이스

  1. UnaryOperator<T>
    반환값과 인수의 타입이 같은 함수를 뜻하며, 인수가 한개이다.
  @FunctionalInterface
  public interface UnaryOperator<T> extends Function<T, T> {
     ...
  }

  @FunctionalInterface
  public interface Function<T, R> {
      R apply(T t);
  }
  String::toLowerCase      // str -> str.toLowerCase();
  1. BinaryOperator<T>
    반환값과 인수의 타입이 같은 함수를 뜻하며, 인수가 두개이다.
  @FunctionalInterface
  public interface BinaryOperator<T> extends BiFunction<T,T,T> {
     ...
  }
  @FunctionalInterface
  public interface BiFunction<T, U, R> {
      R apply(T t, U u);
  }
  BinaryOperator<BigInteger> op = BigInteger::add   //  (int1, int2) -> int1.add(int2);
  op.apply(BigInteger.valueOf(10L), BigInteger.valueOf(20L));  // 함수형 프로그래밍

2. Predicate 인터페이스

인수 하나를 받아 boolean 을 반환하는 함수이다. 인자로 주어진 매개변수가 함수의 조건에 맞다면 true 를 반환하고, 아니라면 false 를 반환한다.

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
} 
Predicate<String> predicate = Collection::isEmpty // (str) -> str.isEmpty();
predicate.test("string");

Predicate 사용 예시

Predicate타입을 체크할 때 사용하면 좋다. 사용하지 않는 경우, 아래와 같이 조건문을 통해 클래스의 특정 타입 검사를 진행해야 하기 때문이다.

if (Vehicle.Type.SUV.equals(vehicle.getType())) {
    // ...
} else {
    // ...
}

Predicate 을 사용한다면, 람다를 이용하여 검사 로직을 독립적인 함수의 형태로 구현할 수 있다. 따라서 여러 검사 로직을 하나 하나 구체 클래스로 구현하지 않아도, 다양한 형태의 구현이 가능해졌다.

@RequiredArgsContructor
public class VehiclePredicator {
    private final Vehicle vehicle;  

    public boolean predicate(Predicate<Vehicle> predicate) {   // 표준 함수형 인터페이스
        return predicate.test(vehicle);
    }
}
public class VehiclePredicateService {
    ...
    public void predicate() {
        boolean isSuv = vehiclePredicator.predicate(
            (Vehicle vehicle) -> vehicle.getType().equals(Vehicle.Type.SUV)
        );
        boolean isSedan = vehiclePredicator.predicate(
            (Vehicle vehicle) -> vehicle.getType().equals(Vehicle.Type.SEDAN)
        );
    }
}

3. Function 인터페이스

인수와 반환 타입이 다른 함수를 뜻한다.

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
Function<Integer, List<Integer>> function = Arrays::asList; // x -> Arrays.asList(x);
function.apply(3);

4. Supplier 인터페이스

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

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
Supplier<Instant> supplier = Instant::now // () -> Instant.now();
supplier.get();

☁️ Consumer 인터페이스

인수를 하나 받고 반환값은 없는(특히 인수를 소비하는) 함수를 뜻한다. 다른 대부분의 함수형 인터페이스들과 달리, Consumer 는 부작용(side-effects)을 일으켜 동작하는 것을 기대한다.

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);  
System.out::println

☁️ Comparator와 표준 함수형 인터페이스

보통의 경우에는 직접 작성하지 않고 표준 함수형 인터페이스를 사용해야 하지만, 구조적으로 같아도 직접 작성해야 하는 경우가 존재한다.

예를 들어 Comparator<T> 인터페이스는, 구조적으로 ToIntBiFunction<T,U> 와 동일하다. 하지만 Comparator 가 독자적인 인터페이스로 남아야 하는 이유는 다음과 같았다.

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}
  1. API에서 자주 사용되며, 이름 자체가 용도를 명확히 설명해준다.
  2. 구현하는 쪽에서 반드시 따라야 하는 규약이 있다.
  3. 비교자들을 변환하고 조합해주는 유용한 디폴트 메서드를 제공할 수 있다.

☁️ 함수형 인터페이스 주의사항

서로 다른 함수형 인터페이스를, 같은 위치의 인수로 받는 메서드들을 다중 정의해서는 안된다. 아래와 같이 CallableRunnable 함수형 인터페이스를 다중 정의한 것을 볼 수 있다.

하지만 이는 올바른 다중정의 메서드를 선택하기 위해 인자에서 형변환 해야 할 때가 생기기 때문에 좋지 않다. (자세한 내용은 아이템 52: 다중정의는 신중히 사용하라를 참고하자.)

public interface ExecutorService extends Executor {
    <T> Future<T> submit(Callable<T> task);
    Future<?> submit(Runnable task);
}

📚 핵심 정리
입력값과 반환값에 함수형 인터페이스 타입을 활용하자. 보통은 java.util.function 패키지의 표준 함수형 인터페이스를 사용하는 것이 가장 좋은 선택이지만, 직접 새로운 함수형 인터페이스를 만들어 쓰는 편이 나을 수도 있다.

참고자료
https://7772.github.io/2021-05-29-lambda-functional-interface/

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글