JAVA/Effective Java

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

꾸준함. 2024. 2. 21. 01:20

제네릭 매개변수화 타입은 불공변

앞서 아이템 28에서 언급했다시피 매개변수화 타입은 불공변(invariant)입니다.

매개변수화 타입은 오로지 한 타입을 지칭하며 해당 타입의 상속구조를 따지지 않습니다.

  • ex) String은 Object의 하위 타입이지만 List<String>은 List<Object>의 하위 타입이 아님
  • List<Object>에는 어떤 객체든 넣을 수 있지만 List<String>에는 문자열만 넣을 수 있으므로 List<String>은 List<Object>가 하는 일을 제대로 수행하지 못하니 SOLID 원칙의 리스코프 치환 원칙에 어긋나 하위 타입이 될 수 없음

 

PECS(Producer-Extends, Consumer-Super) 

하지만 때로는 불공변 방식보다 유연한 방식이 필요한데 아이템 29에서 다루었던 스택 클래스를 통해 이를 설명하겠습니다.


 

위 코드에 일련의 원소를 스택에 넣는 메서드를 추가해야할 때 보통 아래와 같이 코드를 작성했을 것입니다.


 

위 코드는 Iterable src의 원소 타입이 스택의 원소 타입과 일치하면 잘 작동하지만 하위 타입일 경우 잘 작동해야할 것 같지만 불공변 특성 때문에 컴파일 오류가 발생합니다.

 

 

자바는 이런 상황에 대처하기 위해 한정적 와일드카드 타입이라는 특별한 매개변수화 타입을 지원했습니다.

pushAll의 입력 매개변수 타입은 'E의 Iterable'이 아닌 'E의 하위 타입의 Iterable'이어야 하며, 와일드카드 타입 Iterable<? extends E>가 정확히 이런 뜻입니다.

이처럼 받아서 넣는 것을 생산자(Producer)라고 지칭하고 extends 키워드를 활용하여 상위 타입 한정을 수행합니다.

아래와 같이 pushAll 메서드를 수정하면 컴파일이 정상적으로 됩니다.


 

이제 pushAll과 짝을 이루는 popAll 메서드를 작성할 차례입니다.

popAll 메서드는 Stack 내 모든 원소를 주어진 컬렉션으로 작성하며 한정적 와일드카드 타입을 사용하지 않을 경우 아래와 같이 구현될 것입니다.


 

위 코드도 주어진 컬렉션의 원소 타입이 스택의 원소 타입과 일치한다면 정상적으로 컴파일되고 동작합니다.

하지만 Number가 Object의 하위 타입이기 때문에 Stack<Number>의 원소를 Object용 컬렉션으로 옮기려고 할 때 제네릭 매개변수화 타입의 불공변 특성 때문에 컴파일 오류가 발생합니다.

 

 

이번에도 해결책은 한정적 와일드카드 타입이며 popAll의 입력 매개변수 타입을 ? super E로 선언하면 됩니다.

이는 popAll의 입력 매개변수의 타입을 E의 컬렉션이 아니라 E의 상위 타입의 Collection으로 선언하는 것을 의미합니다.

이처럼 꺼내는 것을 소비자(Consumer)라고 지칭하고 super 키워드를 활용하여 하위 타입 한정을 수행할 수 있습니다.

아래와 같이 popAll 메서드를 수정하면 컴파일이 정상적으로 됩니다.


 

위 내용을 전부 적용한 최종 Stack 코드는 아래와 같습니다.


 

정리하자면 유연성을 극대화하기 위해 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용을 권장합니다.

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

 

* 주의: 입력 매개변수가 생산자와 소비자 역할을 동시에 할 경우 타입을 정확히 지정해야 하는 상황이므로 와일드카드 타입을 써도 좋을 것이 없음

 

PECS 공식을 두 번 적용한 예

 

Comparable을 직접 구현하지 않았지만 직접 구현한 다른 타입을 확장한 타입을 지원하려면 와일드카드가 필요하며 PECS 공식을 두 번 적용해야 합니다.

참고로 Comparator와 Comparable은 무조건 꺼내서 비교하는 역할이기 때문에 소비자(consumer)입니다.

List를 매개변수로 받아 최댓값을 구하는 메서드로 예를 들자면 아래와 같습니다.


 

메서드 설명

  • 입력 매개변수에서는 E 인스턴스를 생산하므로 원래의 List<E>를 List<? extends E>로 수정
  • 타입 매개변수 E는 기존에는 Comparable<E>를 확장한다고 정의했는데, 이때 Comparable<E>는 E 인스턴스를 소비
    • 따라서 매개변수화 타입 Comparable<E>를 한정적 와일드카드 타입인 Comparable<? super E>로 대체
    • 앞서 설명했다시피 Comparator와 Comparable은 항상 소비자 역할이기 때문에 Comparable<E>보다는 Comparable<? super E>를 사용하는 편을 권장

 

이처럼 PECS 공식을 두번 적용하면 아래의 구조도 커버가 가능합니다.

IntegerBox는 직접 Comparable을 구현하지 않았지만 Comparable을 구현한 Box를 상속했기 때문에 max 메서드를 적용 가능합니다.

 

 

 

와일드카드 활용 팁

메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하는 것을 권장합니다.

  • 한정적 타입이라면 한정적 와일드 카드로 대체
  • 비한정적 타입이라면 비한정적 와일드 카드로 대체

 

저자는 비한정적 타입 매개변수라면 비한정적 와일드카드로 바꾸고 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드를 작성하여 활용하라고 하지만 개인적으로 굳이 helper성 메서드를 쓰면서 단독으로 ?를 써야 하는지는 의문입니다.

물론 비한정적 와일드카드인 ?를 쓰면 표현이 간단해진다는 장점이 있지만 ? 타입을 알 수 없기 때문에 null 밖에 넣을 수 없는 단점이 있습니다.

따라서 비한정적 와일드카드는 조회만 하는 메서드에만 사용하는 것을 권장합니다.


 

위 예제에서 볼 수 있다시피 굳이 helper성 메서드를 쓰면서 단독으로 ?를 쓰는 메리트가 없습니다.

따라서 단순 조회성 메서드가 아닌 이상 ? 보다는 그냥 매개변수화 타입을 부여하는 것을 권장합니다.


 

정리

와일드카드 타입을 사용하면 API가 더 유연해지므로 PECS 원칙을 숙지하는 것이 권장됩니다.

  • 생산자(producer)는 extends를 소비자(consumer)는 super를 사용

 

참고

이펙티브 자바
이펙티브 자바 완벽 공략 2부 - 백기선 강사님

반응형