이펙티브 자바 #item31 한정적 와일드 카드를 사용해 API의 유연성을 높여라

임현규·2023년 2월 7일
0

이펙티브 자바

목록 보기
31/47

한정적 와일드 카드를 써야 하는 이유

item 30 에서 잠깐 한정적 와일드 카드를 쓴 경우가 있다. 바로 상속을 고려해서 제네릭을 짜야 할 때이다. 제네릭은 기본적을 불공변이다. List<Object>와 List<String>은 서로 아무런 관계가 없는 타입이라는 뜻이다. 그러나 경우에 불공변보다 조금 더 유연한 방식이 필요할 때가 있다. 이럴 때 한정적 와일드 카드를 활용해 제네릭의 유연성을 높인다.

public class Stack<E> {
	public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

여기에 일련의 원소들을 추가하는 메서드를 추가한다고 하자

public void pushAll(Iterable<E> src) {
	for (E e : src) {
    	push(e);
    }
}

이 코드는 문제는 없지만 완벽한 코드라 볼 수 없다. 그 이유는 만약 Stack<Number>로 선언한 후 pushAll(intVal)을 호출하면 어떻게 될까? intVal은 Integer이다.

논리적으로 볼 때는 될 것 같았지만 컴파일 조차 되지 않는다. 그 이유는 불공변이기 때문에 Number를 상속한 Integer라도 Iterable<Integer>와 Iterable<Number> 는 서로 아무런 연관이 없는 타입이기 때문이다.

이러한 불공변의 문제점을 한정적 와일드 카드를 통해 해결할 수 있다.

public void pushAll(Iterable<? extends E> iterable) {
    for (E e : iterable) {
        push(e);
    }
}

입력을 받을 때 타입 E를 상속받은 매개 변수 타입을 가진 iterable이 입력으로 들어왔을 때 타입 e 형태로 push를 하시오. 라는 뜻이 된다. 이를 통해 상속에 유연한 API가 된다.

비한정적 와일드카드 적용 방법

<? extends E>를 통해 우리는 와일드 카드를 활용하면 상속에 유연한 제네릭 코드를 만들 수 있음을 알았다. 그러나 비한정적 와일드 카드 적용에는 일련의 규칙이 있는데 이를 알아보자.

PECS(펙스)

Producer-Extends Consumer-super 라는 공식으로 공급할 때는 extends를 활용하고 사용할 때는 super를 사용하는 공식이다. 예를 통해 확인해보자

Producer - extends

public static <E> Set<E> union(Set<E> s1, Set<E> s2)

위의 코드를 살펴보면 s1, s2는 생산자이다. 여기서 생산자란 의미는 s1, s2를 입력으로 사용해서 무언가 작업한다는 것이다. 이 경우 E보다 하위 타입을 입력받더라도 공변이면 E로 자동 형변환해서 저장된다. 이를 제네릭에서 가능하게 해야하기 때문에 다음과 같이 코드를 변경하면 된다.

public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2);

반환 매개변수 타입이 E인 이유는 와일드카드를 통해 리턴시 유연성을 높여주는 효과는 없고 클라이언트 코드에도 와일드 카드 타입을 써야 하기 때문이다.

또 다른 예제로 생성자가 있다.

public Chooser(Collection<T> choices)

위의 코드는 생성자에 제네릭을 적용한 모습이다. 그러나 만약에 Chooser 생성사 클래스 타입을 Number로 정의하고 인자로 List<Integer> 타입을 사용한다고 가정하자. 이 경우 T는 상위 클래스인 Number 이고 Integer는 Number의 하위타입이다. 그리고 Collection<T>는 Producer 즉, 생산자가 되며 이를 유연하게 확용하려면 T => ? extends T 형태로 바꾸면 된다.

Consumer - super

Stack 클래스에 다음과 같은 메서드를 만든다고 가정하자.

public void popAll(Collection<E> dst) {
	while (!isEmpty()) {
    	dst.add(pop());
    }
}

해당 코드는 Collection<E> 타입의 dst에 Stack 원소들을 담는 메서드이다. 이를 상속에 유연한 공변 입장에서 생각해보자..

Stack<Number>로 정의된 클래스에 List<Object> 타입의 클래스로 Stack에 내용을 뽑고 싶다. 공변인 경우 당연히 성립한다. 그 이유는 Object는 Number의 상위 타입이기 때문에 충분이 Number를 담아 낼 수 있기 때문이다. 물론 Integer와 같은 하위 타입은 당연히 받을 수 없다. 즉 공변 상황이면 데이터를 가져올 때 상위 타입에서도 유연하게 가져올 수 있어야 한다.

이때 Collection<E>를 소비자라 하며 이 경우엔 다음과 같이 코드를 개선할 수 있다.

```java
public void popAll(Collection<? super E> dst) {
	while (!isEmpty()) {
    	dst.add(pop());
    }
}

이로써 해당 제네릭 코드는 상위 타입에 대해서도 유연한 인자를 가질 수 있게 되었다.

둘다 사용한 복잡한 경우

public static <E extends Comparable<E>> E max(List<E> list) {}

위의 재귀적 와일드카드 코드를 해석하면 다음과 같다.


매개변수화 타입 E는 Comparable<E>를 구현한 타입 E이다.


위는 PECS 모두 적용해야 하는 경우인데 위의 코드의 문제점을 알아보자. 만약 Comaparable 인터페이스를 구현한 클래스가 있고 해당 클래스의 하위 클래스가 있다고 가정하자. 우리가 원하는 결과는 하위 클래스는 따로 Comparable을 구현하지 않더라도 부모 클래스에 구현되어 있기 때문에 부모 클래스에서 정의한 Comparable에 따라 대소관계가 비교되길 원한다. 위의 메서드 또한 리스코프 원칙에 따라 호환되어야하는 것이 맞다. 그러나 하위 타입의 E의 경우 Comparable이 구현되어 있지 않다.

제네릭에서는 Comparable<ParentClass>와 Comparable<ChildClass>는 아무런 관계도 없는 타입이다. 이를 호환되게 만들기 위해서는 Comparable 인자는 E의 상위 타입을 활용할 수 있어야 할 것이다.

코드를 개선해보자.

public static <E extends Comparable<? super E>> E max(List<E> list) {}

정리

책에서 Comparable은 항상 소비자라 한다. 그 이유는 E를 가져와서 대소 관계를 처리하기 때문이다.

구분 방법은 솔직히 어렵다. 그러나 다음과 같이 비교하면 쉬울 것 같다.

  • 공급자: 어떤 인자들을 받아서 사용하는데 상위 타입으로 정의되어 있고 하위 타입을 받아서 작업하는 다형성을 활용하는 경우

  • 소비자: 해당 타입이 데이터를 가져가는 경우(데이터를 가져가는 경우는 해당 타입과 상위타입만 가능)나, 하위타입에서 정의되지 않고 상위 타입에서 정의된 무엇인가를 활용하는 경우

profile
엘 프사이 콩그루

0개의 댓글