Posts [이펙티브자바] 아이템31-한정적 와일드카드를 사용해 API 유연성을 높이라
Post
Cancel

[이펙티브자바] 아이템31-한정적 와일드카드를 사용해 API 유연성을 높이라

  • 이전 아이템28에서 이야기했듯 매개변수화 타입은 불공변(invariant)이다. 이를 꼭 기억하자.
    • 즉 서로 다른 타입 Type1Type2가 있을 때 List<Type1>List<Type2>의 하위 타입도 상위 타입도 아니다.
    • 예를 들어 List<Object>에는 어떤 객체든 넣을 수 있지만 List<String>에는 문자열만 넣을 수 있다. 즉, List<String>List<Object>가 하는 일을 제대로 수행하지 못하니 하위 타입이 될 수 없다(리스코프 치환 원칙에 어긋난다. 아이템10 참조)
  • 이전에 언급되었던 Stack 예제에서 일련의 원소를 스택에 넣는 public API(pushAll 메서드)를 추가한다고 해보자.
1
2
3
4
5
6
7
8
9
10
11
12
public class Stack<E> {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();

    public void pushAll(Iteable<E> src) {
        for (E e : src) {
            push(e);
        }
    }
}
  • 이 메서드는 깨끗이 컴파일되지만 완벽하진 않다. Iterable의 src의 원소 타입이 스택과 일치하면 잘 동작한다.
  • 하지만 Stack<Number>로 선언한 후 pushAll(intVal)을 호출하면 어떻게 될까? 여기서 intVal은 Integer 타입이다.
  • Integeer는 Number의 하위 타입이니 잘 동작할 것이라 예상이 된다.

image

1
2
3
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = Arrays.asList(3, 1, 4, 1, 5, 9);
numberStack.pushAll(integers);
  • 하지만 실제 오류가 발생한다. 매개변수화 타입이 불공변이기 때문이다.
1
2
3
4
StackTest.java:7: error: incompatible types: Iterable
cannot be converted to Iterable<Number>
Stack.pushAll(integers);
⌃
  • 다행히 해결책은 있다. 자바는 이런 상황에 대처할 수 있는 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원한다.
  • pushAll의 입력 매개변수 타입은 E의 Iteralbe이 아니라 E의 하위 타입의 Iterable이어야 하며, 와일드 카드 타입 Iterable<? extends E>가 정확히 이런 뜻이다.
  • 이를 와일드카드 타입을 사용하도록 puahAll 메서드를 수정하면 아래와 같다.
1
2
3
4
5
6
// 코드 31-2 E 생산자(producer) 매개변수에 와일드카드 타입 적용 (182쪽)
public void pushAll(Iterable<? extends E> src) {
    for (E e : src) {
        push(e);
    }
}
  • 이번에는 pushAll과 짝을 이루는 popAll 메서드를 작성해보자.
  • popAll 메서드는 Stack 안의 모든 원소를 주어진 컬렉션으로 옮겨 담는다.
  • 아래처럼 작성했다고 생각해보자.
1
2
3
4
5
// 코드 31-3 와일드카드 타입을 사용하지 않은 popAll 메서드 - 결함이 있다! (183쪽)
public void popAll(Collection<E> dst) {
    while (!isEmpty())
        dst.add(pop());
}
  • 이번에도 주어진 컬렉션의 원소 타입이 스택의 원소 타입과 일치한다면 말끔히 컴파일되고 문제없이 작동된다.
  • 하지만 이번에도 역시나 완벽하진 않다. Stack의 원소를 Object용 컬렉션으로 옮기려 한다 했을때 "Collection"는 Collection의 하위 타입이 아니다" 라는, 위의 예시와 유사한 오류가 발생한다.</b>
  • 이번에도 와일드카드 타입으로 해결할 수 있다.
  • popAll의 입력 매개변수의 타입이 ‘E의 Collection이 아니라 E의 상위 타입의 Collection’ 이어야 한다.
1
2
3
4
5
// 코드 31-2 E 생산자(producer) 매개변수에 와일드카드 타입 적용 (182쪽)
public void pushAll(Iterable<? extends E> src) {
    for (E e : src)
        push(e);
}
  • 메시지는 분명하다. 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용해라!
    • 한편, 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을 게 없다.
    • 타입을 정확히 지정해야 하는 상황으로, 이때는 와일드카드 타입을 쓰지 말아야 한다.

팩스(PECS) 원칙: producer-extends, consumer-super

  • 즉, 매개변수 타입 T가 생산자라면 <? extends T>를 사용하고, 소비자라면 <? super T>를 사용해라.
    • Stack 예에서 pushAll의 src 매개변수는 Stack이 사용할 E 인스턴스를 생산하므로 src의 적절한 타입은 Iterable<? extends E>이다. 한편, popAll의 dst 매개변수는 Stack으로부터 E 인스턴스를 소비하므로 dst이 적절한 타입은 Collection<? super E>이다.
  • PECS 원칙은 와일드 카드 타입을 사용하는 기본 원칙이다.

팩스 원칙을 적용한 예시

1) 아이템28의 Chooser 클래스

  • Chooser 생성자는 아래와 같이 선언했다.
1
public Chollser(Collection<T> choices)
  • 이 생성자로 넘겨지는 choices 컬렉션은 T 타입의 값을 생산 하기만 하니 T를 확장하는 와일드 카드 타입을 사용해 선언해야 한다.
  • 이를 수정한 모습은 아래와 같다.
1
2
3
4
// 코드 31-5 T 생산자 매개변수에 와일드카드 타입 적용 (184쪽)
public Chooser(Collection<? extends T> choices) {
    choiceList = new ArrayList<>(choices);
}
  • Chooser<Number>의 생성자에 List<Integer>를 넘기고 싶다고 해보자.
    • 수정 전 생성자에선 컴파일조차 되지 않지만, 수정 후 생성자에선 문제가 전혀 되지 않는다.

2) 코드 30-2의 union 메서드

1
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
  • s1과 s2 모두 E의 생성자이니 PECS 공식에 따라 다음처럼 선언해야 한다.
1
2
3
4
5
6
public static <E> Set<E> union(Set<? extends E> s1,
                                   Set<? extends E> s2) {
    Set<E> result = new HashSet<E>(s1);
    result.addAll(s2);
    return result;
}

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

1
2
3
Set<Integer> integers = Set.of(1, 3, 5);
Set<Double> doubles = Set.of(2.0, 4.0, 6.0);
Set<Number> numbers = union(integers, doubles);
  • 수정한 선언을 사용하면 위 코드도 말끔히 컴파일되어 동작한다.
  • 사용자는 와일드 카드 타입이 쓰였다는 사실조차 의식하지 못할 것이다.
  • 받아들여야 할 매개변수를 받고 거절해야 할 매개변수는 거절하는 작업이 알아서 이뤄진다.
  • 클래스 사용자가 와일드카드 타입을 신경써야 한다면 그 API에 무슨 문제가 있을 가능성이 크다.
  • 자바 7까진 타입 추론 능력이 충분히 강력하지 못해서 문맥에 맞는 반환 타입을 명시해야 한다.
    • ex) Set<Number> numbers = Union.<Number>union(integers, doubles);

Note: 매개변수(parameter)와 인수(argument)의 차이를 인지하자. 매개변수는 메서드 선언에 정의한 변수이고, 인수는 메서드 호출시 넘기는 ‘실제값’이다.

3) 코드 30-7의 max 메서드

1
2
public static <E extends Comparable<E>> E max(List<E> list)
public static <E extends Comparable<? super E>> E max(List<E> list)
  • 두 번재 max 메서드는 펙스 공식을 두 번 적용했다.
    • 입력 매개변수에서는 E 인스턴스를 생산하므로 원래의 List<E>List<? extends E>로 변경했다.
    • Comparable<E>는 E 인스턴스를 소비하므로 한정적 와일드 카드 타입을 적용해 Comparable<? super E>로 대체했다.
  • Comparable은 언제나 소비자이므로, 일반적으로 Comparable<E>보다는 Comparable<? super E>를 사용하는 편이 낫다.
  • 위와 같은 내용들이 복잡해보일 수 있지만 충분히 가치있는 타입 선언이다.
1
List<ScheduledFuture<?>> scheduleFutures = ...;

스크린샷 2022-08-23 오후 10 57 55 출처: https://rok93.tistory.com/entry/아이템31-한정적-와일드카드를-사용해-API-유연성을-높이라 [티끌모아 로키산맥 🏔:티스토리]

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

타입 매개변수와 와일드카드에는 공통되는 부분이 있어서, 메서드를 정의할 때 둘 중 어느 것을 사용해도 괜찮을때 어떻게 하면 좋을까?

  • 예를 들어, 주어진 리스트에서 명시한 두 인덱스의 아이템들을 교환(swap) 하는 정적 메서드를 두 방식 모두로 정의해보자.
1
2
public static <E> void swap(List<E> list, int i, int j); // 비한정적 타입 매개변수(아이템30)을 사용
public static void swap(List<?> list, int i, int j); // 비한정적 와일드 카드 사용
  • public API 라면 간단한 두 번째가 낫다.
  • 어떤 리스트든 이 메서드에 넘기면 명시한 인덱스의 원소들을 교환해줄 것이다. 신경써야 할 타입 매개변수도 없다.
    • 첫 번째도 문제 없이 교환할 수 있지 않을까? 어떤 문제가 있을까?라는 생각이 드는데 나중에 직접 테스트를 해보도록 하자.
  • 기본 규칙은 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하라.
  • 이때 비한정적 타입 매개변수라면 비한정적 와일드카드로 바꾸로, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 된다.
  • 하지만 두 번째 swap 선언에선 문제가 하나 이쓴ㄴ데 아래처럼 직관적으로 구현한 코드가 컴파일되지 않는 다는 것이다.
1
2
3
public static void swap(List<?> list, int i, int j) {
    list.set(i, list.get(j, list.get(i)));
}
  • 이 코드를 컴파일하면 에러 메시지가 나온다.
1
2
Swap.java:5: error: incompatible types: Object cannot beconverted to CAP#1list.set(1, list.set(i, list.get(j)));
where CAP#1 is a fresh type-variable:CAP#1 extends Object from capture of ?
  • 원인은 리스트의 타입이 List<?>인데, List<?>에는 null 외에는 어떤 값도 넣을 수 없다는 데 있다.
  • 다행히 (런타임 오류를 낼 가능성이 있는) 형변환이나 리스트의 로 타입을 사용하지 않고도 해결할 길이 있다. 바로 와일드카드 타입의 실제 타입을 알려주는 메서드를 private 도우미 메서드로 따로 작성하여 활용하는 방법이다.
    • 실제 타입을 알아내려면 이 도우미 메서드는 제네릭 메서드여야 한다. 아래 코드를 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)));
}

public static void main(String[] args) {
    // 첫 번째와 마지막 인수를 스왑한 후 결과 리스트를 출력한다.
    List<String> argList = Arrays.asList(args);
    swap(argList, 0, argList.size() - 1);
    System.out.println(argList);
}
  • swapHelper 메서드는 리스트가 List 임을 알고 있다. 즉, 이 리스트에서 꺼낸 값의 타입은 항상 E이고, E타입의 값이라면 이 리스트에 넣어도 안전함을 알고 있다. 그러기에 정상적으로 문제 없이 동작하게 된다.
1
조금 복잡하더라도 와일드카드 타입을

핵심 정리: 조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다. 그러니 널리 쓰일 라이브러리엔 반드시 와일드카드 타입을 적절히 사용해줘야 한다. 생산자에는 extends 소비자에는 super를 사용하는 PECS를 기억하자. 추가적으로 Comparable과 Comparator는 모두 소비자라는 사실도 잊지 말자.

참고 및 출처

This post is licensed under CC BY 4.0 by the author.

[이펙티브자바] 아이템30-이왕이면 제네릭 메서드로 만들자

[JPA] Connection 누수