Skip to content

Latest commit

 

History

History
130 lines (113 loc) · 6.41 KB

타입_안전_이종_컨테이너를_고려하라_초롱.md

File metadata and controls

130 lines (113 loc) · 6.41 KB

타입 안전 이종 컨테이너

타입 안전 이종 컨테이너란

  • 타입 안전 이종 컨테이너는 영어로 type safe heterogeneous container이다.
    • heterogeneous여러 다른 종류들로 이뤄진 이라는 뜻이다.
  • 즉, 타입 안전 이종 컨테이너란 여러 다른 종류들로 이루어진 값을 저장하는 타입 안전한 객체를 의미한다.

타입 안전 이종 컨테이너 패턴

  • 제네릭은 컬렉션과 단일원소 컨테이너에 흔히 쓰인다.
  • 이 때 매개변수화되는 대상은 원소가 아닌 컨테이너 자신이다.
    • 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.
    • ex) Set, Map<String, Integer>
  • 하지만 유연한 수단이 필요할 때도 있다.
    • ex) 데이터베이스의 모든 열을 타입 안전하게 이용하고 싶다.
    • 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공한다.

타입 안전 이종 컨테이너 패턴 예제

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));
    }
}

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());
}

Class<?>

  • 각 타입의 Class 객체를 매개변수화한 키로 사용하며, 이러한 Class 객체를 타입 토큰이라고 한다.
    • 타입 토큰 : 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴
  • 키 타입에 와일드카드 타입을 사용하여, 모든 키가 서로 다른 매개변수화 타입일 수 있다.

Object

  • 키와 값 사이의 타입 관계를 보증하지 않는다.
  • 하지만 우리는 이 관계가 성립함을 알고 있다.

pubFavorite

  • 주어진 Class 객체와 즐겨찾기 인스턴스를 favorites에 추가하여 관계를 짓는다.
  • 이 때 키와 값 사이의 '타입 링크' 정보는 버려진다.
    • 즉, 그 값이 그 키 타입의 인스턴스라는 정보가 사라진다.

getFavorite

  • 주어진 Class 객체에 해당하는 값을 favorites 맵에서 꺼낸다.
  • 이 객체가 반환해야할 타입 객체는 맞지만, 잘못된 컴파일타임 타입(Object)를 가지고 있어 T 타입으로 바꿔야 한다.
  • Class의 cast() 메서드를 사용해 객체 참조를 Class 객체가 가리키는 타입으로 동적 형변환한다.

cast 메서드

  • cast 메서드의 시그니처가 Class 클래스가 제네릭이라는 이점을 완벽히 활용한다.
  • cast의 반환 타입은 Class 객체의 타입 매개변수와 같다.
public class Class<T> {
    @SuppressWarnings("unchecked")
    public T cast(Object obj) {
        if (obj != null && !isInstance(obj))
            throw new ClassCastException(cannotCastMsg(obj));
        return (T) obj;
    }
}
  • 따라서 Favorites를 T로 비검사 형변환하지 않고도 타입 안전하게 만들 수 있다.

Favorites 클래스의 제약 사항

  1. Class 객체를 제네릭이 아닌 로 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다.
    • 예를 들어 아래의 코드는 동작한다.
     HashSet<Integer> tmp = new HashSet<>();
     ((HashSet)tmp).add("하이");
    • 해당 문제는 동적 형변환을 통해 해결할 수 있다.
  2. 실체화 불가 타입은 사용할 수 없다.
    • 예를 들어 String 이나 String[ ]은 저장할 수 있어도 즐겨찾는 List은 저장할 수 없다.
    • List용 Class 객체를 얻을 수 없기 때문이다.
    • 이 제약에 대한 완벽한 우회로는 없다.

동적 형변환

  • 동적 형변환을 통해 인수로 주어진 인스턴스의 타입이 type으로 명시한 타입과 같은지 확인하면 된다.
public <T> void putFavorite(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), type.cast(instance));
}

한정적 타입 토큰

  • Favorites가 사용하는 타입 토큰은 비한정적이다.
    • 즉 getFavorite과 putFavorite은 어떤 Class든 받아들인다.
  • 때로는 이 메서드들이 허용하는 타입을 제한하고 싶을 수 있는데, 이 때는 한정적 타입 토큰을 사용하면 된다.
  • 한정적 타입 토큰이란, 한정적 타입 매개변수한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입 토큰이다.
  • 어노테이션 API는 한정적 타입 토큰을 적극적으로 사용한다.
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
  • annotationType 인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰이다.
  • 애너테이션된 요소는 키가 애너테이션 타입인 타입 안전 이종 컨테이너다.

Q. Class<?> 타입의 객체가 있고, 이를 한정적 타입 토큰을 받는 메서드에 넘기려면 어떻게 해야 할까?

  • 메서드에 넘기기 위해서는 객체를 Class<? extends Annotation>으로 형변환해야 한다.
  • 운 좋게도, Class 클래스는 이런 형변환을 안전하고 동적으로 수행해주는 메서드를 제공한다.
  • asSubclass 메서드
    • 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환한다.
    • 형변환에 성공하면 인수로 받은 클래스 객체를 반환하고, 실패하면 `ClassCastException을 던진다.
static Annotation getAnnotation(AnnotatedElement element,
                                String annotationTypeName) {
    Class<?> annotationType = null; // 비한정적 타입 토큰
    try {
        annotationType = Class.forName(annotationTypeName);
    } catch (Exception ex) {
        throw new IllegalArgumentException(ex);
    }
    return element.getAnnotation(
            annotationType.asSubclass(Annotation.class));
}