본문 바로가기

STUDY/이펙티브자바

5-7) 타입 안전 이종 컨테이너를 고려하라

타입 안전 이종 컨테이너

 

public class Favorites {
    public <T> void putFavorite(Class<T> type, T instance);
    public <T> T getFavorite(Class<T> type);
}

제네릭은 Set<T>, Map<K, V> 등 단일원소 컨테이너에 흔히 쓰인다. 이러한 컨테이너는 원소로 넣을 수 있는 타입의 수가 제한되어 있기 때문에 매개변수 타입의 수가 제한되어 있다. 하지만 더욱 유연한 컨테이너가 필요할 때도 있다. 다양한 타입의 원소를 가지는 컨테이너가 필요하다면 어떨까? 다양한 타입을 가지면서도 값을 넣거나 뺄 때 안전하게 사용할 수 있어야 할 것이다. 그리고 이를 타입 안전 이종 컨테이너라고 부른다. 위의  Favorites 클래스는 각 타입마다 좋아하는 객체를 넣을수 있는 이종 컨테이너 클래스이다. putFavorite에서 제네릭을 통해 instance가 type이 나타내는 클래스의 객체라는 것을 보장하였다. 그리고 getFavorate에서도 메소드의 반환 값이 type이 나타내는 클래스의 객체라는 것을 보장하였다.

 

public static void main(String[] args) {
    Favorites f = new Favorites();
    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());
}

 

 


구현

 

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

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

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

Favorites 클래스의 구현은 놀랍도록 단순하다. 객체를 담는 favorates의 타입이 Map<Class<?>, Object>와 같이 와일드카드를 사용해서 Map의 키값에 어떤 타입의 클래스도 올 수 있도록 하였다. 다양한 타입을 지원하는 힘이 여기서 나온다. 다만 favorates 자체가 키와 값의 동일 타입을 보장해주지는 않는다. putFavorate 메소드는 타입의 Class와 객체를 받아서 type과 instance가 동일 타입을 바라본다는 사실을 보장한다. 그리고 getFavorate는 매개변수 Class와 반환값이 같은 타입을 사용하고 Class로 형변환을 하여 같은 타입이 반환된다는 사실 보장한다.

 

 


Favorites 클래스의 제약

 

// Integer.class를 raw 타입은 Class로 형변환 하면 타입 안정성이 깨진다.
Favorites favorites = new Favorites();
favorites.putFavorite((Class)Integer.class, "Integer 객체 아님");
favorites.getFavorite(Integer.class); // ClassCastException 런타임 에러 발생

악의적인 클라이언트가 Class 객체를 로 타입으로 넘기면 Favorates의 타입 안정성은 쉽게 깨진다. 잘못된 타입의 객체를 putFavorate로 넣을때는 에러가 발생하지 않지만 getFavorate로 객체를 가져올때 에러가 발생한다. 완전히 문제를 해결할 수 는 없지만 putFavortate에 동적 형변환 코드를 넣음으로서 객체를 넣을 때 에러가 발생하도록 한다.

 

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

 

또 Favorates에 List<String>와 같은 실체화 불가 타입에는 사용할 수 없다. List.class와 같이 List에는 Class 객체가 있지만 List<String>, List<Integer> 각각에는 Class 객체가 없다.

 

 


한정적 타입 토큰 활용

 

// Number의 하위 클래스만을 사용하도록 제한
public <T extends Number> void putFavorite(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), type.cast(instance));
}

// type으로 T의 부모 클래스만을 사용하도록 제한
public <T> void putFavorite(Class<? super T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), type.cast(instance));
}

Favorate는 모든 Class 객체를 받아들이지만, 한정적 타입 토큰을 활용하여 이를 제한할 수 있다. 

728x90