자바(SE)

[Generic]와일드카드와 PECS

  • -
반응형

이번 포스트에서는 generic에서 사용되는 와일드카드와 PECS(Produce-extends, Consumer-super)에 대해서 알아보자.

 

와일드 카드와 사용법

 

와일드 카드

Generic Type의 객체를 할당 받을 때 정확히 어떤 타입을 받아야 하는지 모를 경우 와일드 카드를 사용할 수 있다. 와일드 카드는 아래와 같이 3가지 형태로 사용할 수 있다.

표현 설명
Generic type<?> 비 한정적 와일드 카드(unbounded wildcard): 타입에 제한이 없음(Object)
Generic type<? extends T> 한정적 와일드 카드(bounded wildcard): T 또는 T를 상속받은 타입들만 사용 가능
Generic type<? super T> 한정적 와일드 카드(bounded wildcard): T 또는 T의 조상 타입만 사용 가능

 

와일드 카드 활용 예

먼저 어떤 객체를 담을 수 있는 GenericBox<T>라는 간단한 클래스를 생각해보자.

public class GenericBox<T> {
    private T some;
    
    public GenericBox() {}
    
    public GenericBox(T some) {
        this.some = some;
    }

    public T getSome() {
        return some;
    }

    public void setSome(T some) {
        this.some = some;
    }
}

그리고 아래와 같이 상속 구조에 있는 클래스를 생각해보자.

class Person{
    public void work() {
        System.out.println("열심");
    }
}
class SpiderMan extends Person{
   public void fireWeb() {
       System.out.println("쉭쉭~~");
   }
}
class VenomSpider extends SpiderMan{
    public void transform() {
        System.out.println("변신~~");
    }
}

이제 Object, Person, SpiderMan을 저장할 수 있도록 box를 구성후 다양하게 할당해보자.

GenericBox<Object> pObj = new GenericBox<>();
GenericBox<Person> pPer = new GenericBox<>();
GenericBox<SpiderMan> pSpi = new GenericBox<>();

GenericBox<?> pAll = pPer; // 타입 무관! 모두다 할당 가능
pAll = pSpi;
pAll = pObj;

GenericBox<? extends Person> pChildPer = pPer;
pChildPer = pSpi;
//pChildPer = pObj;  // Person 또는 Person을 상속 받은 자손만 가능

GenericBox<? super Person> pSuperPer = pPer;
//pSuperPer = pSpi; //Person 또는 Person의 조상만 할당 가늘
pSuperPer = pObj;

이처럼 동작 자체를 이해하는 것은 어렵지 않은데 이 와일드 타입이 필요한 경우를 생각하는게 쉽지 않다. 차근 차근히 살펴보자.

 

 

unbounded wildcard

unbounded wildcard는 타입에 제한을 없애고 싶을 때 사용한다.

raw 타입을 사용하면?

타입의 제한을 없애기 위해서 raw 타입을 사용하면 어떻게 될까? raw 타입은 타입 체킹을 안하겠다는 말과 동일하다.

다음의 useRawType에 사용된 mb는 타입 파라미터 선언이 없는 raw 타입이다. 따라서 GenericBox<Double>이나 GenericBox<Integer>를 모두 처리할 수 있다. 타입을 체킹하지 않기 때문이다. 

 
public void useRawType(GenericBox mb, String type) {
    mb.setSome("hello");
    System.out.printf("type: %s, t: %s%n", type, mb.getSome());
}

public void rawTypeTest() {
    GenericBox<Double> dbox = new GenericBox<>(3.14);
    GenericBox<Integer> ibox = new GenericBox<>(3);

    useRawType(dbox, "Double");
    // 컴파일 타임만 통과하면 문제 없음: type safe 하지 않음
    useRawType(ibox, "Integer");
}

문제는 타입을 체킹하지 않기 때문에 mb.setSome("Hello") 처럼 느닷없이 String을 할당해버릴 수도 있다. 타입의 안정성이 깨지는 것이다.

type: Double, t: hello
type: Integer, t: hello

 

unbounded wildcard 활용(<?>)

이제 unbounded wildcard를 활용해보자. 이제 useGenericWild의 파라미터인 mb는 GenericBox 타입이 되었다. 이 메서드를 호출하는 쪽에서는 마치 raw 타입을 썼을 때처럼 전혀 호출에 제약이 발생하지 않는다.

 
public void useGenericWide(GenericBox<?> mb, String type) {
    // mb의 type parameter를 알 수 없으므로 값을 설정할 수는 없음
    // mb.setSome(new Object());
    System.out.printf("type: %s, t: %s%n", type, mb.getSome());
}
    
public void genericTypeTest() {
    GenericBox<Double> sbox = new GenericBox<>(3.14);
    GenericBox<Integer> ibox = new GenericBox<>(3);

    useGenericWide(sbox, "Double");
    useGenericWide(ibox, "Integer");
}

다만 unbounded wildcard는  타입을 체킹 하는데 어떤 타입이 들어올 지 (GenericBox<Integer>,  GenericBox<String> ....) 모르기 때문에 이번에는 mb.setSome(new Object())처럼 값을 설정할 수가 없다! (compile error 발생)

따라서 mb.getSome() 단지 기존에 mb에 저장된 값을 사용할 뿐이다.  또한 mb에서 getSome()한 결과물은 Object 타입이다. 당연히 compile error가 부적절한 값이 runtime에 설정되는 것보다야 100배 낫지만 조금 아쉬움이 남는다.

 

bounded wildcard

이번에는 bounded wildcard를 사용해보자.

lower bounded wildcard 활용(<? extends Type>)

만약에 아래처럼 메서드를 작성한다면 이 메서드에 연결될 수 있는 타입은 GenericBox<SpiderMan> 뿐이다.

public void onlySpiderMan(GenericBox<SpiderMan> box) {}

그런데 아래와 같이 lower bounded wildcard를 사용하면 SpiderMan 또는 SpiderMan을 상속받은 자식 클래스들로 범위가 한정된다.

public void covarianceBox(GenericBox<? extends SpiderMan> box) {
    // 최소한 SpiderMan이 들어있으므로 SpiderMan로 빼낸 후 사용 가능
    SpiderMan some = box.getSome();
    some.fireWeb();
    // 하지만 box의 정확한 타입이 SpiderMan인지 알 수 없기 때문에 set은 불가
    //box.setSome(new SpiderMan());
}

이렇게 현재 클래스에서 시작해서 자식들을 받을 수 있는 성질을 covariance하다라고 한다. 메서드에 전달된 box에는 무엇이 담겨있을까? 와일드카드가 사용되었기 때문에 정확히는 알수 없으나 SpiderMan, VenomSpider등 SpiderMan을 상속받은 것은 확실하다.!!

따라서 메서드 내부에서 box.getSome()을 하면 반환 타입은 SpiderMan이 반환된다.(? 가 Object 였던 것을 기억해주자!)

하지만 GenericBox<SpiderMan>인지, GenericBox<VenomSpider>인지 모르기 때문에 setSome()을 통해서 새롭게 정보를 설정할 수는 없다. 

이처럼 lower bounded wildcardextends는 조회 시 최대 부모 타입을 보장하지만 설정은 불가하다.

upper bounded wildcard 활용(<? super Type>)

이번에는 upper bounded wildcard를 이용해서 메서드를 작성해보자. <? super SpiderMan>이 적용되면 SpiderMan 본인을 포함, 조상 타입들이 연결될 수 있다. 따라서 이 메서드에 연결할 수 있는 타입은 GenericBox<SpiderMan>, GenericBox<Person>, GenericBox<Object>가 있다. 이렇게 자식에서 시작해서 현재 클래스까지 받을 수 있는 것을  contravariance라고 한다.(covariance의 반대 개념이다.)

public void contravarianceBox(GenericBox<? super SpiderMan> box) {
    // SpiderMan의 조상은 Object까지 갈 수 있으므로 Object로 처리해야함(정확한 타입 모름) 
    Object obj = box.getSome();
    System.out.println(obj);
    // 최소한 SpiderMan의 자식은 저장 가능
    box.setSome(new SpiderMan());
    box.setSome(new VenomSpider());
}

여기서는 전달된 파라미터가 Object까지 될 수 있기 때문에 box에서 getSome을 하면 반환 값은 Object 타입이 된다. 하지만 최대한 내려가 봤자 SpiderMan 까지 이므로 SpiderMan의 자식들은 저장해줄 수 있다! (만약 전달된 객체가 SpiderMan 이었다면 Person을 담을수 없을 것이다.)

이처럼 upper bounded wildcard는 읽기는 Object로만 처리할 수 있지만 저장할 상한 부모 타입을 지정해서 직접 값을 저장할 수 있다.

 

PECS(Producer extends, Consumer super)

그런데 <? extends Type>을 써야할지 <? super Type>을 써야할지는 많이 헷갈리는 경우가 많다. 그래서 말줄임 좋아하는 사람들이 PECS라는 말을 만들어 놓았다. 즉 인자가 생산자라면 extends 를 사용하고 소비자라면 super를 사용하라는 이야기이다. 뭔가 좋은 힌트를 주는 말 같긴 한데 "누구를 기준으로 하는지"를 잘 생각하지 않으면 완전 정 반대로 사용하게 되므로 주의가 필요하다.

여기서 생산자전달된 와일드 타입 객체를 이용해서 생성하는 녀석이다. 따라서 값을 조회하기 좋아야 하므로 <? extends T>를 쓴다. 소비자전달된 객체에게 소비하는(값을 저장하는) 녀석이다. 따라서 값을 저장할 수 있는 <? super T>를 쓴다.

Collections에 선언된 copy라는 메서드를 살펴보자.

public class Collections {
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        . . .
    }
}

이 메서드는 src 의 내용을 dest에 복사해주는 역할을 해야 한다. src의 내용을 조회해서 저장할 데이터를 생산한 후 dest에 소비해야 한다. 따라서 src는 생산자인 P 즉 extends이고 dest는 소비자인 C 즉 super가 적용되었다.

만약 read도 해야 하고 write도 해야 하는 상황이라면 wildcard를 사용하면 안된다.

 

반응형

'자바(SE)' 카테고리의 다른 글

[Java]버전 별 특징 JDK 09  (0) 2023.05.19
[JAVA] JRE 버전 문제 정리  (0) 2023.02.19
[Generic] 배열과 List<E>의 차이점  (0) 2023.01.19
[자바]JVM Heap Memory size  (0) 2022.08.14
[Generic]raw 타입을 사용하지 말자.  (0) 2022.07.24
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.