raw 타입은 사용하지 말자

2021-03-25

클래스와 인터페이스 선언에 타입 매개변수가 쓰이면 이를 제너릭 클래스 혹은 제너릭 인터페이스라고 한다. List 인터페이스로 예를 들자면, List 인터페이스는 원소의 타입을 나타내는 타입 매개변수 E를 받는다.

List<String> 에서는 String이 정규 타입 매개변수 E에 해당하는 실제 타입 매개 변수이다.

단어 정리

한글용어 영문용어
매개변수화 타입 parameterized type List<String>
실제 타입 매개변수 actual type parameter String
제너릭 타입 generic type List<E>
정규 타입 매개변수 formal type parameter E
비한정적 와일드카드 타입 unbounded wildcard type List<?>
로 타입 raw type List
한정적 타입 매개 변수 bounded type parameter <E extends Number>
재귀적 타입 한정 recursive type bound <T extends Comparable<T>>
한정적 와일드카드 타입 bounded wildcard type List<? extends Number>

Raw 타입

raw 타입이란, 제너릭 타입에서 타입 매개 변수를 전혀 사용하지 않을 때를 말한다. 예를 들어 List를 선언할 때 List 만 선언해 놓은 경우를 말한다.

컬렉션의 Raw 타입

private final Collection stamps = ...;

이 코드를 사용하면 실수로 도장(Stamp) 대신 동전(Coin)을 넣어도 아무 오류 없이 컴파일 되고 실행된다.

//실수로 동전을 넣는다.
stamps.add(new Coin(...)); // "unchecked call" 경고를 내뱉는다.

매개변수화된 컬렉션 타입 - 타입 안전성 확보!

private final Collection<Stamp> stamps = ...;

이렇게 선언하면 컴파일러는 stamps에는 Stamp의 인스턴스만 넣어야 함을 컴파일러가 인지하게 된다. 따라서 아무런 경고 없이 컴파일된다면 의도대로 동작할 것임을 보장해준다.

제너릭 소거(Generic erasure)

Raw 타입을 쓰는 걸 언어 차원에서 막아 놓지는 않았지만 절대로 써서는 안 된다. Raw 타입을 쓰면 제너릭이 안겨주는 안전성과 표현력을 모두 잃게 된다.

그렇다면 Raw 타입을 왜 없애지 않았을까? 바로 호환성 때문이다. 자바가 제너릭을 받아들이기까지 거의 10년이 걸린 탓에 제너릭 없이 짠 코드가 이미 많아졌다. 그래서 기존 코드를 모두 수용하면서 제너릭을 사용하는 새로운 코드와도 맞물려 돌아가게 해야만 했다.

Raw 타입을 사용하는 메서드에 매개변수화 타입의 인스턴스를 넘겨도 동작해야만 해야했다. 이 마이그레이션 호환성을 위해 Raw 타입을 지원하고 제너릭 구현에는 소거 방식을 사용하기로 했다.

List vs List<Object>의 차이

List 같은 Raw 타입은 사용해서는 안되지만, List<Object> 처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮다.

이 둘의 차이점은 List는 제너릭 타입에서 완전히 발을 뺀 것이고 List<Object>는 모든 타입을 허용한다는 의사를 컴파일러에게 명확히 전달한 것이다.

List 매개변수에는 List<String>을 전달할 수는 있지만, List<Object> 매개변수에는 List<String>을 전달할 수 없다. 즉 타입 안정성을 잃게 된다.

제너릭 소거 더 알아보기

다음과 같은 메서드가 있다.

public static <E> void printArray(E[] array) {
    for (E element : array) {
        System.out.printf("%s ", element);
    }
}

이 메서드를 컴파일 하면 다음과 같이 된다.

public static void printArray(Object[] array) {
    for (Object element : array) {
        System.out.printf("%s ", element);
    }
}

컴파일 시 E타입이 Object 타입으로 변경이 된다.

이렇게 E 타입만 있는 상황을 unbound 되었다라고 한다.

이번엔 이렇게 해보자.

public static <E extends Comparable<E>> void printArray(E[] array) {
    for (E element : array) {
        System.out.printf("%s ", element);
    }
}

이 메서드를 컴파일하면 다음과 같이 된다.

public static void printArray(Comparable[] array) {
    for (Comparable element : array) {
        System.out.printf("%s ", element);
    }
}

아까랑은 다르게 E가 부모타입인 Comparable로 되었다.

이렇게 부모 타입으로 변경되는 상황을 bound 되었다라고 한다.

Raw 타입을 써야 할 때

제너릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 물음표(?)를 사용하자. 제너릭 타입인 Set<E>의 비한정적 와일드 카드(Unbound wildcard type)은 Set<?> 이다. 이것이 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 Set이다.

static int numElementsInCommon(Set<?> s1, Set<?> s2) {...}

Set vs Set<?>

이 둘의 특징을간단히 말하자면 와일드카드 타입은 안전하고, Raw타입은 안전하지 않다. Raw 타입 컬렉션은 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽다. 반면 Collection<?> 에는 어떤 원소도 넣을 수 없다. 다른 원소를 넣으려고 하면 오류메세지가 나온다.

즉 컬렉션의 타입 불변식을 훼손하지 못하게 막았다. 구체적으로는 어떤 원소도 Collection<?>에 넣지 못하게 했으며 컬렉션에서 꺼낼 수 있는 객체의 타입도 전혀 알 수 없게 했다. 이러한 제약을 받아들일 수 없다면 제너릭 메서드나 한정적 와일드카드 타입을 사용하면 된다.


로 타입을 쓰지 말라는 규칙에도 예외가 몇 개 있다.

class 리터럴에는 Raw 타입을 써야 한다. 자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다.예를 들어 List.class, String[].class, int.class는 허용하고 LIst<String>.class와 List<?>.class는 허용하지 않는다.

또 다른 예로 instanceof 연산자와 관련이 있다. 런타임에는 제너릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다. 그리고 Raw 타입이든 비한정적 와일드카드 타입이든 instanceof는 완전히 똑같이 동작한다. 비한정적 와일드카드 타입의 꺽쇠괄호와 물음표는 아무런 역할 없이 코드만 지저분하게 만드므로, 차라리 raw 타입을 쓰는 편이 깔끔하다.

if(o instanceof Set) {
  Set<?> s = (Set<?)o;
}

정리

Raw 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안 된다. Raw 타입은 제너릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐이다.