NMP-Study / EffectiveJava2022

Effective Java Study 2022
5 stars 0 forks source link

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

Closed okhee closed 2 years ago

luckyDaveKim commented 2 years ago

왜? 타입 안전 이종 컨테이너가 필요한가?

일반적인 제네릭에서는 한 컨테이너가 다룰 수 있는 타입의 수가 정해져있다. 예를 들면, Map<K, V> 은 2개의 타입, Set<E> 은 1개의 타입을 갖는다.

하나의 컨테이너에서 다양한 타입을 다룰 수 없을까? 그래서 등장한 타입 안전 이종 컨테이너

어떻게 생겼나?

인터페이스

public interface Favorites {
    <T> void putFavorite(Class<T> type, T instance);

    <T> T getFavorite(Class<T> type);
}

구현체

public class MyFavorites implements Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<>();

    @Override
    public <T> void putFavorite(Class<T> type, T instance) {
        favorites.put(Objects.requireNonNull(type), instance);
    }

    @Override
    public <T> T getFavorite(Class<T> type) {
        // type.cast()는 비검사 형변환 없이도 타입 안전하게 변환 한다.
        return type.cast(favorites.get(type));
    }
}

class를 키로 사용하며, 이를 타입 토큰이라 함

사용예

public class Main {
    public static void main(String[] args) {
        Favorites f = new MyFavorites();

        f.putFavorite(String.class, "Java");
        f.putFavorite(Integer.class, 0xcafebabe);
        f.putFavorite(Class.class, Favorites.class);

        String favoriteString = f.getFavorite(String.class);
        int favoriteInteger = f.getFavorite(Integer.class);
        Class<?> favoriteClass = f.getFavorite(Class.class);

        System.out.printf("%s %x %s %n", favoriteString, favoriteInteger, favoriteClass.getName());
    }
}

타입 안전 이종 컨테이너의 단점은?

1. class 객체를 raw 타입으로 넘기면 타입의 안정성이 깨진다.

기존 MyFavorites 사용 시...

public class AttackRawClass {
    public static void main(String[] args) {
        /* raw 타입 class 공격 */
        Favorites f = new MyFavorites();

        // 타입의 안정성 깨짐
        f.putFavorite((Class)Integer.class, "No Integer 인스턴스");

        // 예외 발생!!!
        int favoriteInteger = f.getFavorite(Integer.class);
        System.out.printf("%d %n", favoriteInteger);
    }
}

MyFavoritesDefenseRawClass 사용 시...

public class MyFavoritesDefenseRawClass implements Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<>();

    @Override
    public <T> void putFavorite(Class<T> type, T instance) {
        /*
         * raw type 공격 방어
         * */
        favorites.put(Objects.requireNonNull(type), type.cast(instance));
        // favorites.put(Objects.requireNonNull(type), instance);
    }

    @Override
    public <T> T getFavorite(Class<T> type) {
        // type.cast()는 비검사 형변환 없이도 타입 안전하게 변환 한다.
        return type.cast(favorites.get(type));
    }
}
public class AttackRawClass {
    public static void main(String[] args) {
        /* raw 타입 class 방어 */
        Favorites f = new MyFavoritesDefenseRawClass();

        // 타입의 안정성 깨짐
        // 예외 발생!!!
        f.putFavorite((Class)Integer.class, "No Integer 인스턴스");

        int favoriteInteger = f.getFavorite(Integer.class);
        System.out.printf("%d %n", favoriteInteger);
    }
}

checkedSet(), checkedList(), checkedMap() 컬렉션 래퍼들이 이와 같은 방법을 사용한다.

2. 실체화 불가 타입에는 사용할 수 없다.

String 이나 String[] 은 사용할 수 있어도 List<String> 이나 List<Integer> 는 저장할 수 없다. List<String> 용 Class 객체를 얻을 수 없기 때문인데, 그 이유는 List<String> 이나 List<Integer>List.class 라는 같은 Class 객체를 공유하기 때문이다.

확장편

지금까지의 Favorites 의 타입 토큰은 비한정적이다. 즉, getFavoriteputFavorite 은 어떤 Class 객체든 받아들인다. 이 메소드들의 타입을 제한하고 싶은 경우, <T extends MyToken> 를 사용하여 한정적 타입 토큰을 사용할 수 있다.

public class MyFavoritesLimitTypeToken {
    private Map<Class<?>, Object> favorites = new HashMap<>();

    public <T extends MyToken> void putFavorite(Class<T> type, T instance) {
        favorites.put(Objects.requireNonNull(type), type.cast(instance));
    }

    public <T extends MyToken> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}

Class<?> 타입의 객체가 있고, 이를 한정적 타입 토큰을 받는 메소드에 넘기기 위해서는 객체를 Class<? extends MyToken> 으로 형변환할 수도 있지만, 이는 비검사 형변환 이라서 컴파일 경고가 발생한다. 이 때, 형변환을 안전하게 동적으로 수행해주는 asSubclass() 메소드를 사용하여 type.asSubclass(MyToken.class) 로 쓸 수 있다.