ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 제네릭 - 아이템 26. 로 타입은 사용하지 말라
    Study/Effective Java 2023. 1. 3. 14:49

    로 타입은 사용하지 말라

    제네릭이란?

    클래스와 인터페이스 선언에 타입 매개변수가 사용되면, 이를 제네릭 클래스 혹은 제네릭 인터페이스라고 부르며 이를 통틀어 제네릭 타입이라고 한다.

    각각의 제네릭 타입은 일련의 매개변수화 타입을 정의한다. 먼저 클래스의 이름이 나오고, 바로 옆에 꺾쇠괄호 안에 실제 타입 매개변수들을 나열한다.

    예를 들어, List<String>은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입이다. 여기서 String은 정규 타입 매개변수 E에 해당하는 실제 타입 매개변수이다.

     

    로 타입이란?

    제네릭 타입을 하나 정의하면 그에 딸린 로 타입도 함께 정의된다. 로 타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다. 예를 들어, List<E>의 로 타입은 List이다.

    로 타입은 타입선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작하는데, 제네릭 도입이 되기 전인 자바 1.4 버전 이하의 코드와 호환되도록 하기 위한 궁여지책이라고 할 수 있다.

     

    로 타입과 제네릭

    1. 로 타입의 사용

    Collection stamps = RawType.getStamps();

    제네릭을 지원하기 전에는 컬렉션을 위와 같이 선언했다. 위 코드를 사용하면 실수로 도장(Stamp) 대신 동전(Coin)을 넣어도 아무런 오류 없이 컴파일되고 실행된다.

     

    public static void main(String[] args) {
        Collection stamps = RawType.getStamps();
        stamps.add(new Coin());
        
        for (Object collectionStamp : stamps) {
            Stamp stamp = (Stamp) collectionStamp;   // ClassCastException을 던짐
        }
    }

    위 코드와 같이, 컬렉션에서 동전을 꺼내기 전까지는 오류를 알아채지 못한다. 이러한 오류는 가능한 발생 즉시, 이상적으로는 컴파일할 때 발견하는 것이 좋다.

    위 예에서는 코드를 작성하고 한참 뒤인 런타임에야 알아챌 수 있는데, 이렇게 되면 런타임에 문제를 겪는 코드와 원인을 제공한 코드가 물리적으로 상당히 떨어져 있을 가능성이 커진다.

     

    하지만, 제네릭을 활용하면 이러한 문제를 해결할 수 있다.

     

    2. 제네릭의 사용

    위 코드와 같이 제네릭을 사용하면, 컴파일러가 stamps에는 Stamp 인스턴스만 넣어야 함을 인지하게 된다. stamps에 엉뚱한 타입의 인스턴스를 넣으면 컴파일 오류가 발생하여 무엇이 잘못되었는지를 정확하게 알려준다.

     

    정리하자면, 로 타입을 사용하는 것을 자바 언어 차원에서 막아 놓지는 않았지만 사용하지 않는 것을 권장한다. 그 이유는 앞선 코드에서 보았듯이 로 타입 사용 시 안겨주는 안정성과 표현력을 모두 잃게 되기 때문이다.

     

    그렇다면, 왜 이러한 로 타입이 존재하는가에 대한 의문이 생길 수 있다. 그 이유는 바로 호환성 때문이다. 자바 1.4 이하 버전에 작성된 코드와의 마이그레이션 호환성을 지켜주기 위해서 타입 이레이저라는 방식을 사용하여 호환성을 지켜주고 있다.

     

    와일드카드 타입
    static int numElementsInCommon(Set s1, Set s2) {
        int result = 0;
        for (Object o1 : s1)
            if (s2.contains(o1))
                result++;
        return result;
    }

    2개의 Set을 받아 공통 원소를 반환하는 메서드를 만든다고 한다면, 어떤 매개변수가 들어오든 상관이 없다. 이때, 로 타입을 사용해도 괜찮지 않을까라는 생각을 할 수도 있다. 이 메서드는 동작을 하지만 로 타입을 사용해 안전하지 않다.

     

     

    static int numElementsInCommon(Set<?> s1, Set<?> s2) {
        int result = 0;
        for (Object o1 : s1)
            if (s2.contains(o1))
                result++;
        return result;
    }

    로 타입 대신 비한정적 와일드카드 타입을 사용하는 것이 좋다. 제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않다면 위 코드와 같이 물음표(?)를 사용하는 것이 좋다. 이것이 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 Set 타입이다.

     

    비한정적 와일드카드 타입인 Set<?>와 로 타입인 Set의 차이는 안전성에 있다. 와일드카드 타입은 안전하고, 로 타입은 안전하지 않다. 로 타입 컬렉션에는 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽다. 반면, Collection<?>에는 다른 어떤 원소도 넣을 수 없다.

     

    로 타입을 사용하는 경우

    로 타입을 쓰지 말라는 규칙에도 소소한 예외가 몇 가지 존재한다.

     

    첫 번째 예외는 class 리터럴에는 로 타입을 써야한다. 여기서 class 리터럴이란 String.class, Integer.class 등을 말한다. 자바 명세는 class 리터럴에 매개변수화 타입을 사용하지 못하게 했다.

     

     

    위 코드에서 보면 알 수 있듯이, String.class, Integer.class, List.class는 가능하지만, List<Integer>.class는 허용하지 않는다.

     

    두 번째 예외는 instanceof 연산자와 관련이 있다. 런타임에는 제네릭 타입 정보가 지워지므로 instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다. 그리고 로 타입이든 비한정적 와일드카드 타입이든 instanceof는 완전히 똑같이 동작한다.

     

     

    instanceof 연산자의 <?>는 오히려 가독성을 떨어트리므로, 차라리 로 타입을 사용하는 것이 좋다.

     

    최종 정리

    로 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안 된다. 로 타입은 제네릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐이다. 빠르게 훑어보자면, Set<Object>는 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입이고, Set<?>는 모종의 타입 객체만 저장할 수 있는 와일드카드 타입이다. 그리고 이들의 로 타입인 Set은 제네릭 타입 시스템에 속하지 않는다. Set<Object>와 Set<?>은 안전하지만, 로 타입인 Set은 안전하지 않다.


    출처

    이펙티브 자바 Effective Java 3/E. 조슈아 블로크 저자(글) · 개앞맵시(이복연) 번역

    https://github.com/woowacourse-study/2022-effective-java/blob/main/05%EC%9E%A5/%EC%95%84%EC%9D%B4%ED%85%9C_26/%EB%A1%9C%20%ED%83%80%EC%9E%85%EC%9D%80%20%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80%20%EB%A7%90%EB%9D%BC.md

     

    728x90

    댓글

Designed by Tistory.