지식
Java
PECS 원칙 — Producer Extends, Consumer Super
PECS(Producer Extends Consumer Super) 원칙의 의미와 암기법, Collections.copy 시그니처 분석, 생산자와 소비자 역할 구분, 그리고 API 설계에서 와일드카드를 올바르게 쓰는 방법
지난 글에서 세 가지 와일드카드의 읽기·쓰기 제약을 배웠다. 이번에는 PECS 원칙을 다룬다. PECS는 Joshua Bloch가 《Effective Java》에서 제안한 와일드카드 선택 기준으로, “Producer Extends, Consumer Super”의 약자다. 와일드카드를 어떤 상황에 어떻게 써야 하는지 한 문장으로 정리한 황금 규칙이다.
PECS의 핵심 관점
코드를 “내가 컬렉션에서 값을 꺼내는가(Producer)” 아니면 “내가 컬렉션에 값을 넣는가(Consumer)” 의 관점으로 바라본다.
- Producer(생산자): 컬렉션이 값을 제공 → 내가 읽는다 →
? extends T - Consumer(소비자): 컬렉션이 값을 받음 → 내가 쓴다 →
? super T
Collections.copy로 이해하는 PECS
표준 라이브러리 Collections.copy의 시그니처가 PECS의 교과서적 예제다.
public static <T> void copy(
List<? super T> dest, // Consumer: T를 받으므로 super T
List<? extends T> src // Producer: T를 제공하므로 extends T
) {
int srcSize = src.size();
// ...
for (ListIterator<? super T> i = dest.listIterator(); ...) {
i.next();
i.set(src.get(srcSize++));
}
}
src에서 원소를 읽어서 dest에 넣는다. 읽는 쪽(src)은 extends, 쓰는 쪽(dest)은 super.
List<Integer> src = List.of(1, 2, 3);
List<Number> dest = new ArrayList<>(Arrays.asList(0, 0, 0));
Collections.copy(dest, src);
// T = Integer 추론
// dest: ? super Integer → List<Number> 허용 (Number super Integer)
// src: ? extends Integer → List<Integer> 허용 (trivially)
System.out.println(dest); // [1, 2, 3]
PECS 적용 전/후 비교
PECS 없이 작성한 addAll 구현과 PECS를 적용한 구현을 비교해 보자.
// PECS 없음 — 유연성 부족
static <T> void addAll(List<T> dest, List<T> src) {
for (T e : src) dest.add(e);
}
// PECS 적용 — 훨씬 유연
static <T> void addAll(List<? super T> dest, List<? extends T> src) {
for (T e : src) dest.add(e);
}
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
// PECS 없으면 컴파일 오류 (T는 Integer이어야 하는데 dest는 Number)
// PECS 있으면 OK — T = Integer, dest는 Number(super Integer), src는 Integer
addAll(nums, ints); // PECS 버전: OK
추가 예제 — 스택 pushAll/popAll
class Stack<E> {
// 생산자 src에서 읽어 this에 push
public void pushAll(Iterable<? extends E> src) {
for (E e : src) push(e);
}
// 소비자 dst에 this에서 pop해서 씀
public void popAll(Collection<? super E> dst) {
while (!isEmpty()) dst.add(pop());
}
}
Stack<Number> numStack = new Stack<>();
// pushAll: Integer는 Number의 하위 타입 — extends Number
Iterable<Integer> ints = List.of(1, 2, 3);
numStack.pushAll(ints); // OK
// popAll: Object는 Number의 상위 타입 — super Number
Collection<Object> objs = new ArrayList<>();
numStack.popAll(objs); // OK
비교자에서의 PECS
Comparator 파라미터에도 동일한 원칙을 적용한다.
// Effective Java에서 권장하는 sort 시그니처
public static <T extends Comparable<? super T>> void sort(List<T> list)
// Comparator도 Consumer → super 사용
public static <T> void sort(List<T> list, Comparator<? super T> c)
Comparator<? super T> 덕분에 Animal 비교자를 Dog 리스트 정렬에 재사용할 수 있다.
Comparator<Animal> animalComp = Comparator.comparing(Animal::getName);
List<Dog> dogs = new ArrayList<>(List.of(new Dog("Rex"), new Dog("Max")));
dogs.sort(animalComp); // ? super Dog — Animal super Dog → OK
암기법 요약
내가 컬렉션에서 꺼낸다 (읽는다) → extends (Producer Extends)
내가 컬렉션에 넣는다 (쓴다) → super (Consumer Super)
둘 다 안 함 → ?
지난 글: 와일드카드 — ? 타입의 유연성
다음 글: 타입 소거 — 런타임의 제네릭 타입
읽어주셔서 감사합니다. 😊