Study/Effective Java

아이템 33. 타입 안전 이종 컨테이너를 고려하라

공29 2024. 12. 22. 19:17

제네릭은 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.
하지만 더 유연한 수단이 필요하다고 해보자. 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면 된다. 이러한 설계 방식을 타입 안전 이종 컨테이너 패턴(type safe heterogeneous container pattern)이라고 한다.

타입 안전하지 않은 예제 먼저 보자.

public class Favorites {
    private Map<Class, Object> map = new HashMap<>();

    public void put(Class clazz, Object value) {
        this.map.put(clazz, value);
    }

    public Object get(Class clazz) {
        return this.map.get(clazz);
    }

    public static void main(String[] args) {
    // Type Safety 하지 않다.
        Favorites favorites = new Favorites();
        
        favorites.put(String.class, "keesun");
        favorites.put(String.class, 1);
        
        favorites.put(Integer.class, 2);
        favorites.put(Integer.class, "2");
    }

}

 

class 리터럴 타입은 Class가 아닌 Class<T>다. 즉 제네릭이다.
예컨대 String.class의 타입은 Class<String> 이고 Integer.class의 타입은 Class<Integer>이다.

그러므로, 타입 안전하도록 아래처럼 작성할 수 있다.

public class Favorites {
    private Map<Class<?>, Object> map = new HashMap<>(); // 비한정적 와일드카드

    public <T> void put(Class<T> clazz, T value) {
//        this.map.put(clazz, value);

        // 넣기 전에 체크하는 코드 추가.
        this.map.put(Objects.requireNonNull(clazz), value);
    }

    public <T> T get(Class<T> clazz) {
        // put 메서드에서 이미 해당하는 타입 T로만 받아오기 때문에, 안전하다고 믿고 (T) 형변환을 해도 안전하다. @SuppressWarning("unchecked")...
//        return (T) this.map.get(clazz);

        // 위 보다 더 안전한 방법. 검사를 하고 형변환을 한다!
        return clazz.cast(this.map.get(clazz));
    }

    public static void main(String[] args) {
        Favorites favorites = new Favorites();

        // 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고 받는 class 리터럴을 타입 토큰(type token)이라고 한다.
        favorites.put(String.class, "keesun");
//        favorites.put(String.class, 1); // compile error

        favorites.put(Integer.class, 2);
//        favorites.put(Integer.class, "2"); // compile error

        String str = favorites.get(String.class);
        Integer num = favorites.get(Integer.class);
    }

}

 

위 Favorites 클래스에는 두 가지 제약이 있다.

1) 악의적인 클라이언트가 Class 객체를 제네릭이 아닌 로 타입으로 넘기면 (아이템 26) 타입 안전성이 쉽게 깨진다.

2) 실체화 불가 타입 (아이템 28)에는 사용할 수 없다.

▼ 예제 코드

public class Favorites {
    private Map<Class<?>, Object> map = new HashMap<>(); // 비한정적 와일드카드

    public <T> void put(Class<T> clazz, T value) {
//        this.map.put(clazz, value);

        // 넣기 전에 체크하는 코드 추가.
        // Raw 타입으로 바꿨을 때 타입 안전성이 깨지는 것은 막을 수 없지만
        // 동적 형변환을 사용하여 put 시점에 타입을 확인할 수 있다.
        this.map.put(Objects.requireNonNull(clazz), clazz.cast(value));
    }

    public <T> T get(Class<T> clazz) {
        // put 메서드에서 이미 해당하는 타입 T로만 받아오기 때문에, 안전하다고 믿고 (T) 형변환을 해도 안전하긴하다. @SuppressWarning("unchecked")...
//        return (T) this.map.get(clazz);

        // 위 보다 더 안전한 방법. 검사를 하고 형변환을 한다! 비검사 형변환하는 손실 없이도 타입 안전하게 만드는 비결이다.
        return clazz.cast(this.map.get(clazz));
    }

    public static void main(String[] args) {
        Favorites favorites = new Favorites();

        // 단점 1. Raw 타입으로 바꿔주면 타입 안전성을 깰 수 있다.
        favorites.put((Class) String.class, 1); // compile error가 발생하지 않음.
        String str1 = favorites.get(String.class);

        // 단점 2. List의 타입을 정할 class 리터럴이 없어서 구분할 수 없다. 아래 코드는 문법이 허용하지 않는다.
        // Super Type Token으로 해결하려는 시도가 있지만 완벽히 만족스러운 우회로는 아니다.
//        favorites.put(List<Integer>.class, List.of(1, 2, 3));
//        favorites.put(List<String>.class, List.of("a", "b", "c"));

//        List list = favorites.get(List.class);
//        list.forEach(System.out::println);
    }

}

 

타입을 제한하고 싶다면, 한정적 타입 토큰을 활용하라.