본문 바로가기

개발/이펙티브 자바

Effective Java ( 이펙티브 자바 ) - 아이템 31

한정적 와일드카를 사용해 API 유연성을 높이라


매개변수화 타입은 불공변이다. 즉, 서로 다른 타입 Type1과 Type2가 있을 때 List<Type1>은 List<Type2>의 하위 타입도 상위 타입도 아니다. List<String>은 List<Object>의 하위 타입이 아니라는 뜻인데, 곰곰이 따져보면 사실 이쪽이 말이 된다. List<Object>에는 어떤 객체든 넣은 수 있지만 List<String>에는 문자열만 넣을 수 있다. 리스코프 치환 원칙에 어긋난다. 

 

  하지만 때론 불공변 방식보다 유연한 무언가가 필요하다. 여기 Stack의 public API를 추려보았다.

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);
}

이 메서드는 깨끗이 컴파일되지만 완벽하진 않다. Iterable src의 원소타입이 스택의 원소 타입과 일치하면 잘 작동한다. 하지만 Stack<Number>로 선언한 후 pushAll(intVal)을 호출하면 어떻게 될까? 여기서 intVal은 Integer 타입이다. Integer는 Number의 하위 타입이니 논리적으로는 잘 동작해야 할 것 같지만, 오류 메시지가 뜬다. 

매개변수화 타입은 불공변이기때문이다

 

다행히 해결책은 있다. 한정적 와일드카드 타입이라는 타입을 지원한다. pushAll의 입력 매개변수 타입은 'E의 Iterable'이 아니라 'E의 하위 타입의 Iterable'이어야 하며, 와일드 카드 타입 Iterable<? extends E> 가 정확히 이런 뜻이다. 

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

 

그 다음으로 짝을 이루는 popAll 메서드를 작성할 차례다. 

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

이번에도 역시나 완벽하진 않다. Stack<Number>의 원소를 Object용 컬렉션으로 옮기려 한다고 해보자. 

Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = ...;
numberStack.popAll(objects);

"Collection<Object>는 Collection<Number>의 하위 타입이 아니다. 라는 앞서와 비슷한 오류가 발생한다. 이번에는 반대로 'E의 상위 타입의 Collection' 이어야 한다. 상위타입에만 꺼낼 수 있으니까!

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

 

결론은 분명하다. 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라. 한편, 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을게 없다. 타입을 정확히 지정해야 하는 상황이라 쓰지 말아야 한다.

 

  다음 공식을 외워두면 어떤 와일드카드 타입을 써야 하는지 기억하는 데 도움이 될 것이다.

PECS(펙스): proucer-extedns, consumer-super

Stack 예에서 pushAll의 src 매개변수는 Stack이 사용할 E 인스턴스를 "생산", popAll의 dst 매개변수는 Stack으로부터 E 인스턴스를 소비. 

 

코드 30-2의 union 메서드는 

public static <E> Set<E> union(Set<E> s1, Set<E> s2) 로 바뀔 수 있는데,  s1과 s2 모두 E의 생산자니까,

public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) 로 바뀐다.

 

  이때 반환 타입은 여전히 Set<E>다. 반환 타입에는 한정적 와일드카드 타입을 사용하면 안 된다. 유연성을 높여주기는 커녕 클라이언트 코드에서도 와일드카드 타입을 써야 하기 때문이다

 

제대로만 사용하면 클래스 사용자는 와일드카드 타입이 쓰였다는 사실조차 의식하지 못할 것이다. 클래스 사용자가 와일드카드 타입을 신경 써야 한다면 그 API에 무슨 문제가 있을 가능성이 크다. 

 

 

 

  이번에는 코드 30-7의 max 메서드에 주목해보자. 원래 버전은 다음과 같다. 

public statkc <E extends Comparable<E>> E max(List<E> list)

바뀐 버전은 아래와 같다. 

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

PECS 공식을 두 번 적용했다. 입력 매개변수에서는 E 인스턴스를 생산하므로 List<? extends E> 로 수정했다.

 

  타입 매개변수 E는 난해하다. 이 책에서 타입 매개변수와 와일드카드를 적용한 첫 번째 예이기도 하다. 원래 선언에서는 E가 Comparable<E>를 확장한다고 정의했는다, 이때 Comparable<E>는 E 인스턴스를 소비한다(소비해서 선후 관계를 뜻하는 정수를 생산한다.) 그래서 매개변수화 타입 Comparable<E>를 한정적 와일드카드 타입인 Comparable<? super E>로 대체 했다. Comparable은 언제나 소비자이므로, 일반적으로 Comparable<E>보다는 Comparable<? super E>를 사용하는 편이 낫다. Comparator도 마찬가지다. 

  수정된 버전의 max는 이 책에서 가장 복잡한 메서드 선언일 것이다. 이렇게까지 복잡하게 만들 가치가 있다. 그 근거로, 다음 리스트는 오직 수정된 max로만 처리할 수 있다.

List<ScheduledFuture<?>> scheduledFutures = ...;

수정 전 max가 이 리스트를 처리할 수 없는 이유는 (java.util.concurrent 패키지의) ScheduledFuture가 Comparale<ScheduledFuture> 를 구현하지 않았기 때문이다. ScheduledFuture는 Delayed의 하위 인터페이스고, Delayed는 Comparable<Delayed>를 확장했다. 다시 말해, ScheduledFuture의 인스턴스는 다른 ScheduledFuture 인스턴스뿐 아니라 Delayed 인스턴스와도 비교할 수 있어서 수정 전 max가 이 리스트를 거부하는 것이다.  더 일반화해서 말하면, Comparable(Comparator)을 직접 구현하지 않고, 직접 구현한 다른 타입을 확장한 타입을 지원하기 위해 와일드카드가 필요하다.

 

public interface Comparable<E>
public interface Delayed extends Comparable<Delayed>
public interface ScheduledFuture<V> extends Delayed, Future<V>

 

와일드카드와 관련해 논의해야 할 주제가 하나 더 남았다. 타입 매개변수와 와일드카드에는 공통되는 부분이 있어서, 메서드를 정의할 때 둘 중 어느 것을 사용해도 괜찮을 때가 많다. 예를 들어 주어진 리스트에서 명시한 두 인덱스의 아이템들을 교환(swap)하는 정적 메서드를 두 방식 모두를 정의해보자. 다음 코드에서는 첫 번째는 비한정적 타입 매개변수를 사용했고 두 번째는 비한정적 와일드카드를 사용했다.

public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);

public API라면 간단한 두 번째가 낫다. 어떤 리스트든 이 메서드에 넘기면 명시한 인덱스의 원소들을 교환해 줄 것이다. 

 

  기본 규칙은 이렇다. 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하라. 이때 비한정적 타입 매개변수라면 비한정적 와일드카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 된다. 

 

족므 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다. 그러니 널리 쓰일 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용해줘야 한다. PECS 공식을 기억하자. 즉, 생산자(producer)는 extends를 소비자(consumer)는 super를 사용한자. Comprable과 Comparator는 모두 소비자라는 사실도 잊지 말자.