이펙티브 자바 완벽 공략 2부
아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라
아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라
- 아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라
- 아이템 31. 핵심 정리 - Chooser와 Union API 개선
- 아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라
- 먼저 pushAll을 만들어보자
- 제네릭이 불공변이라는 말의 의미
- 지금 pushAll은 지나치게 엄격하다
- ? extends E
- 이제 Integer도 Double도 넣을 수 있다
- 여기서 Producer라는 표현이 등장한다
- 반대 상황도 생각해보자
- 이번에는 ? super E를 사용한다
- Number를 Object에 넣는 것은 안전하다
- 이번에는 Consumer다
- PECS
- PECS를 외우기 전에 데이터의 방향부터 보자
- Producer에서 왜 extends를 사용하는가
- 대신 ? extends에는 값을 마음대로 넣을 수 없다
- Consumer에서는 왜 super를 사용하는가
- 대신 ? super에서 꺼낼 때는 타입 정보가 약해진다
- extends와 super를 비교하면
- pushAll을 다시 보면
- popAll도 다시 보면
- 한정적 타입 매개변수와 한정적 와일드카드는 다르다
- E와 ?의 차이를 쉽게 생각해보자
- API가 값을 읽기만 한다면 extends를 먼저 떠올려보자
- API가 값을 받아들이기만 한다면 super를 생각해보자
- 실제 API 설계에서 중요한 이유
- PECS는 어디에나 무조건 적용하는 규칙은 아니다
- Producer와 Consumer는 메서드를 기준으로 판단한다
- PECS를 가장 쉽게 판단하는 방법
- Stack 예제를 한 번에 정리하면
- 실무에서의 활용
- 아이템 28부터 이어지는 흐름
- 핵심 정리
- 한 줄 요약
- 아이템 31. 핵심 정리 - Comparator와 Comparable은 소비자
- 아이템 31. PECS를 실제 API에 적용하는 방법
- 첫 번째 예제: Chooser
- Chooser
에 List 를 전달하고 싶다 - 하지만 Collection
는 너무 엄격하다 - Chooser의 Collection은 Producer다
- Collection<? extends T>
- 왜 이것이 안전할까?
- Chooser에서 기억할 핵심
- 두 번째 예제: union()
- Integer와 Double을 Number로 합치고 싶다
- 기존 union은 같은 E를 요구한다
- 두 Set 모두 Producer다
- 유연한 union()
- Number 기준으로 보면
- extends가 타입을 섞어도 된다는 뜻은 아니다
- union()의 PECS 판단
- 세 번째 예제: max()
- max()에도 두 군데의 유연성 문제가 있다
- Collection은 Producer다
- 첫 번째 개선
- Comparable은 왜 Consumer일까?
- 최종적인 형태
- Collection<? extends E>부터 해석하자
- Comparable<? super E>를 해석해보자
- Box 예제로 이해해보자
- IntegerBox는 Box를 상속한다
- IntegerBox끼리 비교할 수 없는 걸까?
- 그런데 Comparable
만 요구하면 너무 엄격하다 - Comparable<? super E>라면 해결된다
- 데이터 흐름으로 보면 더 쉽다
- max()의 두 PECS를 한 번에 보면
- 재귀적 타입 한정과 PECS가 함께 사용된 것이다
- 중요한 것은 타입 안전성을 포기하지 않았다는 점이다
- 세 가지 예제에서 같은 패턴을 찾아보자
- PECS에서 Producer의 의미를 다시 생각해보자
- Consumer 역시 객체를 삭제한다는 뜻이 아니다
- extends와 super가 헷갈리면 메서드 안을 보자
- 단순한 암기보다 이 기준이 유용하다
- 한정적 와일드카드가 없더라도 코드는 틀리지 않을 수 있다
- API 설계자의 관점에서 생각해보자
- PECS는 라이브러리를 사용하는 사람을 편하게 만든다
- 세 예제를 최종적으로 비교해보자
- PECS를 코드 리뷰에서 활용하는 방법
- 핵심 정리
- 한 줄 요약
- 아이템 31. 핵심 정리 - Chooser와 Union API 개선
아이템 31. 핵심 정리 - Chooser와 Union API 개선
아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라
제네릭을 처음 배우면 보통 다음과 같은 코드부터 익숙해진다.
List<String>
Set<Integer>
Stack<Number>
그리고 직접 제네릭 타입을 만들 때도 보통 T, E 같은 타입 매개변수를 사용한다.
public class Stack<E> {
public void push(E element) {
// ...
}
public E pop() {
// ...
}
}
여기까지만 보면 꽤 자연스럽다.
Stack<String>이면 문자열을 넣고 꺼내고, Stack<Integer>면 정수를 넣고 꺼낸다.
문제는 상속 관계가 등장하면서 시작된다.
Integer는 Number의 하위 타입이다.
Object
↑
Number
↑
Integer
그렇다면 이런 생각이 들 수 있다.
Stack<Number>에 Integer 여러 개를 넣는 것은 당연히 가능해야 하지 않을까?
하나씩 넣는 것은 실제로 아무 문제가 없다.
Stack<Number> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
Integer는 Number이기 때문이다.
그런데 Integer가 여러 개 들어 있는 Iterable<Integer>를 한꺼번에 넣으려고 하면 조금 의외의 문제가 발생한다.
먼저 pushAll을 만들어보자
Stack에 여러 원소를 한 번에 넣을 수 있도록 다음 메서드를 추가했다고 해보자.
public void pushAll(Iterable<E> src) {
for (E element : src) {
push(element);
}
}
언뜻 보면 아무 문제 없어 보인다.
그리고 Stack<Number>를 만든다.
Stack<Number> stack = new Stack<>();
여기에 List<Number>를 전달하는 것은 가능하다.
List<Number> numbers =
List.of(1, 2.0, 3L);
stack.pushAll(numbers);
그런데 다음 코드는 문제가 된다.
List<Integer> integers =
List.of(1, 2, 3);
stack.pushAll(integers);
우리가 기대하기에는 가능할 것 같다.
Integer는 분명 Number이기 때문이다.
하지만 현재 pushAll()의 매개변수 타입을 보면 이유를 알 수 있다.
Iterable<E>
현재 Stack은
Stack<Number>
이므로 E는 Number다.
결국 메서드의 타입은 다음과 같다.
pushAll(Iterable<Number> src)
그런데 우리가 전달하려는 것은
Iterable<Integer>
다.
그리고 Java의 제네릭은 기본적으로 불공변이다.
즉,
Integer는 Number의 하위 타입
이라고 해서
Iterable<Integer>가 Iterable<Number>의 하위 타입
이 되는 것은 아니다.
이 둘은 서로 다른 매개변수화 타입이다.
제네릭이 불공변이라는 말의 의미
배열과 비교하면 조금 더 이해하기 쉽다.
배열은 공변이다.
Integer[] integers = {1, 2, 3};
Number[] numbers = integers;
Integer[]를 Number[]로 사용할 수 있다.
하지만 제네릭에서는 다음이 불가능하다.
List<Integer> integers =
List.of(1, 2, 3);
List<Number> numbers =
integers;
컴파일되지 않는다.
여기서 한 가지는 정확하게 구분할 필요가 있다.
List<Number>에 Integer 객체를 넣을 수 없다는 의미는 아니다.
다음은 가능하다.
List<Number> numbers =
new ArrayList<>();
numbers.add(1);
numbers.add(3.14);
numbers.add(10L);
Integer, Double, Long은 모두 Number이기 때문이다.
문제는 List<Integer>라는 컨테이너 자체를 List<Number>로 취급할 수 없다는 것이다.
이 차이를 이해하는 것이 와일드카드를 이해하는 출발점이다.
지금 pushAll은 지나치게 엄격하다
다시 pushAll()을 보자.
public void pushAll(Iterable<E> src) {
for (E element : src) {
push(element);
}
}
Stack<Number>라면 이 메서드는 Iterable<Number>만 받을 수 있다.
그런데 실제 pushAll()이 하는 일을 보면 조금 아쉽다.
for (E element : src) {
push(element);
}
src에서 값을 하나씩 꺼내 Stack 안으로 집어넣을 뿐이다.
Stack은 Number를 저장할 수 있다.
그렇다면 Integer를 받아도 괜찮다.
Double을 받아도 괜찮다.
둘 다 Number니까.
List<Integer> integers =
List.of(1, 2, 3);
List<Double> doubles =
List.of(1.1, 2.2, 3.3);
두 컬렉션 모두 Stack<Number>에 넣어도 타입적으로 위험하지 않다.
하지만 Iterable<E>라는 선언이 이 사용을 막고 있다.
이럴 때 한정적 와일드카드(Bounded Wildcard) 를 사용한다.
? extends E
pushAll()을 다음처럼 변경해보자.
public void pushAll(
Iterable<? extends E> src
) {
for (E element : src) {
push(element);
}
}
처음 보는 문법은 이것이다.
? extends E
?는 와일드카드다.
정확한 하나의 타입을 이름 붙여 선언하는 대신
어떤 타입인지 정확히는 모르겠지만 E 또는 E의 하위 타입이다.
라는 의미를 표현한다.
Stack<Number>라면 E는 Number이므로 다음처럼 이해할 수 있다.
? extends Number
Number 또는
Number의 어떤 하위 타입
따라서 다음 타입들이 모두 가능해진다.
Iterable<Number>
Iterable<Integer>
Iterable<Double>
Iterable<Long>
...
API가 훨씬 유연해진다.
이제 Integer도 Double도 넣을 수 있다
다음 Stack이 있다고 해보자.
Stack<Number> stack =
new Stack<>();
Integer 목록을 만든다.
List<Integer> integers =
List.of(1, 2, 3);
이제 가능하다.
stack.pushAll(integers);
Double도 마찬가지다.
List<Double> doubles =
List.of(1.1, 2.2, 3.3);
stack.pushAll(doubles);
왜 안전할까?
Stack이 내부에서 원소를 사용하는 타입은 Number이기 때문이다.
Integer를 꺼내 Number로 사용하는 것은 안전하다.
Integer integer = 10;
Number number = integer;
Double도 마찬가지다.
Double value = 3.14;
Number number = value;
그래서 Number의 하위 타입에서 값을 가져오는 것은 문제가 없다.
여기서 Producer라는 표현이 등장한다
Effective Java에서는 이런 매개변수를 Producer라고 표현한다.
처음 들으면 조금 헷갈릴 수 있다.
현재 코드를 기준으로 보면 src가 Stack에게 원소를 공급하고 있다.
public void pushAll(
Iterable<? extends E> src
) {
for (E element : src) {
push(element);
}
}
데이터 흐름을 보면 이렇다.
Iterable<Integer>
│
│ 원소를 제공
▼
pushAll()
│
▼
Stack<Number>
src 입장에서는 원소를 생산해서 제공하는 쪽이다.
그래서 Producer라고 부른다.
Producer에서는 다음 형태를 사용한다.
? extends E
여기서 유명한 규칙 하나가 나온다.
Producer Extends
앞글자를 따면
PE
다.
반대 상황도 생각해보자
이번에는 Stack 안에 있는 원소를 다른 Collection으로 모두 꺼내는 기능을 만들어보자.
public void popAll(Collection<E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
이번에는 Stack이 가지고 있던 원소를 dst에 넣는다.
다음 Stack이 있다고 하자.
Stack<Number> stack =
new Stack<>();
그리고 Number를 받을 Collection도 있다.
Collection<Number> numbers =
new ArrayList<>();
stack.popAll(numbers);
이것은 당연히 가능하다.
그런데 생각해보면 다음도 가능해야 할 것 같다.
Collection<Object> objects =
new ArrayList<>();
stack.popAll(objects);
Stack<Number>에서 나오는 값은 전부 Number다.
그리고 Object 컬렉션에는 Number를 얼마든지 넣을 수 있다.
Object value = Integer.valueOf(10);
전혀 위험하지 않다.
하지만 현재 메서드는
Collection<E>
만 받는다.
E가 Number이므로
Collection<Number>
만 요구한다.
Collection<Object>는 다른 매개변수화 타입이기 때문에 사용할 수 없다.
다시 API가 필요 이상으로 엄격해진 것이다.
이번에는 ? super E를 사용한다
이 문제는 다음과 같이 해결할 수 있다.
public void popAll(
Collection<? super E> dst
) {
while (!isEmpty()) {
dst.add(pop());
}
}
이번에는
? super E
라는 문법이 등장했다.
의미는 다음과 같다.
정확한 타입은 모르지만 E 또는 E의 상위 타입을 사용하는 Collection이다.
E가 Number라면 다음과 같은 타입을 받을 수 있다.
Collection<Number>
Collection<Object>
Number에서 꺼낸 값을 두 곳 모두 안전하게 넣을 수 있기 때문이다.
Number를 Object에 넣는 것은 안전하다
코드를 아주 단순하게 보면 다음과 같다.
Number number = 10;
Object object = number;
아무 문제가 없다.
따라서
Collection<Object> objects =
new ArrayList<>();
objects.add(number);
역시 안전하다.
그래서 다음 호출도 허용할 수 있다.
Stack<Number> stack =
new Stack<>();
Collection<Object> objects =
new ArrayList<>();
stack.popAll(objects);
이번에는 Consumer다
popAll()에서 dst는 Stack이 꺼낸 값을 받아들인다.
Stack<Number>
│
│ Number를 꺼냄
▼
popAll()
│
▼
Collection<Object>
dst는 원소를 받아 소비하는 역할을 한다.
그래서 Consumer라고 부른다.
Consumer에서는
? super E
를 사용한다.
즉,
Consumer Super
다.
앞글자를 따면
CS
가 된다.
앞에서 Producer Extends가 PE였으므로 두 개를 합치면 유명한 규칙이 완성된다.
PECS
Producer Extends
Consumer Super
즉,
PECS
라고 기억한다.
PECS를 외우기 전에 데이터의 방향부터 보자
PECS를 처음 접하면 단순 암기처럼 느껴질 수 있다.
하지만 실제 코드를 볼 때는 먼저 데이터가 어느 방향으로 움직이는지 생각하면 훨씬 쉽다.
pushAll()을 보자.
public void pushAll(
Iterable<? extends E> src
)
데이터는 src에서 나온다.
src
↓ 데이터가 나옴
Stack
src가 Producer다.
따라서
Producer
→ extends
다.
반대로 popAll()에서는
public void popAll(
Collection<? super E> dst
)
데이터가 dst로 들어간다.
Stack
↓ 데이터가 들어감
dst
dst가 Consumer다.
따라서
Consumer
→ super
다.
이렇게 데이터 흐름을 먼저 보면 extends, super를 외우는 부담이 줄어든다.
Producer에서 왜 extends를 사용하는가
다음 매개변수를 보자.
Iterable<? extends Number>
우리는 이 Iterable의 정확한 타입을 모른다.
다음 중 무엇일 수도 있다.
Iterable<Number>
Iterable<Integer>
Iterable<Double>
Iterable<Long>
정확한 타입은 모르지만 한 가지는 확실하다.
여기에서 값을 꺼내면 최소한 Number로는 사용할 수 있다.
Number number = ...;
그래서 Producer에서는 값을 안전하게 읽어올 수 있다.
대신 ? extends에는 값을 마음대로 넣을 수 없다
이 부분은 PECS를 제대로 이해하려면 중요하다.
다음 코드가 있다고 하자.
List<? extends Number> numbers;
실제 객체가 무엇인지 알 수 없다.
어쩌면
List<Integer>
일 수도 있고,
List<Double>
일 수도 있다.
그런데 다음처럼 해버리면 어떨까?
numbers.add(10);
만약 실제 객체가 List<Double>이었다면 Integer를 넣는 셈이 된다.
안전하지 않다.
그래서 ? extends Number에서는 구체적인 값을 추가하는 것이 일반적으로 허용되지 않는다.
반면 값을 읽으면
Number number =
numbers.get(0);
최소한 Number라는 것은 확실하다.
그래서 간단하게 다음처럼 기억할 수 있다.
? extends T
→ T로 읽기 좋음
→ 구체적인 T를 쓰기는 어려움
Consumer에서는 왜 super를 사용하는가
이번에는 다음 타입을 보자.
List<? super Integer> values;
이 리스트의 실제 타입은 다음 중 하나일 수 있다.
List<Integer>
List<Number>
List<Object>
여기에 Integer를 넣는 것은 모두 안전하다.
values.add(10);
왜냐하면
List<Integer>에는 당연히 Integer를 넣을 수 있고,
List<Number>에도 Integer를 넣을 수 있고,
List<Object>에도 Integer를 넣을 수 있기 때문이다.
그래서 ? super Integer는 값을 넣는 용도에 잘 맞는다.
대신 ? super에서 꺼낼 때는 타입 정보가 약해진다
다음 코드가 있다고 하자.
List<? super Integer> values;
실제 타입이
List<Object>
일 수도 있다.
그러므로 값을 꺼냈을 때 이를 무조건 Integer라고 할 수 없다.
안전하게 보장할 수 있는 타입은 결국 Object다.
Object value =
values.get(0);
그래서 다음처럼 생각하면 이해하기 쉽다.
? super T
→ T를 쓰기 좋음
→ 읽어낼 때 구체적인 타입 정보는 약함
extends와 super를 비교하면
| 구분 | ? extends E | ? super E |
|---|---|---|
| 의미 | E 또는 E의 하위 타입 | E 또는 E의 상위 타입 |
| 대표 역할 | Producer | Consumer |
| E 기준으로 읽기 | 편함 | 구체 타입 보장 어려움 |
| E 값을 넣기 | 일반적으로 어려움 | 가능 |
| 기억법 | Producer Extends | Consumer Super |
이 표를 외우는 것도 좋지만 실제로는 메서드 안에서 컬렉션을 어떻게 사용하는지를 먼저 보는 것이 더 중요하다.
pushAll을 다시 보면
처음에는 다음과 같았다.
public void pushAll(
Iterable<E> src
)
이 API는 Stack<Number>에서 Iterable<Number>만 허용했다.
하지만 실제 메서드는 데이터를 읽기만 한다.
for (E element : src) {
push(element);
}
따라서
public void pushAll(
Iterable<? extends E> src
)
로 바꾸는 것이 더 유연하다.
이제 다음이 모두 가능하다.
Stack<Number> stack =
new Stack<>();
stack.pushAll(
List.of(1, 2, 3)
);
stack.pushAll(
List.of(1.1, 2.2, 3.3)
);
Stack 입장에서는 모두 Number로 다룰 수 있다.
popAll도 다시 보면
처음에는 다음과 같았다.
public void popAll(
Collection<E> dst
)
Stack<Number>라면 Collection<Number>만 받을 수 있었다.
하지만 실제 메서드는 컬렉션에 값을 추가한다.
dst.add(pop());
따라서
public void popAll(
Collection<? super E> dst
)
로 변경할 수 있다.
이제 다음이 모두 가능하다.
Collection<Number> numbers =
new ArrayList<>();
stack.popAll(numbers);
그리고
Collection<Object> objects =
new ArrayList<>();
stack.popAll(objects);
API가 처리할 수 있는 타입의 범위가 넓어졌다.
이것이 이번 아이템에서 말하는 유연성이다.
한정적 타입 매개변수와 한정적 와일드카드는 다르다
앞선 아이템에서는 다음 문법을 배웠다.
<E extends Number>
이것은 한정적 타입 매개변수다.
E라는 타입 변수 자체의 범위를 제한한다.
E는 Number 또는 Number의 하위 타입
이번에는 다음 문법을 사용한다.
? extends Number
이것은 한정적 와일드카드다.
정확한 타입을 이름 붙여서 계속 사용할 필요는 없고
Number의 어떤 하위 타입이다.
라는 사실만 필요할 때 사용할 수 있다.
둘은 비슷해 보이지만 목적이 다르다.
E와 ?의 차이를 쉽게 생각해보자
E에는 이름이 있다.
<E>
그래서 여러 위치에서 같은 타입 관계를 표현할 수 있다.
public static <E> E first(
List<E> values
)
입력 리스트의 E와 반환 E가 같은 타입이라는 관계를 나타낸다.
반면 ?는
?
정확한 타입 이름 자체에는 관심이 없다는 의미에 가깝다.
List<? extends Number>
에서 우리가 알고 싶은 것은
정확히 Integer인가?
Double인가?
Long인가?
가 아니다.
단지
어쨌든 Number 계열이다.
라는 사실이면 충분하다.
API가 값을 읽기만 한다면 extends를 먼저 떠올려보자
예를 들어 이런 메서드를 만들고 있다고 하자.
public void addAll(
Collection<T> source
)
메서드 내부에서 source의 값을 꺼내기만 한다.
for (T value : source) {
...
}
그러면 다음처럼 더 유연하게 만들 수 있는지 생각해볼 수 있다.
Collection<? extends T>
즉
이 매개변수가 내게 T를 공급하는가?
라는 질문을 해보는 것이다.
그렇다면 Producer일 가능성이 높다.
API가 값을 받아들이기만 한다면 super를 생각해보자
반대로 다음과 같은 코드가 있다.
destination.add(value);
destination이 메서드가 만들어낸 값을 받아들이는 역할만 한다면 Consumer다.
이때
Collection<? super T>
를 고려할 수 있다.
즉 질문을 바꿔보면 된다.
이 컬렉션에서 내가 값을 꺼내려고 하는가, 아니면 값을 집어넣으려고 하는가?
이 질문 하나가 PECS를 판단하는 데 상당히 도움이 된다.
실제 API 설계에서 중요한 이유
와일드카드를 사용하지 않아도 많은 제네릭 코드는 정상적으로 동작한다.
그래서 처음 제네릭을 배우면 굳이 왜 ? extends, ? super까지 필요한지 의문이 들 수 있다.
차이는 API를 실제로 사용하기 시작하면 드러난다.
다음 API는 틀린 것은 아니다.
void pushAll(
Iterable<Number> values
)
하지만 List<Integer>를 거부한다.
다음은 같은 작업을 하면서 훨씬 많은 정상적인 입력을 받을 수 있다.
void pushAll(
Iterable<? extends Number> values
)
즉 한정적 와일드카드는 타입 안전성을 포기해서 유연성을 얻는 것이 아니다.
오히려
타입 안전성은 그대로 유지하면서 실제로 안전한 상속 관계까지 API가 받아들일 수 있게 만드는 것
에 가깝다.
PECS는 어디에나 무조건 적용하는 규칙은 아니다
PECS를 배운 뒤 모든 제네릭 타입에 extends나 super를 붙이는 것도 좋지 않다.
예를 들어 하나의 컬렉션에서 값을 읽기도 하고 쓰기도 해야 할 수도 있다.
또는 정확히 같은 타입의 관계를 유지해야 하는 메서드도 있다.
그런 경우 단순히
Collection<T>
가 더 적절할 수 있다.
PECS는 특히 메서드 매개변수가 값을 생산하거나 소비하는 역할로 명확하게 나뉘는 경우 유용하다.
Producer와 Consumer는 메서드를 기준으로 판단한다
여기서 또 하나 헷갈리기 쉬운 부분이 있다.
List 자체가 Producer이거나 Consumer인 것은 아니다.
같은 List라도 어떤 메서드에서 어떻게 사용하느냐에 따라 역할이 달라진다.
예를 들어
void print(
List<? extends Number> values
)
에서는 values가 값을 공급한다.
Producer다.
반면
void copyTo(
List<? super Integer> destination
)
에서는 destination에 값을 넣는다.
Consumer다.
즉 Producer와 Consumer는 자료구조의 고정된 성격이 아니라 현재 API에서 데이터가 이동하는 방향을 기준으로 판단하는 역할이다.
PECS를 가장 쉽게 판단하는 방법
메서드 매개변수를 하나 발견했다면 다음 순서로 생각해보자.
첫 번째 질문은 이것이다.
이 매개변수에서 T 값을 꺼내서 사용하는가?
그렇다면 Producer일 가능성이 높다.
? extends T
를 검토한다.
두 번째는 반대다.
이 매개변수에 T 값을 집어넣는가?
그렇다면 Consumer일 가능성이 높다.
? super T
를 검토한다.
그래서 다음 한 문장으로 기억할 수 있다.
꺼내 쓸 곳은 extends, 집어넣을 곳은 super.
조금 더 정확한 용어가 필요하면 그때 PECS를 떠올리면 된다.
Producer Extends
Consumer Super
Stack 예제를 한 번에 정리하면
처음 Stack API는 다음과 같았다.
public void pushAll(
Iterable<E> src
)
public void popAll(
Collection<E> dst
)
타입 안전하기는 하지만 지나치게 엄격하다.
이를 다음과 같이 개선한다.
public void pushAll(
Iterable<? extends E> src
)
public void popAll(
Collection<? super E> dst
)
이제 데이터 흐름을 보면 이유가 명확하다.
Iterable<? extends E>
│
│ E를 공급
▼
Stack<E>
Producer
→ extends
반대로
Stack<E>
│
│ E를 공급
▼
Collection<? super E>
Consumer
→ super
이 두 형태가 이번 아이템에서 가장 중요한 코드다.
실무에서의 활용
Spring 기반 애플리케이션을 개발할 때 직접 Stack 같은 자료구조를 만들 일은 많지 않지만, 라이브러리성 코드나 공통 모듈, 변환기, 이벤트 처리, 배치 처리 등의 API를 만들다 보면 비슷한 상황을 자주 만난다.
예를 들어 여러 객체를 등록하는 메서드가 있다고 하자.
public void registerAll(
Collection<Event> events
)
실제로는 Event의 하위 타입도 모두 안전하게 처리할 수 있다면 다음 형태가 더 유연할 수 있다.
public void registerAll(
Collection<? extends Event> events
)
반대로 어떤 결과를 외부 컬렉션에 담아주는 API라면
Collection<? super Result>
형태를 검토할 수 있다.
중요한 것은 무조건 와일드카드를 넣는 것이 아니다.
API 구현을 보고
이 컬렉션은 나에게 데이터를 주는가?
아니면
내가 이 컬렉션에 데이터를 주는가?
를 먼저 판단하는 것이다.
그 판단이 서면 extends와 super는 생각보다 자연스럽게 따라온다.
아이템 28부터 이어지는 흐름
아이템 28에서는 제네릭이 기본적으로 불공변이라는 사실을 살펴봤다.
Integer <: Number
하지만
List<Integer> <: List<Number>
는 아님
이 불공변성은 타입 안전성을 지켜주는 중요한 특징이지만, 그대로만 사용하면 API가 필요 이상으로 엄격해질 수 있다.
아이템 31에서는 바로 그 문제를 한정적 와일드카드로 해결한다.
기본 제네릭
List<Number>
↓
정확히 Number 타입만 요구
필요하다면
Producer
List<? extends Number>
또는
Consumer
List<? super Number>
처럼 표현해 안전한 범위 안에서 상속 관계를 활용할 수 있다.
즉 와일드카드는 제네릭의 타입 안전성을 무너뜨리는 기능이 아니라 불공변 제네릭을 실제 API 설계에서 조금 더 유연하게 사용할 수 있도록 만들어주는 장치라고 보는 것이 좋다.
핵심 정리
- Java의 일반적인 제네릭 타입은 불공변이므로
Integer가Number의 하위 타입이어도List<Integer>는List<Number>의 하위 타입이 아니다. Stack<Number>에Integer객체 하나를push()하는 것은 가능하지만Iterable<Integer>를Iterable<Number>가 필요한 곳에 그대로 전달할 수는 없다.pushAll(Iterable<E>)는 타입 안전하지만Stack<Number>에서Iterable<Integer>,Iterable<Double>등을 받을 수 없어 필요 이상으로 제한적이다.Iterable<? extends E>를 사용하면 E 또는 E의 하위 타입을 원소 타입으로 가진 Iterable을 받을 수 있다.Stack<Number>의pushAll(Iterable<? extends Number>)에는Iterable<Integer>,Iterable<Double>등을 안전하게 전달할 수 있다.- 값을 메서드에 공급하는 매개변수를 Producer라고 생각할 수 있으며 Producer에는
? extends T를 사용하는 것이 대표적인 패턴이다. ? extends T는 정확한 원소 타입은 알 수 없지만 최소한 T의 하위 타입이라는 사실을 보장하므로 값을 T로 읽어오는 데 적합하다.- 반대로
? extends T에는 실제 타입이 무엇인지 알 수 없기 때문에 일반적으로 구체적인 T 값을 추가할 수 없다. popAll(Collection<E>)역시 타입 안전하지만 E보다 상위 타입의 Collection을 받을 수 없어 필요 이상으로 제한적일 수 있다.Collection<? super E>를 사용하면 E 또는 E의 상위 타입을 사용하는 Collection을 받을 수 있다.Stack<Number>에서 꺼낸 Number는Collection<Number>뿐 아니라Collection<Object>에도 안전하게 저장할 수 있다.- 값을 받아들이는 매개변수를 Consumer라고 생각할 수 있으며 Consumer에는
? super T를 사용하는 것이 대표적인 패턴이다. ? super T에는 T 객체를 안전하게 추가할 수 있지만 값을 읽어올 때는 실제 컬렉션 타입이 무엇인지 알 수 없으므로 일반적으로Object수준의 정보만 보장된다.- Producer에는
extends, Consumer에는super를 사용한다는 규칙을 앞글자로 줄여 PECS라고 부른다. - PECS는 단순 암기보다 데이터 이동 방향을 기준으로 이해하는 것이 쉽다.
- 메서드가 매개변수에서 T 값을 꺼내 사용한다면
? extends T를, 매개변수에 T 값을 넣는다면? super T를 우선 검토할 수 있다. - Producer와 Consumer는 컬렉션 자체의 고정된 특성이 아니라 해당 메서드 안에서 컬렉션이 어떤 역할을 수행하는지에 따라 결정된다.
<E extends Number>는 이름이 있는 타입 매개변수 자체의 범위를 제한하는 한정적 타입 매개변수이고,? extends Number는 정확한 타입 이름에는 관심이 없는 한정적 와일드카드다.- 한정적 와일드카드의 목적은 타입 안전성을 포기하는 것이 아니라 실제로 안전한 상속 관계까지 API가 허용하도록 만들어 불필요한 제약을 줄이는 것이다.
- 모든 제네릭 타입에 무조건 와일드카드를 사용할 필요는 없으며 입력과 출력 사이에서 정확히 같은 타입 관계를 표현해야 한다면
T,E같은 타입 매개변수가 더 적절할 수 있다. - 좋은 제네릭 API를 설계할 때는 타입 이름부터 고민하기보다 먼저 값이 어디에서 나오고 어디로 들어가는지를 살펴보는 것이 중요하다.
한 줄 요약
제네릭은 기본적으로 불공변이기 때문에 안전하지만 다소 엄격할 수 있으며, 값을 공급하는 Producer에는
? extends T, 값을 받아들이는 Consumer에는? super T를 적용하는 PECS 원칙을 활용하면 타입 안전성을 유지하면서도 하위·상위 타입을 자연스럽게 받아들이는 유연한 API를 설계할 수 있다.
아이템 31. 핵심 정리 - Comparator와 Comparable은 소비자
아이템 31. PECS를 실제 API에 적용하는 방법
앞에서는 한정적 와일드카드를 이용해 제네릭 API를 더 유연하게 만드는 방법을 살펴봤다.
핵심 규칙은 PECS였다.
Producer Extends
Consumer Super
줄여서
PECS
라고 부른다.
처음 PECS를 접하면 extends와 super를 외워야 하는 문법처럼 느껴질 수 있다.
하지만 실제 코드를 기준으로 보면 생각보다 단순하다.
어떤 매개변수에서 값을 가져오고 있다면 그 매개변수는 Producer 역할을 한다.
외부 객체
↓ 값을 꺼냄
현재 메서드
이때는
? extends T
를 고려한다.
반대로 현재 메서드가 어떤 객체에 값을 넣고 있다면 그 객체는 Consumer 역할을 한다.
현재 메서드
↓ 값을 넣음
외부 객체
이때는
? super T
를 고려한다.
이번에는 앞에서 살펴본 Stack 외에 Chooser, union(), max() 예제를 통해 PECS가 실제 API에서 어떻게 적용되는지 살펴보자.
첫 번째 예제: Chooser
앞에서 배열보다 리스트를 사용하는 예제로 Chooser를 살펴봤다.
Chooser는 여러 개의 값 가운데 하나를 무작위로 선택해서 반환하는 클래스다.
예를 들어 다음과 같이 사용할 수 있다.
List<Integer> numbers =
List.of(10, 20, 30);
Chooser<Integer> chooser =
new Chooser<>(numbers);
Integer selected =
chooser.choose();
내부 구현은 대략 다음과 같다.
public class Chooser<T> {
private final List<T> choiceList;
public Chooser(
Collection<T> choices
) {
choiceList =
new ArrayList<>(choices);
}
public T choose() {
// 무작위 원소 반환
return choiceList.get(0);
}
}
현재 생성자의 매개변수는 다음과 같다.
Collection<T> choices
이 코드 자체가 잘못된 것은 아니다.
하지만 조금 더 유연하게 만들 수 있다.
Chooser에 List를 전달하고 싶다
다음 상황을 생각해보자.
Chooser<Number>
를 만들고 싶다.
그런데 실제 초기 데이터는 Integer들이다.
List<Integer> integers =
List.of(1, 2, 3);
상식적으로 생각하면 이것은 전혀 위험하지 않다.
Integer는 Number의 하위 타입이다.
Object
↑
Number
↑
Integer
Chooser<Number> 안에 Integer 객체가 들어간다고 해서 문제가 발생하지 않는다.
나중에 choose()를 호출하더라도 결과를 Number로 사용하기 때문이다.
Number value =
chooser.choose();
Integer 객체를 Number로 사용하는 것은 안전하다.
하지만 Collection는 너무 엄격하다
현재 생성자는 다음과 같다.
public Chooser(
Collection<T> choices
)
Chooser<Number>를 만들었다면 T는 Number가 된다.
결국 생성자는 다음 형태가 된다.
Chooser(
Collection<Number> choices
)
그런데 전달하려는 객체는
List<Integer>
이다.
Java 제네릭은 불공변이므로
Integer <: Number
라고 해서
List<Integer> <: List<Number>
가 되는 것은 아니다.
따라서 API가 실제로 안전한 사용까지 막아버리는 상황이 생긴다.
Chooser의 Collection은 Producer다
여기서 choices가 어떻게 사용되는지 살펴보자.
choiceList =
new ArrayList<>(choices);
Chooser는 choices 안에 있는 원소들을 가져와 자신의 리스트에 저장한다.
데이터의 흐름을 보면 다음과 같다.
Collection<Integer>
│
│ 값을 제공
▼
Chooser<Number>
즉 choices는 Chooser에게 값을 공급한다.
Producer다.
PECS 규칙에 따르면
Producer Extends
이므로 다음처럼 변경할 수 있다.
public Chooser(
Collection<? extends T> choices
)
Collection<? extends T>
전체 코드는 다음과 같은 형태가 된다.
public class Chooser<T> {
private final List<T> choiceList;
public Chooser(
Collection<? extends T> choices
) {
choiceList =
new ArrayList<>(choices);
}
public T choose() {
return choiceList.get(0);
}
}
이제 Chooser<Number>를 만들면서 List<Integer>를 전달할 수 있다.
List<Integer> integers =
List.of(1, 2, 3);
Chooser<Number> chooser =
new Chooser<>(integers);
? extends Number에는 다음과 같은 타입들이 올 수 있다.
Collection<Number>
Collection<Integer>
Collection<Double>
Collection<Long>
즉 기존보다 훨씬 유연한 API가 된다.
왜 이것이 안전할까?
생성자가 실제로 하는 일은 choices에서 값을 꺼내는 것이다.
choices가 실제로
Collection<Integer>
라고 해도 꺼낸 Integer는 Number로 사용할 수 있다.
Integer integer = 10;
Number number = integer;
Double도 동일하다.
Double value = 3.14;
Number number = value;
따라서
Collection<? extends Number>
에서 값을 가져와
List<Number>
에 저장하는 것은 안전하다.
Chooser에서 기억할 핵심
기존 API:
Chooser(
Collection<T> choices
)
개선된 API:
Chooser(
Collection<? extends T> choices
)
이유는 간단하다.
choices에서 데이터를 가져온다.
↓
choices가 Producer다.
↓
Producer Extends
↓
Collection<? extends T>
두 번째 예제: union()
이번에는 두 Set을 합치는 union() 메서드를 살펴보자.
아이템 30에서 제네릭 메서드의 예제로 다음 메서드를 만들었다.
public static <E> Set<E> union(
Set<E> s1,
Set<E> s2
) {
Set<E> result =
new HashSet<>(s1);
result.addAll(s2);
return result;
}
Raw Type을 사용하는 것보다 훨씬 안전하다.
하지만 이 메서드 역시 조금 더 유연하게 만들 수 있다.
Integer와 Double을 Number로 합치고 싶다
다음 두 Set이 있다고 하자.
Set<Integer> integers =
Set.of(1, 2, 3);
그리고
Set<Double> doubles =
Set.of(
1.1,
2.2,
3.3
);
우리가 원하는 결과는 다음이다.
Set<Number>
왜냐하면 Integer와 Double 모두 Number이기 때문이다.
개념적으로 다음과 같다.
Set<Integer>
\
\
→ Set<Number>
/
/
Set<Double>
타입적으로 전혀 위험하지 않다.
기존 union은 같은 E를 요구한다
하지만 기존 선언은 다음과 같다.
public static <E> Set<E> union(
Set<E> s1,
Set<E> s2
)
여기 등장하는 E는 하나의 동일한 타입이다.
즉
Set<E>
Set<E>
↓
Set<E>
관계를 요구한다.
그래서 API가 실제 필요한 것보다 조금 더 엄격할 수 있다.
두 Set 모두 Producer다
union() 안에서 s1, s2는 어떻게 사용될까?
Set<E> result =
new HashSet<>(s1);
result.addAll(s2);
두 Set 모두 자신의 원소를 결과 Set에 공급한다.
데이터 흐름은 다음과 같다.
s1 ─────┐
│
▼
result
▲
│
s2 ─────┘
즉 s1, s2 모두 Producer다.
따라서 PECS를 적용하면
Set<? extends E>
를 사용할 수 있다.
유연한 union()
다음처럼 변경한다.
public static <E> Set<E> union(
Set<? extends E> s1,
Set<? extends E> s2
) {
Set<E> result =
new HashSet<>(s1);
result.addAll(s2);
return result;
}
이제 의미는 다음과 같다.
첫 번째 Set
E 또는 E의 하위 타입
두 번째 Set
E 또는 E의 하위 타입
결과
Set<E>
Number 기준으로 보면
E가 Number라고 생각해보자.
첫 번째 매개변수는
Set<? extends Number>
두 번째도
Set<? extends Number>
다.
따라서 다음과 같은 타입을 받을 수 있다.
Set<Integer>
Set<Double>
Set<Long>
Set<Number>
결과는 Set<Number>로 사용할 수 있다.
개념적으로 다음 흐름이다.
Integer ─┐
│
▼
Number
▲
│
Double ──┘
구체적인 객체들을 더 추상적인 Number로 사용하는 것이므로 안전하다.
extends가 타입을 섞어도 된다는 뜻은 아니다
여기서 중요한 것은 아무 타입이나 합칠 수 있다는 의미가 아니라는 점이다.
공통적으로 E로 안전하게 사용할 수 있는 타입 관계가 있어야 한다.
예를 들어
Integer
Double
은 둘 다 Number라는 공통 타입으로 사용할 수 있다.
그래서
E = Number
라는 관계를 만들 수 있다.
한정적 와일드카드는 타입 안전성을 없애는 기능이 아니다.
안전한 상속 관계를 API에 표현하는 기능이다.
union()의 PECS 판단
코드를 다시 보면
public static <E> Set<E> union(
Set<? extends E> s1,
Set<? extends E> s2
)
s1과 s2에서는 원소를 가져온다.
s1 → result
s2 → result
그래서
Producer
↓
extends
다.
이제 Chooser와 같은 패턴이 보인다.
Chooser 생성자의 Collection
→ 값 공급
→ Producer
→ ? extends T
union의 Set
→ 값 공급
→ Producer
→ ? extends E
세 번째 예제: max()
이번에는 조금 더 복잡한 예제를 살펴보자.
아이템 30에서는 컬렉션에서 가장 큰 값을 찾는 제네릭 메서드를 만들었다.
기본적인 형태는 다음과 같았다.
public static
<E extends Comparable<E>>
E max(
Collection<E> collection
) {
// ...
}
여기서
<E extends Comparable<E>>
를 재귀적 타입 한정이라고 했다.
의미는 다음과 같다.
E는 자기 자신의 타입과 비교할 수 있는 타입이어야 한다.
예를 들어 String은
String
implements
Comparable<String>
이므로 이 조건을 만족한다.
max()에도 두 군데의 유연성 문제가 있다
기존 선언은 다음과 같다.
<E extends Comparable<E>>
E max(
Collection<E> collection
)
조금 더 유연하게 만들기 위해 두 곳을 살펴볼 수 있다.
첫 번째는
Collection<E>
이다.
두 번째는
Comparable<E>
이다.
각각 Producer인지 Consumer인지 판단해보자.
Collection은 Producer다
max() 안에서는 Collection의 값을 가져온다.
개념적으로 다음과 같은 코드가 들어간다.
for (E element : collection) {
// element를 비교
}
즉 데이터 흐름은 다음과 같다.
Collection
↓ 원소 제공
max()
Collection이 Producer다.
따라서
Collection<? extends E>
형태를 고려할 수 있다.
첫 번째 개선
기존:
Collection<E>
개선:
Collection<? extends E>
그래서 메서드는 다음과 비슷해진다.
public static
<E extends Comparable<E>>
E max(
Collection<? extends E> collection
) {
// ...
}
하지만 아직 하나가 남았다.
Comparable<E>
부분이다.
Comparable은 왜 Consumer일까?
이 부분이 처음 보면 가장 어렵다.
Comparable<T>의 핵심 메서드를 생각해보자.
int compareTo(T other);
현재 객체가 다른 객체 하나를 매개변수로 받아서 비교한다.
예를 들어
a.compareTo(b);
라고 하면 a가 b를 받아서 사용한다.
개념적으로
b
↓
compareTo(b)
이다.
즉 Comparable은 비교 대상 객체를 받아들이는 역할을 한다.
그래서 Consumer로 볼 수 있다.
Consumer에는 PECS 규칙상
Consumer Super
를 적용한다.
따라서
Comparable<E>
를 다음처럼 조금 더 유연하게 바꿀 수 있다.
Comparable<? super E>
최종적인 형태
두 가지를 모두 반영하면 다음과 같은 형태가 된다.
public static
<E extends Comparable<? super E>>
E max(
Collection<? extends E> collection
) {
// ...
}
처음 보면 꽤 복잡하다.
하지만 두 부분으로 분리해서 보면 된다.
Collection<? extends E>
그리고
Comparable<? super E>
각각 PECS를 적용한 것뿐이다.
Collection<? extends E>부터 해석하자
이 부분은 이제 익숙하다.
Collection<? extends E>
의 의미는
E 또는 E의 하위 타입을 원소로 가지는 Collection을 받을 수 있다.
이다.
Collection에서 원소를 가져오므로 Producer다.
Producer
↓
extends
Comparable<? super E>를 해석해보자
이쪽은 조금 더 어렵다.
Comparable<? super E>
의 의미는 대략 다음과 같다.
E 자신 또는 E의 어떤 상위 타입과 비교할 수 있는 타입도 허용한다.
왜 이것이 필요할까?
반드시 E가 직접
Comparable<E>
를 구현해야만 비교 가능한 것은 아니기 때문이다.
E의 부모 클래스가 Comparable을 구현하고 있을 수도 있다.
Box 예제로 이해해보자
다음 부모 클래스가 있다고 해보자.
public class Box
implements Comparable<Box> {
protected final int value;
public Box(int value) {
this.value = value;
}
@Override
public int compareTo(Box other) {
return Integer.compare(
this.value,
other.value
);
}
@Override
public String toString() {
return String.valueOf(value);
}
}
Box는
Comparable<Box>
를 구현한다.
즉 Box끼리 비교할 수 있다.
Box
implements
Comparable<Box>
IntegerBox는 Box를 상속한다
이번에는 다음 클래스가 있다고 해보자.
public class IntegerBox
extends Box {
public IntegerBox(int value) {
super(value);
}
}
IntegerBox는 직접 다음을 선언하지 않았다.
implements Comparable<IntegerBox>
하지만 Box를 상속하고 있다.
그리고 Box는
Comparable<Box>
를 구현하고 있다.
관계를 그리면 다음과 같다.
Comparable<Box>
↑
│ implements
Box
↑
│ extends
IntegerBox
IntegerBox끼리 비교할 수 없는 걸까?
실제로는 비교할 수 있다.
IntegerBox는 Box다.
따라서 부모 클래스의 compareTo(Box)를 사용할 수 있다.
IntegerBox first =
new IntegerBox(1);
IntegerBox second =
new IntegerBox(10);
first.compareTo(second);
여기서 second는 IntegerBox지만 Box 매개변수에 전달할 수 있다.
IntegerBox
↓
Box
이기 때문이다.
따라서 IntegerBox 역시 비교 가능한 객체다.
그런데 Comparable만 요구하면 너무 엄격하다
만약 max()가 다음 조건만 요구한다고 해보자.
<E extends Comparable<E>>
E가 IntegerBox라면 조건은 다음처럼 된다.
IntegerBox
extends
Comparable<IntegerBox>
즉 IntegerBox가 직접적으로
Comparable<IntegerBox>
를 구현해야 한다는 식의 강한 조건이 된다.
하지만 실제 IntegerBox는
Comparable<Box>
기능을 부모에게서 물려받고 있다.
충분히 비교할 수 있는데도 API가 거부할 수 있다.
이것 역시 필요 이상으로 엄격한 API다.
Comparable<? super E>라면 해결된다
조건을 다음과 같이 바꾼다.
<E extends Comparable<? super E>>
이번에도 E를 IntegerBox로 생각해보자.
IntegerBox
extends
Comparable<? super IntegerBox>
IntegerBox의 상위 타입에는
IntegerBox
↑
Box
↑
Object
가 있다.
그리고 실제로 IntegerBox는 부모를 통해
Comparable<Box>
를 가지고 있다.
Box는 IntegerBox의 상위 타입이다.
따라서
Comparable<? super IntegerBox>
조건에 들어맞는다.
데이터 흐름으로 보면 더 쉽다
Comparable의 핵심 메서드는 다음과 같다.
compareTo(other)
비교 대상인 other가 안으로 들어온다.
E 객체
↓
compareTo(...)
↓
Comparable
즉 Comparable은 E를 받아들이는 Consumer 역할을 한다.
그래서
Consumer Super
를 적용한다.
Comparable<? super E>
가 되는 것이다.
max()의 두 PECS를 한 번에 보면
최종적인 형태는 다음과 같다.
public static
<E extends Comparable<? super E>>
E max(
Collection<? extends E> collection
) {
// ...
}
복잡해 보이지만 사실 PECS 두 개가 들어간 것뿐이다.
첫 번째:
Collection<? extends E>
Collection은 값을 제공한다.
Producer Extends
두 번째:
Comparable<? super E>
Comparable은 비교할 값을 받아들인다.
Consumer Super
이를 그림으로 보면 다음과 같다.
Collection<? extends E>
│
│ E를 공급
▼
max()
│
│ E를 비교 대상으로 전달
▼
Comparable<? super E>
Producer
→ extends
Consumer
→ super
재귀적 타입 한정과 PECS가 함께 사용된 것이다
처음에는 다음과 같았다.
<E extends Comparable<E>>
자기 자신과 비교 가능한 타입만 받는 재귀적 타입 한정이다.
여기에 PECS를 적용해서 다음과 같이 확장했다.
<E extends Comparable<? super E>>
이제
E 자신과 비교 가능한 타입
뿐 아니라
E의 상위 타입과 비교 가능한 타입
까지 허용할 수 있다.
API가 훨씬 유연해졌다.
중요한 것은 타입 안전성을 포기하지 않았다는 점이다
Comparable<? super E>라고 해서 아무 Comparable이나 허용하는 것이 아니다.
여전히 최소한
E를 비교 대상으로 받아들일 수 있는 타입
이어야 한다.
예를 들어 IntegerBox를 비교하는 과정에서 Box를 받을 수 있는 것은 안전하다.
compareTo(Box other)
에
IntegerBox
를 전달할 수 있기 때문이다.
반대로 전혀 관계없는 타입을 받는 Comparable이라면 조건을 만족하지 않는다.
따라서 타입 안전성은 그대로 유지된다.
세 가지 예제에서 같은 패턴을 찾아보자
Chooser
Chooser(
Collection<? extends T> choices
)
choices가 값을 제공한다.
Producer
↓
extends
union
public static <E> Set<E> union(
Set<? extends E> s1,
Set<? extends E> s2
)
s1, s2가 값을 제공한다.
Producer
↓
extends
max의 Collection
Collection<? extends E>
Collection이 비교할 값을 제공한다.
Producer
↓
extends
max의 Comparable
Comparable<? super E>
Comparable이 비교할 E를 받아들인다.
Consumer
↓
super
결국 모든 예제가 같은 원칙으로 설명된다.
PECS에서 Producer의 의미를 다시 생각해보자
Producer라는 말을
새로운 객체를 직접 생성하는 객체
라고 이해하면 헷갈릴 수 있다.
PECS에서 Producer는 더 넓은 의미다.
현재 메서드에 필요한 값을 제공하는 쪽
이라고 이해하는 것이 좋다.
예를 들어 Chooser 생성자의 Collection은 새로운 Integer를 직접 만드는 것은 아니다.
Collection<? extends Number>
하지만 Chooser가 사용할 값을 Collection에서 가져온다.
그래서 Producer다.
마찬가지로
Set<? extends E>
도 union()에게 원소를 공급하기 때문에 Producer다.
Consumer 역시 객체를 삭제한다는 뜻이 아니다
Consumer도
데이터를 제거한다.
라는 뜻이 아니다.
PECS에서 Consumer는
현재 메서드가 제공하는 값을 받아들이는 쪽
이다.
예를 들어
Collection<? super E>
는 E를 받아 저장한다.
그리고
Comparable<? super E>
는 compareTo()를 통해 E를 비교 대상으로 받아들인다.
둘 다 E를 받아들이므로 Consumer다.
extends와 super가 헷갈리면 메서드 안을 보자
API 선언만 보면
? extends T
와
? super T
가 쉽게 헷갈린다.
그럴 때는 해당 매개변수가 메서드 내부에서 어떻게 사용되는지 보면 된다.
다음처럼 값을 꺼낸다면
T value =
source.get(...);
Producer다.
? extends T
를 검토한다.
반대로 다음처럼 값을 넣는다면
destination.add(value);
Consumer다.
? super T
를 검토한다.
Comparable도 같은 관점으로 볼 수 있다.
value.compareTo(other);
other가 compareTo() 안으로 들어간다.
따라서 비교 대상의 관점에서는 Consumer다.
단순한 암기보다 이 기준이 유용하다
PECS 자체를 외우는 것도 좋지만 실제로 코드를 작성할 때는 다음 두 질문이 더 유용하다.
첫 번째:
이 매개변수에서 T를 꺼내는가?
그렇다면
? extends T
를 생각한다.
두 번째:
이 매개변수에 T를 전달하거나 넣는가?
그렇다면
? super T
를 생각한다.
이를 짧게 기억하면
내 쪽으로 나오면 extends
상대 쪽으로 들어가면 super
라고 볼 수도 있다.
한정적 와일드카드가 없더라도 코드는 틀리지 않을 수 있다
이번 예제에서 중요한 부분이다.
다음 코드가 반드시 잘못된 것은 아니다.
Chooser(
Collection<T> choices
)
다음도 마찬가지다.
Set<E> union(
Set<E> s1,
Set<E> s2
)
<E extends Comparable<E>>
모두 타입 안전한 코드다.
문제는 API가 실제로 안전하게 처리할 수 있는 입력보다 좁은 범위만 허용한다는 것이다.
한정적 와일드카드를 사용하면 타입 안전성은 그대로 유지하면서 더 다양한 정상적인 호출을 허용할 수 있다.
이것이 이번 아이템에서 말하는
API의 유연성을 높인다.
는 의미다.
API 설계자의 관점에서 생각해보자
공통 라이브러리를 만든다고 가정해보자.
처음에는 다음처럼 만들기 쉽다.
void process(
Collection<Event> events
)
그런데 구현을 살펴보니 events에서 값을 읽기만 한다.
그리고 PaymentEvent, OrderEvent 같은 하위 타입도 모두 정상적으로 처리할 수 있다.
그렇다면
void process(
Collection<? extends Event> events
)
가 더 좋은 API일 수 있다.
사용자는
List<PaymentEvent>
도 전달할 수 있고
List<OrderEvent>
도 전달할 수 있다.
라이브러리 구현을 변경하지 않았는데 사용할 수 있는 범위는 넓어진다.
PECS는 라이브러리를 사용하는 사람을 편하게 만든다
제네릭 API를 만드는 개발자는 다음 코드를 보면
Collection<T>
별문제가 없다고 생각하기 쉽다.
하지만 해당 API를 사용하는 사람은 계속 타입이 맞지 않는 문제를 만날 수 있다.
"Integer도 Number인데
왜 List<Integer>를 못 넘기지?"
"IntegerBox도 비교 가능한데
왜 max()에 못 넣지?"
이런 제약 중 실제로 타입 안전성 때문에 필요한 것이 아니라 타입 선언을 지나치게 엄격하게 작성했기 때문에 생긴 제약이라면 와일드카드로 풀어줄 수 있다.
그래서 한정적 와일드카드는 특히 재사용 가능한 라이브러리 API를 설계할 때 가치가 크다.
세 예제를 최종적으로 비교해보자
| 예제 | 역할 | 기존 | 개선 |
|---|---|---|---|
Chooser 생성자 | Producer | Collection<T> | Collection<? extends T> |
union() 첫 번째 Set | Producer | Set<E> | Set<? extends E> |
union() 두 번째 Set | Producer | Set<E> | Set<? extends E> |
max() Collection | Producer | Collection<E> | Collection<? extends E> |
max() Comparable | Consumer | Comparable<E> | Comparable<? super E> |
규칙은 결국 하나다.
값을 제공한다
→ Producer
→ extends
값을 받아들인다
→ Consumer
→ super
PECS를 코드 리뷰에서 활용하는 방법
실무에서 제네릭 API를 리뷰할 때 다음과 같은 매개변수를 발견했다고 해보자.
Collection<T>
바로 와일드카드로 바꿀 필요는 없다.
먼저 구현을 확인한다.
값을 읽기만 하는가?
for (T value : collection) {
...
}
그렇다면
Collection<? extends T>
로 더 유연하게 만들 수 있는지 검토한다.
반대로 값을 넣기만 하는가?
collection.add(value);
그렇다면
Collection<? super T>
를 검토한다.
읽기도 하고 쓰기도 하면서 정확한 T 관계가 필요한 경우에는
Collection<T>
가 더 적절할 수도 있다.
즉 PECS는 기계적으로 적용하는 규칙이라기보다 API가 실제로 요구하는 최소 타입 제약을 찾는 기준으로 사용하는 것이 좋다.
핵심 정리
- PECS는
Producer Extends, Consumer Super의 약자다. - PECS에서 Producer는 객체를 직접 생성한다는 뜻이 아니라 현재 코드에 필요한 값을 공급하는 쪽을 의미한다.
- Consumer는 데이터를 삭제한다는 의미가 아니라 현재 코드가 전달하는 값을 받아들이는 쪽을 의미한다.
Chooser<T>의 생성자가Collection<T>만 받으면Chooser<Number>를 만들 때List<Integer>같은 안전한 하위 타입 컬렉션을 전달하기 어렵다.Chooser(Collection<? extends T>)로 변경하면 T 또는 T의 하위 타입을 원소로 가지는 Collection을 받을 수 있다.- Chooser 생성자의 Collection은 내부 리스트에 값을 제공하므로 Producer이며
extends를 적용할 수 있다. - 두 Set을 합치는
union()에서도 두 입력 Set은 결과 Set에 원소를 공급하므로 모두 Producer다. union(Set<? extends E>, Set<? extends E>)형태를 사용하면Set<Integer>와Set<Double>처럼 E의 서로 다른 하위 타입을 더 유연하게 다룰 수 있다.- 한정적 와일드카드는 아무 타입이나 허용하는 기능이 아니라 공통 상위 타입으로 안전하게 사용할 수 있는 타입 관계를 표현하는 기능이다.
max()의 입력 Collection은 비교할 원소를 제공하므로Collection<? extends E>처럼 Producer Extends를 적용할 수 있다.Comparable은compareTo()의 매개변수로 비교할 객체를 받아들이므로 Consumer 관점으로 볼 수 있다.- 따라서
Comparable<E>를Comparable<? super E>로 만들면 E 자체뿐 아니라 E의 상위 타입을 비교 대상으로 받을 수 있는 구현도 허용할 수 있다. <E extends Comparable<? super E>>는 E가 E 또는 E의 상위 타입과 비교 가능한 타입이어야 한다는 제약을 표현한다.IntegerBox extends Box이고Box implements Comparable<Box>라면IntegerBox가 직접Comparable<IntegerBox>를 구현하지 않아도 부모가 제공하는 비교 기능을 사용할 수 있다.Comparable<? super E>는 이런 상위 타입에서 상속받은 Comparable 구현까지 받아들일 수 있어Comparable<E>보다 유연하다.- 최종적인
max()형태에서는Collection<? extends E>에 Producer Extends가,Comparable<? super E>에 Consumer Super가 적용되어 있다. - PECS를 적용할 때는
extends,super부터 외우기보다 데이터가 어느 방향으로 이동하는지를 먼저 보는 것이 이해하기 쉽다. - 매개변수에서 T 값을 꺼내 현재 코드가 사용한다면 Producer일 가능성이 높고
? extends T를 검토할 수 있다. - 현재 코드가 매개변수에 T 값을 전달하거나 저장한다면 Consumer일 가능성이 높고
? super T를 검토할 수 있다. Collection<T>나Comparable<T>자체가 잘못된 것은 아니며, 한정적 와일드카드는 타입 안전성을 유지하면서 실제로 안전한 입력 범위를 더 넓히기 위해 사용한다.- PECS는 모든 타입에 기계적으로 적용하는 규칙이 아니라 제네릭 API가 실제로 필요로 하는 타입 제약을 최소화해 사용하는 사람에게 더 유연한 API를 제공하기 위한 설계 원칙이다.
한 줄 요약
PECS는 단순히
extends와super를 외우는 규칙이 아니라 데이터의 이동 방향을 보고 값을 공급하는 매개변수에는? extends T, 값을 받아들이는 매개변수에는? super T를 적용해 타입 안전성은 유지하면서Chooser,union(),max()같은 제네릭 API가 불필요하게 거부하던 하위·상위 타입까지 자연스럽게 사용할 수 있도록 만드는 설계 원칙이다.