[교재 EffectiveJava] 아이템 33. 타입 안전 이종 컨테이너를 고려하라

반응형
728x90
반응형

이종 컨테이너 패턴 (type safe heterogeneous container pattern)

제네릭은 Set<E>, Map<K, V> 등의 컬렉션과 ThreadLocal<T>, AtomicReference<T> 등의 단일원소 컨테이너도 흔히 쓰인다. 이런 모든 쓰임에서 매개변수화되는 대상은 원소가 아닌 컨테이너 자신이다. 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다. 예를들어, Set에는 원소의 타입을 뜻하는 단 하나의 타입 매개변수만 있으면 되며, Map 에는 key, value의 타입을 뜻하는 2개만 필요한 식이다. 

 

더 유연한 방식이 필요할때가 있다. 데이터베이스희 행(row)은 임의 개수의 열(column)을 가질 수 있는데, 모두 열을 타입 안전하게 이용할 수 있다면 더 편할 것이다. 여기에 쉬운 해법이 있다. 

컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄때 매개변수화한 키를 함께 제공하면 된다.

이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해 줄 것이다. 

 

 

예시

타입별로 즐겨찾는 인스턴스를 저장하고 검색할 수 있는 Favorites 클래스를 생각해보자. 각 타입의 Class 객체를 매개변수화한 키 역할로 사용하면 된다. 이 방식이 동작하는 이유는 class의 클래스가 제네릭이기 때문이다. class 리터럴의 타입은 Class 가 아닌 Class<T>이다.

* 타입 토큰
컴파일타임 타입 정보과 런타임 타입 정보를 알아내기 위해 메서드를 주고받는 class 리터럴

 

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

 

Favorites.java
package com.java.effective.item33;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

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 호출 main 메서드
package com.java.effective.item33;

public class Main {
    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);

        // Java, cafebabe, class com.java.effective.item33.Favorites
        System.out.printf("%s, %x, %s%n", favoriteString, favoriteInteger, favoriteClass);
    }
}

 

Favorites 인스턴스는 타입 안전하다. String 을 요청했는데, Integer 을 반환할 일은 절대 없다. 또한 모든 키의 타입이 제각각이라, 일반적인 맵과 달리 여러가지 타입의 원소를 담을 수 있다. Favorites 는 타입 안전 이종(heterogeneous) 컨테이너라 할만하다.

private Map<Class<?>, Object> favorites = new HashMap<>();

 

Map<Class<?>, Object> 타입을 보고 '비한정적 와일드 카드 타입'이라 이 맵 안에 아무것도 넣을 수 없다고 생각하겠지만, 와일드카드 타입이 중첩되었다는 걸 알아야한다. 맵이 아니라 키가 와일드카드 타입인 것이다. 이는 모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻으로, 다양한 타입을 지원할 수 있다.

 

favorites 맵의 값 타입은 단순히 Object 이다. 이는 key, value 사이에 타입 관계를 보증하지 않는다는 것이다. 

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

 

주어진 Class 객체와 instance 를 favorites 에 각 key, value 로 추가했다. 해당 value가 그 키 type의 인스턴스라는 정보가 사라진다.

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

 

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

public class Class<T> {
    T cast(Object obj);
}

 

cast 메서드의 반환 타입은 Class 객체의 타입 매개변수와 같다. 덕분에 T로 비검사 형변환하는 손실 없이도 Facorites 를 타입 안전하게 만들 수 있었다.

 

 

Facorites 클래스의 제약

1) 악의적인 클라이언트가 Class 객체를 제네릭이 아닌 로 타입으로 넘기면 타입안정성이 깨진다.
package com.java.effective.item33;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

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

 

type.cast(instance) 를 사용하여 동적 형변환을 사용했다. 이는 type, instance 의 타입이 같은지를 확인해준다.

 

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

String, String[]을 저장할수는 있어도, List<String>은 저장할 수 없다. 이는 컴파일 오류를 발생시킨다. List<String>용 Class 객체를 얻을 수 없기 때문이다. List<String>.class 는 오류가 발생한다. List<String> 과 List<Integer>은 List.class 라는 같은 Class 객체를 공유한다. 해당 제약의 해결방안은 없다.

 

 

한정적 타입 토큰

Favorites 클래스가 사용하는 타입 토큰은 비한정적이다. 타입 토큰을 다시 상기시켜보자.

* 타입 토큰
컴파일타임 타입 정보과 런타임 타입 정보를 알아내기 위해 메서드를 주고받는 class 리터럴

getFavorite, putFavorite 메서드는 어떤 Class 객체든 받아들이는데, 이 메서드들이 허용하는 타입을 제한하고 싶은 경우가 있다. 이때 한정적 타입 토큰을 사용하면 된다.

* 한정적 타입 토큰
단순히 한정적 타입 매개변수나 한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입 토큰
public <T extends Annotation> T getAnnotation<Class<T> annotationType);

 

annotationType 인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰이다. 이 메서드는 토큰으로 명시한 타입의 어노테이션이 대상 요소에 달려있다면 그 어노테이션을 반환하고, 없다면 null을 반환한다.

 

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

객체를 Class<? extends Annotation> 으로 형변환할 수도 있지만, 이 형변환은 비검사이므로 컴파일하면 경고가 뜰 것이다. 여기서 Class 클래스가 제공하는 asSubClass 메서드를 사용하여 호출된 인스턴스 자신의 class 객체를 인수가 명시한 클래스로 형변환할 수 있다. (이 클래스가 인수로 명시한 클래스의 하위 클래스라는 뜻이다.)

static Annotation getAnnotation(AnnotationElement 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));
}

 

asSubclass()
public <U> Class<? extends U> asSubclass(Class<U> clazz) {
    if (clazz.isAssignableFrom(this))
        return (Class<? extends U>) this;
    else
        throw new ClassCastException(this.toString());
}

 

위 코드는 오류나 경고없이 컴파일된다.

 

 

 

반응형

Designed by JB FACTORY