반응형

ITEM 31 "한정적 와일드카드를 사용해 API 유연성을 높여라"

 

 

이번 아이템에서 한정적 타입 매개변수를 사용하므로 위 동영상을 통해 아래 개념을 숙지하고 읽는 것을 추천한다.

 

"T extends A 는 상한 한정적 타입 매개변수로 타입 매개변수의 클래스가 A 클래스이거나 A 클래스의 하위 클래스를 의미한다. 반대로 T super A 는 하한 한정적 타입 매개변수로 타입 매개변수의 클래스가 A 클래스이거나 A 클래스의 상위 클래스를 의미한다."

 

매개변수화 타입은 불공변이기 때문에 해당 타입 이외에 다른 타입을 사용할 수 없다. 배열의 공변 특성처럼 상위 클래스나 하위 클래스를 인식할 수 있게 유연한 장치가 필요하다. 

 

public class Stack<E> {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
   	public void pushAll(Iterable<E> src);
    public void popAll(Collection<E> dst);
}

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

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

 

위 스택 클래스를 Number 타입으로 선언하고, pushAll 메서드에 Integer 타입을 넣으면 어떻게 될까?

Integer 가 Number 하위 타입이기 때문에 잘 동작할 것 같지만, 제네릭은 불공변이기 때문에 하위 타입을 인식하지 못해 아래와 같은 에러를 발생시킨다. "error: incompatible types: Iterable<Integer>". 자바는 이러한 불공변에 유연한 설계를 돕기 위해 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다.

 

pushAll 메서드는 E 타입 뿐만 아니라 E 타입의 하위 클래스도 매개 변수로 받을 수 있어야 한다. Iterable<? extends E> 타입을 사용하는 것이 적절하다. Iterable 인터페이스를 통해 E 타입을 "생성" 한다는 특징이 있는 점을 참고하자.

popAll 메서드는 상위 클래스 컬렉션에 저장할 수 있어야 하기 때문에 반대로 E 타입 자신을 포함해서 상위 클래스를 매개 변수로 받을 수 있어야 한다. Collection 인터페이스에서 E 타입을 "소비" 한다는 특징이 있는 점을 참고하자.

 

이처럼 "생성" 과 "소비" 측면으로 바라보면 헤깔리지 않고 상한 경계와 하한 경계를 사용할 수 있다. 책에서는 PECS (producer-extends / consumer-super) 공식을 외워서 사용하라고 권장하고 있다. 매개변수화 타입 T 가 생산자라면 <? extends T> 를 사용하고, 소비자라면 <? super T> 를 사용하면 된다. 위 Stack 예에서 pushAll 의 src 매개변수는 Stack 이 사용할 E 인스턴스를 생성하므로 Iterable<? extends E> 가 적절하다. 한편 popAll 의 dst 매개변수는 Stack 으로부터 E 인스턴스를 소비하므로 Collection<? super E> 가 적절하다.

 

단, 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을 게 없다. 타입을 정확히 지정해야 하는 상황으로, 이때는 와일트카드 타입을 쓰지 말아야 한다.

 

조금 더 복잡한 max 메서드를 살펴보자.

 

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

 

위 max() 메서드를 와일드카드 타입으로 적용하면 아래와 같이 고칠 수 있다.

 

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

 

이번 예제는 PECS 공식이 두 번 적용되었다.

첫 번째로 입력 매개변수 List<E> 는 E 인스턴스를 생산하므로 원래의 List<E> 를 List<? extends E> 로 수정하는 것이 바람직하다.

두 번째는 타입 매개변수 부분이다. E 가 Comparable<E> 를 확장한다고 정의했는데, 이때 Comparable<E> 는 E 인스턴스를 소비한다. 그냥 이렇게 선언만 되어 있을 경우 E 타입만 비교할 수 있게 된다. 그래서 매개변수화 타입 Comparable<E>를 한정적 와일드카드 타입인 Comparable<? super E> 로 대체하는 것이 바람직하다. Comparable 은 언제나 소비자이므로, 일반적으로 Comparable<E> 보다는 Comparable<? super E> 를 사용하는 편이 더 낫다. Comparator도 마찬가지.

 

타입 매개변수와 와일드카드에는 공통 부분이 많아서, 메서드를 정의할 때 둘 중 어느 것을 사용해도 괜찮을 때가 많다. 예를 들어 주어진 리스트에서 명시한 두 인덱스의 아이템들을 교환(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라면 간단한 두 번째가 더 낫다. 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체합니다. 이때 비한정적 타입매개변수라면 비한정적 와일드카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸변 된다.

 

하지만 두 번째 swap 선언에는 문제가 하나 있다. 컴파일이 되지 않는다.

 

public static void swap(List<?> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

 

원인은 리스트의 타입이 List<?> 인데, List<?> 에는 null 이외에는 어떤 값도 넣을 수 없기 때문이다. 이를 해결하기 위해서는 와일드카드 타입의 실제 타입을 알려주는 private 도우미 메서드를 따로 작성하여야 한다.

 

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

// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드
private static <E> void swapHelper(List<E> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

 

swapHelper 메서드는 리스트가 List<E> 임을 알고 있다. 이 리스트에서 꺼낸 값의 타입이 항상 E 이고, E 타입의 값이라면 이 리스트에 넣어도 안전함을 알고 있다. 이상으로 swap 메서드 내부에서는 더 복잡한 제네릭 메서드를 이용했지만, 덕분에 외부에서는 와일드카드 기반의 깔끔한 메서드를 유지할 수 있다는 장점이 있다. 즉, swap 메서드를 호출하는 클라이언트는 복잡한 swapHelper의 존재를 모른 채 그 혜택을 누릴 수 있게 된다.

 

"조금 복잡하더라도 와일드카드 타입을 적용하면 API 가 훨씬 유연해진다."

 

반응형

+ Recent posts