이펙티브 자바 #item44 표준 함수형 인터페이스를 사용하라

임현규·2023년 4월 25일
0

이펙티브 자바

목록 보기
44/47
post-custom-banner

자바 트렌드

책에서는 템플릿 패턴보다는 어떤 효과가 있는 함수 객체를 받는 정적 팩토리나 생성자를 제공하는 것이다. 말이 어려운데 조금 더 간단히 이야기하면 상속 패턴을 활용해서 구체적인 로직을 구현하는 것보다 로직을 구현한 함수형 객체를 주입하는 것이 조금 더 유연하고 확장성 좋게 설계가능하다는 것이다. 예시를 들어보자

템플릿 패턴 예: LinkedHashMap

LinkedHashMap에는 오래된 노드를 제거해주는 메서드가 존재한다.

removeEldestEntry는 노드를 삽입 후 특정 조건을 확인하는 로직을 담당하는 메서드로 이것이 true면 removeNode를 수행한다

LinkedHashMap에서는 기본이 false로 동작하지 않는다. 그러나 이를 상속한다면 protected 특성을 활용해서 자식 객체에서 오래된 노드를 삭제할 수 있도록 할 수 있다.

이를 잘 활용한 것은 LruCache이다.

LruCache는 Map과 동일하게 캐싱을 수행하지만 지정한 사이즈를 초과하는 경우 가장 처음에 삽입했던 오래된 노드를 제거해서 사이즈를 유지하는 역할을 수행한다. 이를LinkedHashMap을 상속받고 removeEldestEntry만 따로 구현하면 아주 쉽게 LruCache를 구현할 수 있다.

이 방법이 나쁜 방법은 아니다. 실제로 Spring에서도 이런 템플릿 패턴을 애용한다. 그 이유는 내가 필요한 로직만 상속을 통해 확장 가능하고, 복잡한 로직은 부모 로직에서 이미 다 구현되어 있기 때문이다.

LinkedHashMap을 함수형 객체 형식으로 사용해보기

removeEldesetEntry로직은 생각보다 간단하고 람다로도 적용 가능할 것 같이 보인다. 차라리 이런 경우 인스턴스로 Predicate 객체를 받고 이를 removeEldestEntry에 활용한다면 상속 사용을 할 필요 없이 쉽게 객체를 생성할 수 있다.

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{
	private final Predicate<Map<K, V>> removeEldestEntryStrategy;
    
    public LinkedHashMap( //...., Predicate<Map<K, V>> strategy) {
    	
    }
}

이런 방식으로 정의하면 LruCache는 쉽고 유연하게 구현 가능하다

LinkedHashMap<String, String> lruCache = new LinkedHashMap<>(entry -> entry.size() > 10);

사실 개선이라고 보기에는 좀 그렇긴 하지만 책에서는 트렌드라고 한다. 하지만 엄격하게필요한 로직을 정의하고 거대한 프로젝트의 복잡한 로직중 일부만을 상속해서 구현하는 템플릿 패턴이 잘 활용하면 가독성이 좋고, 안정적인 설계가 가능할 것 같다. 하지만, 상속을 활용하기 때문에 상속이 가지는 부작용은 항상 고려해야하고 관리해야 하는 클래스 파일이 많아질 수 있다.

함수객체를 주입한다면 표준 함수형 인터페이스를 활용하자

함수형 객체를 선언하기 위해서는 @FunctionalInterface을 class에 정의해주면 된다. 이렇게 정의하면 메서드가 하나임을 보장하는 마커 인터페이스인데 컴파일 단계에서 메서드가 여러개면 에러를 던진다.

하지만 굳이 FunctionalInterface를 직접 구현하는 것보다는 이미 제공하는 함수형 인터페이스를 활용하는 것이 훨씬 효과적이다. 그 이유는 이미 제공하는 표준 함수형 인터페이스는 풍부한 default 메서드를 제공하고, 쉽게 활용할 수 있기 때문이다.

함수형 인터페이스는 크게 6가지를 기억하면 된다.

  • UnaryOperator<T>: 같은 인자로 하나의 입력을 받고 같은 타입을 반환함
  • BinaryOperator<T>: 2개의 같은 인자를 입력으로 받고 같은 타입을 반환함
  • Predicate<T>: 타입 인자 하나를 받고, boolean으로 같은 타입을 반환함
  • Function<T, R>: 인자 하나를 받고, 다른 타입을 반환
  • Supplier<T>: 인자를 받지 않지만, 결과를 반환함
  • Consumer<T>: 인자를 받지만, 결과를 반환하지 않음

이 6개에서 파생한 여러 함수형 인터페이스가 존재한다

표준 함수형 인터페이스를 활용할 때 기본타입을 박싱해서 사용하지 말자

우리가 int 타입을 처리해야 한다고 가정하자 그런데 이를 Predicate<Integer> 이런식으로 사용하는것은 굉장히 비효율적이다. 그 이유는 매번 인자를 받을때 박싱하고 연산할 때 언박싱하면서 데이터를 처리하기 때문이다. 클래스 생성 비용은 생각보다 비싸다.

이런 경우에는 IntPredicate를 활용하면 된다. 이는 int를 인자 타입으로 받는다. 이렇게 성능 문제상 원시 타입을 받아야하는 경우에는 매개변수 타입 지정을 하지 말고 java.util.function 패키지에서 지원하는 클래스가 있는지 찾아보자. 대부분 다 지원할 것이다.

@FunctionalInterface를 사용해야 할 때

보통은 표준 함수형 인터페이스를 사용하지만 위의 마커 어노테이션을 사용해야 할 때가 있다.

  1. 자주 쓰이며, 이름 자체가 용도를 명확히 설명해줄 때
  2. 반드시 따라야 하는 규약이 있을 때
  3. 유용한 디폴드 메소드를 제공해줘야 할 때

이 세가지를 따르는 대표적인 예는 Comparator이다.
해당 interface는 이름을 통해 어떤 역할인지 명확히 파악할 수 있다. 그리고 Comparator 선언시 compare로 어느정도 지켜야할 규약이 있다. 또한 default 메서드로 팩토리 및 메서드 체이닝을 지원해서 Comparator 인터페이스를 유연하게 사용할 수 있도록 한다. 이런 경우 따로 선언해서 사용하면 유용하다.

만약 단순히 이름을 통해 역할을 구분하고, 특별한 default 메서드를 만들지 않는다면 굳이 사용할 필요는 없다.

profile
엘 프사이 콩그루
post-custom-banner

0개의 댓글