Search
🙉

이펙티브 자바:: 아이템 33 <타입 안전 이종 컨테이너를 고려하라>

Intro::

이펙티브 자바 정리본입니다.

결론

컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되는 제약을 극복하는 타입 안전 이종 컨테이너를 만들 수 있습니다.
타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라고 합니다.

이종 컨테이너 패턴

컨테이너 대신 키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하는 방식입니다. 이렇게 하면 제네릭 타입 시스템이 값의 타입이 키와 같음을 보장해줄 것입니다.
// 타입 안전 이종 컨테이너 패턴 (199-202쪽) public class Favorites { // 코드 33-3 타입 안전 이종 컨테이너 패턴 - 구현 (200쪽) private Map<Class<?>, Object> favorites = new HashMap<>();// (1) 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));// (2) // return (T) favorites.get(type); 해당 방식은 unchecked cast } // // 코드 33-4 동적 형변환으로 런타임 타입 안전성 확보 (202쪽) // public <T> void putFavorite(Class<T> type, T instance) { // favorites.put(Objects.requireNonNull(type), type.cast(instance)); // } // 코드 33-2 타입 안전 이종 컨테이너 패턴 - 클라이언트 (199쪽) 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()); } }
Java
복사
Class의 클래스가 제네릭이기 때문에 각 타입의 Class 객체를 매개변수화한 키 역할로 사용하면 됩니다.한편, 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴을 타입 토큰이라고 합니다.
Favorites 인스턴스는 타입 안전합니다. String을 요청했는데 Integer를 반환하는 일은 절대 없습니다. 또한 모든 키의 타입이 제각각이라, 일반적인 맵과 달리 여러 가지 타입의 원소를 담을 수 있습니다. 따라서 Favorite은 타입 안전 이종 컨테이너라고 할 만 합니다.
(1) 을 확인해 보면 비한정적 와일드카드 타입이라 맵 안에 아무것도 넣을 수 없다고 생각할 수 있지만, 와일드카드 타입이 중첩되었다는 점을 깨달아야 합니다. 즉, 맵이 아니라 키가 와일드카드 타입인 것입니다. 이는 모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻으로 이종을 지원한다는 의미 입니다.
또한 (1)에서 favorites 맵의 값 타입은 단순히 Object라는 것입니다. 이 맵은 키와 값 사이의 타입 관계를 보증하지 않습니다. 즉, 모든 값이 키로 명시한 타입임을 보증하지 않습니다. 사실 자바의 타입 시스템에서는 이 관계를 명시할 방법이 없습니다. 하지만 우리는 이 관계가 성립함을 알고 있고, 즐겨찾기를 검색할 때 그 이점을 누리게 됩니다.

cast를 쓰는 이유

public T cast(Object obj) { if (obj != null && !isInstance(obj)) throw new ClassCastException(cannotCastMsg(obj)); return (T) obj; }
Java
복사
(2)에서 cast 메서드는 형변환 연산자의 동적 버전입니다. cast 자체가 Class<T> 에서의 T를 그대로 반환하기 때문에 타입을 그대로 보장해주게 됩니다. 이말인즉슨, Class<String>에서 cast를 하면 반환값이 String으로 보장되어 unchecked cast, 즉 비검사 형변환 경고가 발생하지 않는다는 말입니다.

제약 사항

1.
악의적인 클라이언트가 Class 객체(제네릭이 아닌) 로 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다.
a.
하지만 해당 클라이언트 코드는 비검사 경고가 뜰 것이다.
b.
HashSet과 HashMap 등의 일반 컬렉션 구현체에도 똑같은 문제가 있다.
c.
이 정도의 문제를 감수하겠다면 런타임 타입 안전성을 얻을 수 있습니다.
d.
Favorites가 타입 불변식을 어기는 일이 없도록 보장하려면 putFavorite 메서드에서 인수로 주어진 instance의 타입이 type으로 명시한 타입과 같은지 동적 형변환 방식으로 체크하면 됩니다.
public <T> void putFavorite(Class<T> type, T instance) { favorites.put(Objects.requireNonNull(type), type.cast(instance)); }
Java
복사
e.
java.util.Collections에 checkedSet, checkedList, checkedMap 같은 메서드가 동적 형변환 방식을 적용한 컬렉션 래퍼들입니다.
2.
실체화 불가 타입에는 사용할 수 없습니다.
a.
즐겨찾는 String이나 String[]은 저장할 수 있지만, List<String>을 저장하려는 코드는 컴파일되지 않습니다.
b.
List<String>이던 List<Integer>던 List.class 라는 같은 클래스 객체를 공유하기 때문에 똑같은 타입의 객체 참조를 반환한다면 의도치 않은 동작을 할 수 있습니다.
c.
이는 슈퍼 타입 토큰으로 일부 해결할 수 있지만, 완벽한 해결방법은 아니라고 합니다.

질문

Class<?> 타입의 객체가 있고, 이를 한정적 타입 토큰을 받는 메서드에 넘길때, 객체를 Class<? extends Annotation>으로 형변환할 수 있지만, 비검사이므로 컴파일 에러가 뜬다는게 이해가 안간다

1. Class<?>Class<? extends Annotation>의 차이

Class<?>: 이는 모든 타입Class 객체를 나타냅니다. 즉, Class<?>String.class, Integer.class, Annotation.class 등 어떤 타입이든 담을 수 있습니다.
Class<? extends Annotation>: 이는 Annotation의 하위 타입만을 가리키는 Class 객체를 의미합니다. 즉, Class<? extends Annotation>Override.class, Deprecated.class, 등 애노테이션 타입만을 허용합니다.

2. 형변환의 문제점: 타입 소거(Type Erasure)

자바의 제네릭은 컴파일 시점에만 타입 정보를 활용하며, 런타임에는 타입 정보가 지워집니다(타입 소거). 이로 인해 형변환 시 컴파일러는 타입을 정확히 확인할 수 없어서 비검사 형변환 경고가 발생하게 됩니다.
예를 들어, Class<?>는 모든 타입의 클래스 객체를 가리킬 수 있기 때문에, 애노테이션이 아닌 다른 타입의 클래스 객체Class<?>로 나타낼 수 있습니다. 그러나 Class<? extends Annotation>은 오직 애노테이션 타입에 대해서만 허용되므로, 컴파일러는 Class<?>가 애노테이션 타입을 보장하지 않기 때문에 비검사 형변환을 경고합니다.

3. 비검사 형변환이 발생하는 이유

컴파일러가 Class<?>Class<? extends Annotation>으로 변환할 때, 다음과 같은 상황이 발생할 수 있습니다:
Class<?> clazz = String.class; // String.class는 애노테이션이 아님 Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) clazz; // 비검사 형변환 경고 발생
Java
복사
여기서 clazzString.class일 수 있지만, Class<? extends Annotation>으로 형변환을 시도하고 있습니다. 이는 애노테이션 타입이 아닌 Class<?> 객체가 형변환될 가능성이 있기 때문에, 컴파일러가 이를 경고합니다.
즉, Class<?>는 모든 타입을 수용하므로 String.class 같은 비애노테이션 타입이 포함될 수 있지만, Class<? extends Annotation>는 오직 애노테이션 타입만을 허용하기 때문에 런타임에 문제가 생길 가능성이 있다고 컴파일러가 판단합니다.

4. 요약

Class<?>는 모든 타입의 Class 객체를 수용할 수 있는 일반적인 타입이므로, 애노테이션이 아닌 타입도 포함될 수 있습니다.
Class<? extends Annotation>애노테이션 타입에만 한정되므로, Class<?>에서 이를 형변환하려고 하면 비검사 형변환 경고가 발생합니다.
이는 자바의 제네릭 타입 소거 때문에 런타임에 타입 정보가 유지되지 않기 때문에 발생하는 경고입니다.
형변환이 안전한지 확인하기 위해서는 instanceofisAnnotation() 같은 런타임 타입 검사를 통해 안전성을 확인한 후 형변환을 해야 합니다.

References::

이펙티브 자바 / 조슈아 블로크 지음 (프로그래밍 인사이트)