PECS의 원칙은 'Producer-Extends, Consumer-Super'의 약자로, 안전한 데이터 이동 및 API 설계의 유연성을 위한 와일드카드를 사용하여 메소드로 전달되는 컬렉션의 역할을 명확히 구분하는 원칙이다.
데이터를 읽기만 하는 생산자(Producer) 파라미터에는 ? extends T를 사용하여 상위 경계를 지정하고, 데이터를 쓰기만 하는 소비자(Consumer) 파라미터에는 ? super T를 사용하여 하위 경계를 지정한다. 이를 통해 컴파일러는 컬렉션을 '읽기 전용' 또는 '쓰기 전용'으로 강제하여 런타임 오류를 원천적으로 방지하고, 코드의 유연성은 극대화한다.
| 키워드 | 역할 | 사용 목적 | 예시 타입 | 설명 |
|---|---|---|---|---|
| ? extends T | Producer | 읽기 전용 | List <? extends Number> | 생산된 걸 꺼내어 쓰는 입장. 안전하게 꺼낼 수 있음 |
| ? super T | Consumer | 쓰기 전용 | List <? super Integer> | 소비자에게 값을 제공하는 입장. 안전하게 넣을 수 있음 |
예제1
import java.util.ArrayList;
import java.util.List;
abstract class Animal {
abstract void speak();
}
class Dog extends Animal {
void speak() {
System.out.print("멍");
}
}
class Cat extends Animal {
void speak() {
System.out.print("야옹");
}
}
public class Main {
static List<Animal> animals = new ArrayList<>();
static void makeAnimalsSpeak(List<? extends Animal> animals) {
for(Animal a:animals) {
a.speak();
}
System.out.println("\n---------------------");
}
static <T> void copyAnimals(List<? extends T> src, List<? super T> dest) {
for(T animal : src) {
dest.add(animal);
}
}
public static void main(String[] args) {
animals.add(new Dog());
animals.add(new Cat());
makeAnimalsSpeak(animals);
List<Dog> dogs = List.of(new Dog(), new Dog(), new Dog());
List<Cat> cats = List.of(new Cat(), new Cat(), new Cat());
copyAnimals(dogs, animals);
copyAnimals(cats, animals);
makeAnimalsSpeak(animals);
}
}
실행 결과:
멍야옹
---------------------
멍야옹멍멍멍야옹야옹야옹
---------------------
또한 아래 이미지의 에러가 나는 이유를 설명하자면, 컴파일러는 src 인자인 List로부터 T는 Animal의 상위 타입이어야 한다고 추론하고, dest 인자인 List으로부터 T는 Cat의 하위 타입이어야 한다고 추론한다. 이 두 조건을 동시에 만족하는 타입 T는 없으므로 에러가 발생하는 것이다.
PECS 원칙을 처음 공부할 때 읽기/쓰기 기능에서 각각 add()/get() 메소드를 제한한다고 배워, ‘그럼 get() 메소드가 없는 Set과 같은 컬렉션은 PECS 원칙이 적용이 안되나?’ 라는 단순무식한 궁금증이 들었다.😂
당연히 그런게 전혀 아니며, 와일드카드를 사용할 수 있는 모든 제네릭 클래스나 인터페이스에서 원칙이 적용된다. 따라서 각 컬렉션이 가지고 있는 객체 추가 기능(쓰기), 객체 검색 기능(읽기)의 메소드가 모두 PECS 규칙에 적용된다. 이를 더 확실히 이해할 수 있게 컴파일러의 정확한 동작원리를 밑에서 알아보자.
먼저 PECS 원칙은 논리적인 권장 사항이나 약속이 아닌 자바 언어 명세에 정의된 강제적인 컴파일러의 규칙이다. 위에서 <? extends T>를 사용하면 읽기 전용이고 <? super T>를 사용하면 쓰기 전용이라고 하였다. 그런데 컴파일러는 어떻게 읽기 기능과 쓰기 기능을 구분하는 걸까? 컴파일러는 ‘메소드 시그니처’라는 기준으로 판단한다
그렇다면 여기서 또 하나 궁금증이 들 수 있다. 만약 T가 반환 타입과 매개변수에 모두 존재한다면 어떻게 될까? 답은 당연히 읽기, 쓰기 전용 모드 둘 다 사용할 수 없게 된다.
예제2
아래 이미지처럼 T를 반환 타입과 매개변수에 모두 사용한 메소드를 정의하면 컴파일 시점에 다음과 같은 에러가 생기게 된다.
지금까지 PECS 원칙이 무엇이며 어떻게 동작하는지 알아보았다. 처음 PECS 원칙을 접했을 때는 extends와 super의 복잡한 규칙에 막막함이 앞섰지만 컴파일러가 정확히 어떻게 동작하는지 공부하니, 이 원칙이 코드의 안정성을 확보해주는 정교한 설계임을 깨달을 수 있었다.