Programming/Java

[Effective Java] item26 - 로타입은 사용하지 말라

VSFe 2021. 3. 24. 18:24
로타입은 사용하지 말라!

제네릭이 뭐였더라? 클래스와 인터페이스 선언 과정에서 타입 매개변수를 사용하게 된다면, 우리는 그것을 제네릭 클래스/인터페이스라고 한다. 각각의 제네릭 타입은 일련의 매개변수화 타입을 정의하고, 이어서 괄호 안에 실제 타입 매개변수들을 나열한다. (ex. list<String>: 원소의 타입이 String인 리스트를 뜻하는 매개변수화 입이고, 여기서 String은 정규타입 매개변수 E에 해댕하는 실제 타입 매개변수)

동시에, 제네릭 타입을 하나 정의하면 그에 따른 로 타입 (Law Type)도 함께 정의되는데, 이는 타입 매개변수를 전혀 사용하지 않은 제네릭 타입이다. 즉, List<E>를 선언했으면, 그에 따른 로 타입은 List가 되는 것이다. 왜 이런게 필요한걸까? 이유는 제네릭이 도입되기 전 레거시 코드 때문이다. 물론 제네릭이 도입된 시기는 자바5고, 지금은 그런 코드가 사실상 멸종된 시기일 것이다.

그럼 예전 코드는 어땠을까?

private final List franchisee = ....; // 치킨집 가맹점만 넣는다.
franchisee.add(new Pizza(...));

for (Iterator i = franchisee.iterator(); i.hasNext(); ) {
	Chicken chieken = (Chicken) i.next(); // ClassCastException!
}

이런식으로 컴파일 타임이 아닌, 런타임 타임에 예외가 발생하는 것을 볼 수 있다. 이렇게 되면 런타임에 문제를 겪는 코드 부분이 실제 잘못된 코드와 한참 떨어져 있을 수 있다... 물론 제네릭을 사용하면 List<Chicnken> 같은 형식으로 선언하기에 엉뚱한 타입의 인스턴스를 넣으려고 하면 컴파일 타임에서 에러가 발생한다.

따라서 로 타입을 사용하려고 하는걸 언어차원에선 막지 않지만, 레거시 지원을 위한 것이지, 쓰라고 열어둔게 아니니 절대 사용하지 말도록 하자!

그렇다면 임의의 모든 타입에 대해 전부 열어두고 싶다면 어떨까? 이럴 땐 로 타입을 써도 되는걸까? 결론만 말하면 쓰지 말라면 좀 쓰지 말자!! 그냥 List<Object> 같은 방식으로 사용하길 바란다. 기존 로 타입과 비교하면, 전자는 모든 타입을 허용한다는 의사를 컴파일러에 전달한 거고, 후자의 경우 그냥 체킹 자체를 포기한 것이다.

특히, 제네릭의 불공변성에 따라, List<String>은 List<Object>의 하위 타입이 아니다. String[]은 Object[]의 하위타입이기 때문에 배열이나 로 타입을 사용해서 충돌하는 코드를 작성해도 컴파일 타임에선 문제가 발생하지 않지만, 제네릭의 경우 컴파일 타임에 잡아주는 만큼 이 경우에도 피해주는 것이 좋다.

public static int numElemetnsInCommon(Set s1, Set s2) {
    int result = 0;
    for (Object o1: s1)
        if(s2.contains(o1))
            result++;
    return result;
}

다음 코드는 어떨까? 역시 동작은 하지만 로 타입이라... 이런 경우엔 비한정적 와일드카드 타입을 사용하는 것을 권장한다. 제네릭을 쓰고 싶지만 실제 타입 매개변수가 불분명한 경우 ?을 쓰면 된다.

그럼 Set<?>와 Set의 차이는 무엇일까? Set<?>의 경우 타입 자체가 불분명하기 때문에 null을 제외한 그 어떤 타입의 인스턴스도 넣을 수 없다. 즉, 컬렉션의 타입 불변식을 훼손하지 못하도록 막아버린 것이다! 따라서 Set 보다 훨씬 안전하다.

그럼 로 타입은 아예 쓸 일이 없는걸까? 또 그건 아니다. class 리터럴에는 로 타입을 써야 한다. 자바 명세에 따르면, class 리터럴에는 매개변수화 타입을 사용할 수 없다. 즉, List.class는 가능하지만 List<String>.class는 불가능하다. 또, 제네릭은 컴파일 타임에서 잡아주는 친구이므로, 런타임에서는 해당 정보가 날아간다. 따라서 instanceof 연산자는 비한정적 와일드카드 이외의 매개변수화 타입에는 적용할 수 없다.