Java/Java 를 파헤쳐보자

[Java 파헤쳐보기] Generic (제너릭) - PECS

동구름이 2023. 2. 24. 00:15

 지난 포스팅에서 자바 제너릭의 도입 배경과 기능을 살펴보았습니다. 이번 포스팅에서는 제너릭의 중요한 특징인 PECS에 대해 살펴보겠습니다.

 

 

PECS

 PECS(Producer Extends Consumer Super)란, Producer(데이터 생산: 조회) Component에서는 extends 를 사용하고 Consumer (데이터 소비: 저장, 수정 등) Component 에서는 super를 사용한다는 의미입니다.

 

 

불공변이란?

 이것을 보다 자세히 이해하기 위해서는 제너릭의 불공변이라는 특징을 알아야만 합니다. 불공변이란, 서로 다른 제너릭 타입 간에는 상하위 관계가 없다는 것입니다.

 

public class Vehicle {
}
//Car 클래스
public class Car extends Vehicle {
    private String name;
    public Car(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

//Bus 클래스
public class Bus extends Vehicle {
    private String number;
    public Bus(String number) {
        this.number = number;
    }
    public String getNumber() {
        return number;
    }
}

 

만약 위와 같은 상속 관계인 클래스가 있다고 가정해보겠습니다.

 

Car car = new Car("차");
Bus bus = new Bus("버스");
HandleController<Vehicle> carHandleController = new HandleController<Car>(car);
HandleController<Vehicle> busHandleController = new HandleController<Bus>(bus);

 

 그렇다면 위의 코드는 잘 동작을 할까요? 얼핏 보기에는 Vehicle이 car와 bus의 상위 클래스이기 때문에 잘 동작하는 것처럼 보이지만 위 코드는 컴파일 에러가 발생하게 됩니다.

 

 제너릭의 불공변이라는 특성 때문에 Vehicle이 Car, Bus와는 전혀 상관없는 타입으로 인식되기 때문입니다. 제너릭이라는 개념 자체가 안정된 타입 지정을 위한 것을 생각해보면 당연하기도 합니다.

 

 

와일드 카드

 하지만 이런 불공변 특성은 개발자들이 개발을 하는데 있어서 유연성이 떨어진다는 단점이 있습니다. 그래서 등장한 것이 와일드카드 라는 개념입니다.

Car car = new Car("차");
Bus bus = new Bus("버스");
HandleController<?> carHandleController = new HandleController<Car>(car);
HandleController<?> busHandleController = new HandleController<Bus>(bus);

 

위 코드처럼 와일드카드<?>를 사용하면 해당 타입의 위치에 어떤 타입이 와도 받을 수 있습니다. 그리고 와일드 카드를 사용하는 동시에 타입 인자에  extends와 super 키워드를 통해서 제약을 걸어줄 수 있습니다.

 

Upper Bounded 와일드카드 ( extends )

Car car = new Car("차");
Bus bus = new Bus("버스");
HandleController<? extends Vehicle> carHandleController = new HandleController<Car>(car);
HandleController<? extends Vehicle> busHandleController = new HandleController<Bus>(bus);

 

<? extends Vehicle>를 통해 Vehicle의 하위 타입을 받을 수 있습니다. 이것을 Upper Bounded 와일드카드라고 합니다.

 

List<Car> cars = new ArrayList<>();
cars.add(new Car("차1"));
cars.add(new Car("차2"));
cars.add(new Car("차3"));
List<? extends Vehicle> vehicle = cars;
for (Vehicle v : vehicle) {
    System.out.println(v);
}

 

위 코드에서 extends를 사용한 와일드 카드로 리스트를 구성했습니다. 만약 for문을 돌면, 리스트 객체는 Vehicle의 하위 타입이라는 것은 명확하니 for문은 정상적으로 실행됩니다.

 

 하지만 만약 아래와 같은 코드가 있다고 생각해보겠습니다.

vehicle.add(new Car("차4"));

 

이 코드는 어떻게 동작할까요? 컴파일 에러가 발생하게 됩니다. 왜냐하면, vehicle이 참조하는 객체가 Car일 수도, Bus일 수도 있지만 무엇인지 정확히 알 수 없기 때문입니다. 그래서 Car 로 특정된 객체는 컴파일 오류가 발생합니다.

 

 

 

 

Lower Bounded 와일드 카드 ( super )

Vehicle vehicle = new Vehicle();
HandleController<? super Vehicle> handleController = new HandleController<Vehicle>(vehicle);

 

extends와 달리 super는 상위 타입을 받는 것입니다. <? super Vehicle>를 통해 Vehicle의 상위 타입을 받을 수 있습니다. 이것을 Lower Bounded 와일드 카드라고 합니다.

List<? super Vehicle> list = new ArrayList<Vehicle>();
list.add(new Car("차"));
list.add(new Bus("버스"));
list.add(new Vehicle());

 

 그래서 위 코드는 정상적으로 작동합니다. List안의 객체는 Vehicle의 상위 타입이라는 것이 명확하기 때문에, Car든 Bus든 Object 등의 객체를 담을 수 있습니다.

 

for (Vehicle v : list) {
    System.out.println(v);
}

 

 이 코드는 어떻게 실행될까요? 컴파일 오류가 발생하게 됩니다. 왜냐하면 list의 객체가 Vehicle의 상위 객체이기 때문에, Object 같은 객체도 list 의 객체가 될 수 있기 때문입니다.

 

 

 

 이렇듯 PECS는 for (Vehicle v : vehicle) 처럼 데이터를 제공하는 Producer에서는 extends를 사용하고, list.add 처럼 데이터를 수정하고 저장할 때에는 super를 사용하는 것을 말합니다.

 

 실제로 자바의 라이브러리들을 살펴보면 이런 PECS 특성을 활용한 것을 쉽게 볼 수 있습니다. 

 

 

 

참고자료

Java Generic 을 파헤쳐보자 - 심화편
https://dev.gmarket.com/28

제네릭(Generic)의 이해
https://st-lab.tistory.com/153