반응형

 

매개변수화 타입은 유연할까?

매개변수화 타입은 불공변이다. 불공변은 계층적으로 설계된 타입을 아예 다른 타입으로 인식하는 성질이다. 일반적인 레퍼런스 타입의 경우 다형성을 활용하여 상위 타입 변수에 하위 타입 인스턴스가 할당될 수 있지만, 매개변수화 타입은 계층 관계가 있다 한들 그저 다른 타입으로 인식하기 때문에 할당이 불가능하다. 다형성을 활용할 수 없기 때문에 타입에 대해 유연하다고 할 수 없는 것이다.

List<Object> a = new ArrayList<String>(); // 불공변. 컴파일 에러
Object b = "hi";

List<Animal> c = new ArrayList<Cat>(); // 불공변. 컴파일 에러
Animal d = new Cat();

 

 


매개변수화 타입도 유연해질수 있다

매개변수화 타입도 여러 타입을 받을 수 있도록 유연해지는 방법이 있다. 바로 한정적 와일드카드를 사용하는 것이다. 먼저 기본적인 매개변수화 타입을 사용하는 스택 클래스를 보고 타입 유연성에 대한 문제점을 인지해보자.

 


스택 클래스의 문제점

아래는 필자가 간단하게 구현한 스택 클래스이다. Stack 인스턴스 생성 시 E 라는 타입 매개변수를 받고 있으며, 이때 받은 타입 매개변수가 여러 메서드에서 사용되고 있다. 이 스택 클래스의 첫번째 문제점은 pushAll에서 찾을 수 있다. 

public class Stack<E> {

    private final List<E> elementList = new ArrayList<>();
    private int size = 0;
    
    public void push(E element){
        elementList.add(element);
        size++;
    }

    public void pushAll(List<E> anotherList){
        elementList.addAll(anotherList);
        size += anotherList.size();
    }

    public void flush(List<E> anotherList){
        while(!isEmpty()){
            anotherList.add(pop());
        }
    }

    private E pop(){
        E element = elementList.get(--size);
        elementList.remove(size);
        return element;
    }

    private boolean isEmpty(){
        return elementList.isEmpty();
    }

    public void print(){
        elementList.forEach(System.out::println);
    }
}

 


Number 타입 엘리멘트 저장용 스택

Number 타입 엘리멘트를 저장하기 위해 Number 타입의 스택을 생성하였다. Number 클래스는 Double, Integer와 같은 숫자 타입 클래스의 상위 클래스이며, push 메서드를 통해 값을 넣을 수 있다. push 메서드는 그저 제네릭 타입의 매개변수를 받고있기 때문이다. 여기서는 Double 타입으로 오토박싱될 1.1 값을 통해 push 메서드를 호출하고 있다. 

Stack<Number> stack = new Stack<>();
stack.push(1.1);

 

 

문제는 매개변수화 타입을 받는 pushAll에서 발생한다. Double 타입의 리스트를 받지 못하는 것이다.

컴파일 타임에서의 pushAll 메서드 매개변수 타입은 List<Number> 인데 List<Double> 타입의 값을 매개변수로 사용하려 했기 때문이다. List<Number>와 List<Double>은 타입이 다르므로 컴파일 에러가 발생한다.

List<Double> list = new ArrayList<>();
list.add(2.2);
list.add(3.3);

stack.pushAll(list); // 컴파일 에러 발생

 


한정적 와일드카드를 통해 유연성을 높여보자.

한정적 와일드 카드를 사용하여 Number 타입과 같거나 서브 타입에 대한 타입 매개변수를 갖는 리스트로 변경하였다. List<Double> 타입의 Double은 Number의 하위타입이므로 컴파일 에러가 발생하지 않게 된다. 

public void pushAll(List<? extends E> anotherList){
    elementList.addAll(anotherList);
    size += anotherList.size();
}

 

 

여기서 끝이 아니다 flush 메서드도 비슷한 문제가 있다. 아래 테스트 케이스를 보자.

Integer 타입의 스택 인스턴스를 생성하고, Integer 리스트로 스택의 데이터를 모두 추출하여 전달하고 있다. 여기서의 문제도 마찬가지로 유연성이다. Stack을 Integer 타입으로 생성하면 flush 시 List<Integer> 타입의 매개변수밖에 받지 못한다.

Stack<Integer> stack = new Stack<>();
stack.push(1);

List<Integer> numberList = new ArrayList<>();
stack.flush(numberList);

 

 

 Integer 상위 타입인 Number 리스트로 받을 수 있을 것이라 생각했지만 컴파일 에러가 발생해버린다.

List<Number> numberList = new ArrayList<>();
stack.flush(numberList); // 타입 불일치 컴파일에러 발생

 

 

이것도 마찬가지로 한정적 와일드카드를 사용하여 해결 가능하다. pushAll과의 차이는 extends가 아닌 super를 사용하는 것이다. <? super E> 는 E 클래스의 상위 타입을 의미한다. 즉, Number는 Integer의 상위타입이므로 컴파일 에러가 발생하지 않게 된다.

public void flush(List<? super E> anotherList){
    while(!isEmpty()){
        anotherList.add(pop());
    }
}

 

 

이로써 수정된 Stack 클래스는 다형성을 지원하는 것처럼 유연한 클래스로 변경되었다.


팩스(PECS)

 PECS는 Producer-Extends, Consumer-Super의 약자로, 들어온 매개변수화 타입이 생산자라면 extends를 사용하고 소비자라면 super를 사용하라는 공식이다.

 pushAll 메서드는 들어오는 매개변수화 타입 인스턴스를 엘리멘트에 추가하기 위해 외부에서 생성해 들어온 값이다. 즉 생산자이므로 List<? extends E>를 사용하고, flush 메서드는 들어오는 매개변수화 타입 인스턴스에 엘리멘트를 소비한다. 즉, 소비자이므로 List<? super E> 를 사용한다.

 


 

정리

 와일드카드 타입을 사용하면 API가 유연해진다. 생산자와 소비자를 잘 구분하여 적절한 한정적 와일드카드를 사용하자. 공식을 활용하는 것도 좋지만 왜 이런 공식이 나왔는지를 이해하고 사용하는 것이 중요하다고 생각한다.

반응형

+ Recent posts