티스토리 뷰

728x90
반응형

타입 안전 이종 컨테이노를 고려하라


  • 제네릭은 Set<E>, Map<K, V>등의 컬렉션이나 ThreadLocal<T>, AtomicReference<T>등의 단일원소 컨테이너에서도 흔히 사용됩니다. 이런 모든 쓰임에서 매개변수화되는 대상은 원소가 아닌 자기자신입니다. 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수는 제한이 되는데, 이러한 상황에서 제네릭을 조금 더 유연하게 사용하고자 나온 패턴이 타입 안전 이종 컨테이너 패턴입니다.

💡 타입 안전 이종 컨테이너 패턴이란?

  • 컨테이너 대신 키를 매개변수화한 다음 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하는 방식입니다. 이렇게 된다면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해줄것입니다.

 

💡 예제

  • Class 클래스는 제네릭 클래스이기 때문에 Class 리터럴의 타입은 Class가 아닌 Class<T>입니다.
  • 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴을 타입 토큰이라 합니다.
  • 키가 매개변수화되었다는 점을 제외하면 일반 맵(Map)과 유사합니다.
  • 아래 Favorites 인스턴스는 타입 안전합니다. 그리고 내가 요청한 클래스(Ex: String.class)와 다른 타입을 반환하는 일은 없습니다.
    그리고 맵과 다르게 여러 타입의 원소를 담을 수도 있으며, 이런 객체를 타입 안전 이종 컨테이너라고 할 수 있습니다.
public class Favorites {

    private Map<Class<?>, Object> favorites = new HashMap<>();
    
    public <T> void putFavorites(Class<T> type, T instance) {
        favorites.put(Objects.requireNonNull(type), instance);
    }
    public <T> T getFavorites(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}

public class EffectiveJavaApplication {

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

        favorites.putFavorites(String.class, "Java");
        favorites.putFavorites(Integer.class, 123);
        favorites.putFavorites(Class.class, Favorites.class);

        String favoriteStr = favorites.getFavorites(String.class);
        Integer favoriteInt = favorites.getFavorites(Integer.class);
        Class favoriteClass = favorites.getFavorites(Class.class);

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

 

💡 Map<Class<?>, Object> favorites

  • Favorites 클래스에서 사용하는 private 변수는 Map<Class<?>, Object>입니다.
  • 비한정적 와일드카드 타입인데 null 말고는 아무것도 넣을 수 없다고 했는데 어떻게 가능할까요?
  • 이는 와일드카드 타입이 중첩(nested)되어 있다는것을 알아야합니다. Map이 아니라 key가 와일드카드 타입인 것입니다. 이는 모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻으로, 첫번째는 Class<String>, 두번째는 Class<Integer>식으로 될 수 있습니다.
  • 그리고 값 타입이 Object라는 점은 이 Map은 키와 값 사이의 타입 관계를 보증하지 않는다는 말입니다. 즉 모든 값이 키로 명시한 타입임을 보증하지 않습니다. 사실 자바의 타입 시스템에서는 이 관계를 명시할 방법이 없습니다. 하지만 우리는 이 관계가  성립한다는것을 알고 있기에 이점을 누릴 수 있습니다.

💡 putFavorites

  • putFavorites의 구현 방식은 아주 쉬운데, 주어진 Class객체와 즐겨찾기 인스턴스를 favorites 변수에 추가해 관계를 지으면 끝입니다. 말했듯이 키와 값 사이의 타입 링크 정보는 버려집니다.
    • 즉 그 값이 그 키 타입의 인스턴스라는 정보가 사라집니다. 하지만 getFavorites 메서드에서 이 관계를 되살릴 수 있으니 상관없습니다.

💡 getFavorites

  • getFavorites 메서드는 먼저 주어진 Class 객체에 해당하는 값을 favorites 맵에서 꺼냅니다. 이 객체가 바로 반환해야할 객체가 맞지만, 잘못된 컴파일타임 타입을 가지고 있습니다. 이 객체의 타입은 값 타입인 Object이나 우리는 이를 T로 바꿔 반환해야 합니다.
  • 따라서 getFavorites 구현은 Class와 cast 메서드를 사용해 이 객체 참조를 Class객체가 가리키는 타입으로 동적 형변환합니다.

💡 cast 메서드 사용 이유

  • cast 메서드는 형변환 연산자의 동적 버전입니다. 이 메서드는 단순히 주어진 인수가 Class객체가 알려주는 타입의 인스턴스인지 검사한 다음 맞다면 그 인수를 그대로 반환하고 아니면 ClassCastException을 발생시킵니다. 그런데 favorites 맵 안의 값은 해당 키의 타입과 항상 일치하기 때문에 클라이언트 코드가 항상 깔끔히 컴파일됨을 알 수 있습니다. 따라서 cast 메서드가 ClassCastException을 던질 일은 없습니다.
  • cast 메서드는 단지 인수를 그대로 반환하기만 하는데 왜 사용할까요? 그 이유는 cast 메서드의 시그니처가 Class 클래스가 제네릭이라는 이점을 활용하기 위해서 입니다.
@SuppressWarnings("unchecked")
@HotSpotIntrinsicCandidate
public T cast(Object obj) {
    if (obj != null && !isInstance(obj))
        throw new ClassCastException(cannotCastMsg(obj));
    return (T) obj;
}

 

💡 제약사항

  • Favorites 클래스에는 두 가지 제약사항이 있습니다.
  • 클라이언트에서 Class 객체를 로 타입(raw type)으로 넘기면 Favorites 인스턴스의 타입 안정성이 쉽게 깨집니다. (물론 컴파일시에 비검사 경고가 뜹니다.)
    • putFavorites에서 instance의 타입이 type과 같은 타입인지 확인해서 타입 불변식을 지킬수 있습니다.
public <T> void putFavorites(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
  • 실체화 불가 타입에는 사용할 수 없습니다.
    • List<String>, List<Integer>같은 실체화 불가 타입은 쓸 수 없다는 의미입니다. List<String>.class를 얻을 수 없기 때문입니다. List<String>, List<Integer> 둘 다 List.class를 공유하기에 실체화 불가 타입이 허용되면 문제가 심각해집니다.

💡 정리

  • 컬렉션 API로 대표되는 일반적인 제네릭 형태에는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있습니다.
  • 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너로 만들 수 있습니다.
  • 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 합니다.

 

 

 

728x90
반응형