on
[교재 EffectiveJava] 아이템 33. 타입 안전 이종 컨테이너를 고려하라
[교재 EffectiveJava] 아이템 33. 타입 안전 이종 컨테이너를 고려하라
728x90
이종 컨테이너 패턴 (type safe heterogeneous container pattern)
제네릭은 Set, Map 등의 컬렉션과 ThreadLocal, AtomicReference 등의 단일원소 컨테이너도 흔히 쓰인다. 이런 모든 쓰임에서 매개변수화되는 대상은 원소가 아닌 컨테이너 자신이다. 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다. 예를들어, Set 에는 원소의 타입을 뜻하는 단 하나의 타입 매개변수만 있으면 되며, Map 에는 key, value의 타입을 뜻하는 2개만 필요한 식이다.
더 유연한 방식이 필요할때가 있다. 데이터베이스희 행(row)은 임의 개수의 열(column)을 가질 수 있는데, 모두 열을 타입 안전하게 이용할 수 있다면 더 편할 것이다. 여기에 쉬운 해법이 있다.
컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄때 매개변수화한 키를 함께 제공하면 된다.
이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해 줄 것이다.
예시
타입별로 즐겨찾는 인스턴스를 저장하고 검색할 수 있는 Favorites 클래스를 생각해보자. 각 타입의 Class 객체를 매개변수화한 키 역할로 사용하면 된다. 이 방식이 동작하는 이유는 class의 클래스가 제네릭이기 때문이다. class 리터럴의 타입은 Class 가 아닌 Class이다.
* 타입 토큰
컴파일타임 타입 정보과 런타임 타입 정보를 알아내기 위해 메서드를 주고받는 class 리터럴
Facorites.java API
public class Favorites { public void putFavorite(Class type, T instance); public T getFavorite(Class 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, Object> favorites = new HashMap<>(); public void putFavorite(Class type, T instance) { favorites.put(Objects.requireNonNull(type), instance); } public T getFavorite(Class 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, Object> favorites = new HashMap<>();
Map, Object> 타입을 보고 '비한정적 와일드 카드 타입'이라 이 맵 안에 아무것도 넣을 수 없다고 생각하겠지만, 와일드카드 타입이 중첩되었다는 걸 알아야한다. 맵이 아니라 키가 와일드카드 타입인 것이다. 이는 모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻으로, 다양한 타입을 지원할 수 있다.
favorites 맵의 값 타입은 단순히 Object 이다. 이는 key, value 사이에 타입 관계를 보증하지 않는다는 것이다.
public void putFavorite(Class type, T instance) { favorites.put(Objects.requireNonNull(type), instance); }
주어진 Class 객체와 instance 를 favorites 에 각 key, value 로 추가했다. 해당 value가 그 키 type의 인스턴스라는 정보가 사라진다.
public T getFavorite(Class type) { return type.cast(favorites.get(type)); }
주어진 Class 객체에 해당하는 값을 맵에서 꺼낸다. 이 객체가 바로 반환해야할 객체지만, 잘못된 컴파일 타임 타입을 가지고있다. 이 객체의 타입은 Object 이나, 우리는 이를 T로 바꿔서 반환해야한다. 따라서 getFavorite 구현은 Class 의 cast 메서드를 사용해 이 객체 참조를 Class 객체가 가리키는 타입으로 동적 형변환한다.
public class Class { 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, Object> favorites = new HashMap<>(); public void putFavorite(Class type, T instance) { favorites.put(Objects.requireNonNull(type), type.cast(instance)); } public T getFavorite(Class type) { return type.cast(favorites.get(type)); } }
type.cast(instance) 를 사용하여 동적 형변환을 사용했다. 이는 type, instance 의 타입이 같은지를 확인해준다.
2) 실체화 불가 타입에는 사용할 수 없다.
String, String[]을 저장할수는 있어도, List은 저장할 수 없다. 이는 컴파일 오류를 발생시킨다. List용 Class 객체를 얻을 수 없기 때문이다. List.class 는 오류가 발생한다. List 과 List은 List.class 라는 같은 Class 객체를 공유한다. 해당 제약의 해결방안은 없다.
한정적 타입 토큰
Favorites 클래스가 사용하는 타입 토큰은 비한정적이다. 타입 토큰을 다시 상기시켜보자.
* 타입 토큰
컴파일타임 타입 정보과 런타임 타입 정보를 알아내기 위해 메서드를 주고받는 class 리터럴
getFavorite, putFavorite 메서드는 어떤 Class 객체든 받아들이는데, 이 메서드들이 허용하는 타입을 제한하고 싶은 경우가 있다. 이때 한정적 타입 토큰을 사용하면 된다.
* 한정적 타입 토큰
단순히 한정적 타입 매개변수나 한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입 토큰
public T getAnnotation annotationType);
annotationType 인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰이다. 이 메서드는 토큰으로 명시한 타입의 어노테이션이 대상 요소에 달려있다면 그 어노테이션을 반환하고, 없다면 null을 반환한다.
ClasS 타입의 객체가 있고, 이를 한정적 타입 토큰을 받는 메서드에 넘기려면 어떻게 해야할까?
객체를 Class 으로 형변환할 수도 있지만, 이 형변환은 비검사이므로 컴파일하면 경고가 뜰 것이다. 여기서 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)); }
위 코드는 오류나 경고없이 컴파일된다.
from http://devfunny.tistory.com/585 by ccl(A) rewrite - 2021-11-04 13:01:53