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

2024. 10. 28. 21:02·1️⃣ 백앤드/이펙티브 자바
한 시간 반 동안 정리한 내용이 날아갔다. . . 두 번째 쓰는 [아이템 31] 

하...

1. 매개변수화 타입은 불공변이다.

이전 장에서 학습한 매개변수화 타입에 대해 다시 한번 복기해보자. 아이템 26에서, 로 타입과 매개변수화 타입을 비교하며 학습했다. 매개변수화 타입은, 클래스나 메서드에 특정 타입을 매개변수로 전달하여 코드의 타입 안전성과 재사용성을 높이는 방식이다. 그리고 불공변의 특성상, 서로 다른 타입 Type1 과 Type2 가 있을 때, List<Type1> 은 List<Type2> 의 하위 타입도 상위 타입도 아니다. 아래 예시와 같이, 

List<Object> objList = new ArrayList<String>();  // 컴파일 오류

자식 클래스 (String) 가 부모 클래스 (Object) 클래스로 대체될 수 없기 때문에, 컴파일 시에 오류가 발생한다. 하지만, 이런 불공변 방식이 만능일까? 더욱 유연한 무언가가 필요할 때가 있다. 아래 Stack 클래스와 public API 를 확인해보자.

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

네 개 메서드에 더해, 추가로 일련의 원소를 한 번에 스택에 넣는 메서드를 추가해야 한다고 가정해보자.

 

1-1. Stack 클래스에 pushAll 메서드 만들기

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

이 메서드는 컴파일은 되지만, 완벽한 형태는 아니다. 만약 아래와 같이 pushAll 메서드를 호출한다고 해보자.

Stack<Number> numberStack = new Stack<>();
Iterable<Integer> intergers = ...;
numberStack.pushAll(integers);

타입이 Number 인 스택을 한번에 꺼내 모조리 integers 반복자에 넣고 싶다. 이 때 무엇이 문제일까? 겉으로 보기엔 잘 동작할 것 같은데, 문제는 불공변의 특성에 있다. 3행이 동작하기 위해선, Iterable<E> src = new Iterable<Integer>() ... 이 동작해야 하는데, 'E의 Iterable' 이 아니라 불공변의 특성 상 pushAll 의 매개변수는 'E의 하위타입의 Iterable' 이 와야 정확하다. 여기선 E 가 Number 이므로 하위 타입 (Integer) 의 Iterable 이 와야한다는 것이다. 

StackTest.java:7: error: incompatible types: Iterable<Integer>
cannot be converted to Iterable<Number>
    numberStack.pushAll(integers);
                        ^

위와 같은 에러가 발생하게 된다. 그렇다면, 불공변 문제를 해결할 방법은 없는 것일까? 바로, 한정적 와일드카드 타입으로 해결할 수 있다. 한정적 와일드카드 타입이 뭐였더라?

 

* 한정적 와일드카드 타입이란

한정적 와일드카드는 특정 타입의 상위 또는 하위 클래스만 허용하도록 제한한다. 두 가지로 나뉘는데, 상한 한정 와일드카드 타입과 하한 한정 와일드카드 타입이 있다.

public void printNumbers(List<? extends Number> list) {
    for (Number number : list) {
        System.out.println(number);
    }
}

상한 한정 와일드카드 타입은<? extends T> 의 형태로 특정 타입 T 의 하위 클래스만 허용하며, 주로 읽기 작업에서 사용된다. 위 예시에선 Number, Integer, Double 등이 허용된다.

public void addIntegers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
}

하한 한정 와일드카드 타입은 <? super T> 형태로  특정 타입 T의 상위 클래스들만을 훠용하며, 주로 쓰기 작업에서 사용된다. 위 예시에선 Number, Object 이 올 수 있다.

 

그렇다면 한정적 와일드카드 타입을 사용하여 pushAll 메서드를 수정해보자.

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

Iterable<? extends E> 로 수정함으로써, Stack 클래스에서 Number 타입으로 객체를 생성했을 때 Iterable<Integer> 타입을 pushAll 매개변수로 집어넣을 수가 있게되었다. 이번 수정으로, 컴파일 에러도 발생하지 않고 클라이언트도 깔끔하게 사용할 수 있다.

 

1-2. Stack 클래스에 popAll 메서드 만들기

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

주어진 Stack 원소를 모두 매개변수에 옮겨 담는다. 이번에도 컴파일은 되지만, 불공변의 문제가 다시 찾아온다.

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

Stack<Number> 의 원소를 Object 컬렉션에 옮겨 담으려 popAll 메서드를 사용할 때, 문제가 발생한다. Collection<Object> 는 Collection<Number> 의 하위타입이 아니라는 에러를, 아래와 같이 하한 한정 와일드카드 타입을 이용해 고쳐보자.

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

이렇게 하면, Stack 과 클라이언트 코드 모두 컴파일 에러 없이 해결된다.

 

1-3. 생산자 vs 소비자

그러면, 한정적 와일드카드 타입을 사용함에 있어 상한과 하한을 어떻게 구분하여 사용해야 하는가? 여기서 PECS 원칙의 생산자와 소비자의 개념이 나온다.

매개변수화 타입 T 가 생산자라면 상한 <? extends T> 를, 소비자라면 하한 <? super T> 를 사용한다.

위 Stack 예시에서, Stack 이 사용할 E 인스턴스를 생산하므로 pushAll 은 상한을 사용하고 그와 반대로 E 인스턴스를 소비하는 popAll 은 하한을 사용한다. 이를 겟풋원칙으로 부르기도 한다.

 

2. 다시 살펴보는 메서드와 생성자 선언

2-1. [아이템 28] chooser 클래스

아이템 28에서 살펴봤던 아래  chooser 클래스의 생성자를 와일드카드 타입을 적용해 수정해보자.

public class Chooser<T> {
    private final List<T> choiceList;

    public Chooser(Collection<T> choices) {
        this.choiceList = new ArrayList<>(choices);
    }
    
    public T choose(){
        Random random = ThreadLocalRandom.current();
        return choiceList[random.nextInt(choiceList.size())];
    }
}

생성자를 수정해야 한다. 만약, Chooser<Number> 의 생성자에 List<Integer> 를 매개변수로 넘기고 싶다고 했을 때, 컴파일 조차  되지 않을 것이다. 매개변수화 타입은 불공변이기 때문이다. 한정적 와일드카드 타입을 사용하자. 여기서는 생성자로 넘겨지는 choices 컬렉션이 T타입의 값을 생산하니, 아래와 같이 상한 와일드카드 타입을 사용하자.

public Chooser(Collection<? extends T> choices)

2-2. [아이템 30] union 메서드

아이템 30에서 살펴봤던 아래 두 집합의 합집합을 구하는 union 메서드를 와일드카드 타입을 적용해 수정해보자.

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> result = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}

union 메서드를 통해 두 매개변수 s1, s2 모두 E 의 생산자이니, 아래와 같이 상한 와일드카드 타입을 사용하자.

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

수정한 후, 아래 코드도 Integer 와 Double 은 Number 의 자식클래스이므로 말끔히 컴파일 될 것이다.

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 이전에서는 오류가 나타난다. 자바 7 이전에는 아래와 같이 문맥에 맞는 반환 타입을 정확히 명시해주어야 한다.

Set<Number> numbers = Union.<Number>union(integers, doubles);

 

3. 타입 매개변수 vs 와일드카드

메서드를 정의할 때, 둘 중 어느것을 사용해도 괜찮을 때가 있다. 예를 들어, 주어진 리스트에서 명시한 두 인덱스의 아이템을 교환하는 정적 메서드로 두 상황을 살펴보자.

// 타입 매개변수
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<?> 인데, List<?> 에는 null 외에 아무것도 넣을 수 없다는 것에 있다. 이를 위해, 와일드 카드 타입의 실제 타입을 알려주는 private 도우미 메서드를 따로 작성해야 한다.

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

클라이언트는 와일드카드 기반의 메서드를 사용할 수 있지만, 사실 내부 구현은 더 복잡한 swapHelper 라는 제네릭 메서드를 이용해 실제 타입을 알아내어 매개변수 list 의 swap 행위를 할 수 있게 만들었다.

'1️⃣ 백앤드 > 이펙티브 자바' 카테고리의 다른 글

[아이템33] 타입 안전 이종 컨테이너를 고려하라  (0) 2024.10.31
[아이템32] 제네릭과 가변인수를 함께 쓸 때는 신중하라  (0) 2024.10.30
[아이템30] 이왕이면 제네릭 메서드로 만들라  (1) 2024.10.24
[아이템29] 이왕이면 제네릭 타입으로 만들라  (0) 2024.10.24
[아이템28] 배열보다는 리스트를 사용하라  (1) 2024.10.23
'1️⃣ 백앤드/이펙티브 자바' 카테고리의 다른 글
  • [아이템33] 타입 안전 이종 컨테이너를 고려하라
  • [아이템32] 제네릭과 가변인수를 함께 쓸 때는 신중하라
  • [아이템30] 이왕이면 제네릭 메서드로 만들라
  • [아이템29] 이왕이면 제네릭 타입으로 만들라
HOZINU
HOZINU
주니어 백앤드 개발자의 세상만사 이모저모. 주로 개발 이야기를 다룸.
  • HOZINU
    백엔드 탐험 일지
    HOZINU
  • 전체
    오늘
    어제
  • 블로그 메뉴

    • ⛪ HOME
    • 🌍 GITHUB
    • 카테고리 (73)
      • 1️⃣ 백앤드 (72)
        • 이펙티브 자바 (72)
      • 2️⃣ CS (0)
        • 운영체제 (0)
        • 네트워크 기초 (0)
        • 네트워크 응용 (0)
        • SSL & PKI (0)
        • 기타 (0)
      • 3️⃣ 코딩테스트 (0)
      • 4️⃣ 개인공부 (0)
        • MSA (0)
        • REDIS (0)
      • 5️⃣ 일상이야기 (1)
  • 인기 글

  • 태그

    표준예외
    Comparable
    optional
    계층구조
    빌더
    finalizer
    정보은닉
    의존객체
    컴포지션
    CLONE
    캡슐화
    맥북
    정적 팩터리 메서드
    hashcode
    멤버클래스
    로타입
    equals
    Cleaner
    try-with-resources
    싱글턴
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
HOZINU
[아이템31] 한정적 와일드카드를 사용해 API 유연성을 높이라
상단으로

티스토리툴바