Study/Effective Java
아이템 33. 타입 안전 이종 컨테이너를 고려하라
공29
2024. 12. 22. 19:17
제네릭은 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.
하지만 더 유연한 수단이 필요하다고 해보자. 컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면 된다. 이러한 설계 방식을 타입 안전 이종 컨테이너 패턴(type safe heterogeneous container pattern)이라고 한다.
타입 안전하지 않은 예제 먼저 보자.
public class Favorites {
private Map<Class, Object> map = new HashMap<>();
public void put(Class clazz, Object value) {
this.map.put(clazz, value);
}
public Object get(Class clazz) {
return this.map.get(clazz);
}
public static void main(String[] args) {
// Type Safety 하지 않다.
Favorites favorites = new Favorites();
favorites.put(String.class, "keesun");
favorites.put(String.class, 1);
favorites.put(Integer.class, 2);
favorites.put(Integer.class, "2");
}
}
class 리터럴 타입은 Class가 아닌 Class<T>다. 즉 제네릭이다.
예컨대 String.class의 타입은 Class<String> 이고 Integer.class의 타입은 Class<Integer>이다.
그러므로, 타입 안전하도록 아래처럼 작성할 수 있다.
public class Favorites {
private Map<Class<?>, Object> map = new HashMap<>(); // 비한정적 와일드카드
public <T> void put(Class<T> clazz, T value) {
// this.map.put(clazz, value);
// 넣기 전에 체크하는 코드 추가.
this.map.put(Objects.requireNonNull(clazz), value);
}
public <T> T get(Class<T> clazz) {
// put 메서드에서 이미 해당하는 타입 T로만 받아오기 때문에, 안전하다고 믿고 (T) 형변환을 해도 안전하다. @SuppressWarning("unchecked")...
// return (T) this.map.get(clazz);
// 위 보다 더 안전한 방법. 검사를 하고 형변환을 한다!
return clazz.cast(this.map.get(clazz));
}
public static void main(String[] args) {
Favorites favorites = new Favorites();
// 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고 받는 class 리터럴을 타입 토큰(type token)이라고 한다.
favorites.put(String.class, "keesun");
// favorites.put(String.class, 1); // compile error
favorites.put(Integer.class, 2);
// favorites.put(Integer.class, "2"); // compile error
String str = favorites.get(String.class);
Integer num = favorites.get(Integer.class);
}
}
위 Favorites 클래스에는 두 가지 제약이 있다.
1) 악의적인 클라이언트가 Class 객체를 제네릭이 아닌 로 타입으로 넘기면 (아이템 26) 타입 안전성이 쉽게 깨진다.
2) 실체화 불가 타입 (아이템 28)에는 사용할 수 없다.
▼ 예제 코드
public class Favorites {
private Map<Class<?>, Object> map = new HashMap<>(); // 비한정적 와일드카드
public <T> void put(Class<T> clazz, T value) {
// this.map.put(clazz, value);
// 넣기 전에 체크하는 코드 추가.
// Raw 타입으로 바꿨을 때 타입 안전성이 깨지는 것은 막을 수 없지만
// 동적 형변환을 사용하여 put 시점에 타입을 확인할 수 있다.
this.map.put(Objects.requireNonNull(clazz), clazz.cast(value));
}
public <T> T get(Class<T> clazz) {
// put 메서드에서 이미 해당하는 타입 T로만 받아오기 때문에, 안전하다고 믿고 (T) 형변환을 해도 안전하긴하다. @SuppressWarning("unchecked")...
// return (T) this.map.get(clazz);
// 위 보다 더 안전한 방법. 검사를 하고 형변환을 한다! 비검사 형변환하는 손실 없이도 타입 안전하게 만드는 비결이다.
return clazz.cast(this.map.get(clazz));
}
public static void main(String[] args) {
Favorites favorites = new Favorites();
// 단점 1. Raw 타입으로 바꿔주면 타입 안전성을 깰 수 있다.
favorites.put((Class) String.class, 1); // compile error가 발생하지 않음.
String str1 = favorites.get(String.class);
// 단점 2. List의 타입을 정할 class 리터럴이 없어서 구분할 수 없다. 아래 코드는 문법이 허용하지 않는다.
// Super Type Token으로 해결하려는 시도가 있지만 완벽히 만족스러운 우회로는 아니다.
// favorites.put(List<Integer>.class, List.of(1, 2, 3));
// favorites.put(List<String>.class, List.of("a", "b", "c"));
// List list = favorites.get(List.class);
// list.forEach(System.out::println);
}
}
타입을 제한하고 싶다면, 한정적 타입 토큰을 활용하라.