이펙티브 자바, 쉽게 정리하기 - item 33. 타입 안전 이종 컨테이너를 고려하라
타입 안전 이종 컨테이너란?
static class Favorites {
private final 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));
}
}
- 하나의 프로그래밍 패턴이다.
Map
에서key
를 특정 클래스로 받고,value
를Object
의 형태로 받는다.- 데이터를 넣을 때,
key
의 클래스 타입과instance
의 타입이 동일해야 하므로, 타입 안전이 보장된다.- 특정 클래스 타입의
key
를 불렀을때 해당 타입의 인스턴스가 나올 것이라 예측 가능하다.
- 특정 클래스 타입의
- 이 방식이 동작하는 이유는
Class
클래스가 기본적으로 제네릭 타입을 받는 클래스기 때문이다.- 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴을 타입토큰이라 한다.
Map<Class<?>, Object>
에서는 와일드카드가 중첩되어 맵이 아니라key
가 와일드 타입임을 인지해야 한다.- 그래서 모든 키가 서로 다른 매개변수화 타입일 수 있다는 의미가 된다.
흔히 제네릭 타입을 쓰는 클래스들은 매개변수화 대상을 컨테이너 자신으로 하는 반면, 타입 안전 이종 컨테이너는 매개변수화 대상을 키로 사용하는
Class
에 둔다.
클라이언트 코드 작성해보기
@Test
public void favoriteTest() {
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());
}
출력 결과
Java cafebabe item33.Item33test$Favorites
Favorites
클래스의 제약
제약 1: 클라이언트의 악의적인 로 타입 사용
- 악의적인 클라이언트가
Class
객체를 제네릭이 아닌 로 타입으로 넘기면Favorites
인스턴스의 타입 안전성이 쉽게 깨진다.- 하지만, 컴파일할 때 비검사 경고가 뜰 것이다.
f.putFavorite((Class) List.class, "난 리스트가 아닌데");
- 위와 같이 악의적으로
(Class)
와 같은 캐스팅을 통해 로타입을 사용하는 것에 대한 취약점이 있다. - 그냥 실행하면, 런타임에
ClassCastException
을 만나게 될 것이다.
제약 2: 실체화 불가 타입에는 사용할 수 없다
List<String>.class
와 같이 실체화 되지 않는 타입에는 사용 불가능하다.
슈퍼 타입 토큰
위의 제약을 해결하려 슈퍼 타입 토큰(super type token)을 사용하려는 시도도 있었다. 스프링에서는 ParameterizedTypeReference
라는 클래스로 미리 구현해놓았다.
Favirotes f = new Favorites();
List<String> pets = Arrays.asList("개", "고양이", "앵무");
f.putFavorite(new TypeRef<List<String>>(){}, pets);
List<String> listofStrings = f.getFavorite(new TypeRef<List<String>>(){});
위와 같은 코드를 사용하여 해결한다.
한정적 타입 토큰을 이용한 타입 제한
Favorites
가 사용하는 타입 토큰은 기본적으로 비 한정적이다. 한정적 타입 매개변수나 한정적 와일드카드를 이용하여 표현 가능한 타입을 제한하는 것을 한정적 타입 토큰이라고 한다.- 애너테이션 API에서는 이를 적극 활용한다.
- 애너테이션 API는 대상 요소에 달려있는 애너테이션을 런타임에 읽어오는 기능을 한다.
- 아래는 애너테이션 API인
AnnotatedElement
코드 예이다.
public <T extends Annotation>
T getAnnotation(Class<T> annotationType);
annotationType
인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰이다.- 이 메서드에서는 토큰으로 명시한 타입의 애너테이션이 해당 요소에 달려있다면 애너테이션을 반환하고 없으면
null
을 반환한다. - 애너테이션된 요소는 그 키가 애너테이션 타입인 타입 안전 이종 컨테이너인 것이다.
한정적 타입 토큰을 받는 메서드에 Class<?>
타입의 객체를 넘기는 법
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));
}
핵심 정리
- 키를 타입 매개변수로 설정하면, 타입 안전 이종 컨테이너를 만들 수 있다.
Class
를 키로 쓰며, 이렇게 쓰이는Class
객체를 타입 토큰이라 한다.- 직접 구현한 키 타입도 사용 가능하다.
- 데이터베이스의 행을 표현한
DatabaseRow
타입에는 제네릭 타입인Column<T>
를 키로 사용할 수 있다.
반응형
'Java > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바, 쉽게 정리하기 - item 35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2022.02.24 |
---|---|
이펙티브 자바, 쉽게 정리하기 - item 34. int 상수 대신 열거 타입을 사용하라 (0) | 2022.02.24 |
이펙티브 자바, 쉽게 정리하기 - item 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) | 2022.01.24 |
이펙티브 자바, 쉽게 정리하기 - item 31. 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) | 2022.01.24 |
이펙티브 자바, 쉽게 정리하기 - item 30. 이왕이면 제네릭 메서드로 만들라 (0) | 2022.01.09 |