어려워요..꼭 다시 복습해야 하는 아이템
Class 객체
Java에서는 각 타입의 Class 객체를 key로 사용할 수 있다. 이 Class 객체는 제네릭으로 정의되어 있어서 타입에 따라 구체화될 수 있다.
예를 들어, String 클래스의 Class 객체는 Class<String> 타입을 갖게 되며, Integer 클래스의 Class 객체는 Class<Integer> 타입을 갖게 된다.
이렇게 Class 객체를 매개변수화한 키로 사용할 수 있는 이유는 Class 클래스 자체가 제네릭으로 정의되어 있기 때문이다. Class 클래스는 타입 매개변수 T를 가지고 있어서 실제 타입에 따라서 Class<T> 형태로 사용된다.
이러한 Class<T> 타입의 객체를 매개변수로 받는 메소드를 정의할 수 있다. 이 메소드는 특정 타입의 Class 객체를 전달받아 해당 클래스에 대한 작업을 수행할 수 있다.
public static void printClassName(Class<?> c) {
System.out.println(c.getName());
}
public static void main(String[] args) {
Class<?> myClass = Integer.class;
printClassName(myClass); // Integer 클래스의 이름 출력
}
위의 메소드는 Class<?> 타입의 객체를 매개변수로 받아서 해당 클래스의 이름을 출력한다. 이렇게 매개변수화된 Class 객체를 사용하여 원하는 작업을 할 수 있다.
이 방식은 Java에서 리플렉션(Reflection)과 관련된 작업에서 많이 활용된다. 리플렉션을 사용하면 실행 중에 클래스의 정보를 조사하고, 객체를 동적으로 생성하거나 메소드를 호출하는 등의 작업을 할 수 있다.
즉, Class<T> 타입을 사용하면 제네릭한 방식으로 타입에 따라 구체화된 Class 객체를 키로 사용할 수 있다. 이를 활용하여 타입 안정성을 유지하고, 클래스에 대한 작업을 수행할 수 있다.
타입 토큰(type token)
타입 토큰은 컴파일 타임과 런타임에서 타입 정보를 알아내기 위해 사용되는 Class 리터럴이다. Class 객체는 클래스의 메타 데이터를 포함하고 있으며, 타입 토큰은 이러한 Class 객체를 활용하여 타입 정보를 전달하고 조작하는 데 사용된다.
Java에서는 컴파일 타임에 타입 정보가 지워지며, 런타임 시에는 객체의 타입 정보를 알아내기 어렵다. 그러나 Class 객체를 사용하면 객체의 타입 정보를 얻을 수 있다. 이때 Class 객체를 메소드에 전달하여 타입 정보를 전달하거나 활용하는데, 이를 타입 토큰이라고 한다.
타입 토큰은 다양한 상황에서 활용될 수 있다. 아래 예시를 보자.
1. 제네릭 타입에서 타입 정보를 전달하기 위해 사용
: 제네릭 클래스나 메소드를 정의할 때, 타입 매개변수에 해당하는 타입을 알아내기 위해 타입 토큰을 사용할 수 있다.
2. 리플렉션(Reflection)에서 타입 정보를 사용
: 리플렉션은 실행 중에 클래스의 정보를 조사하고, 객체를 동적으로 생성하거나 메소드를 호출하는 등의 작업을 수행할 수 있다. 이때 타입 토큰을 사용하여 클래스의 정보를 얻어오고 조작하는 데 활용된다.
3. 제네릭한 컬렉션에서 타입 안정성을 유지하기 위해 사용
: 컬렉션을 사용할 때 컴파일 타임과 런타임 타입 정보가 일치하지 않을 수 있다. 이때 타입 토큰을 활용하여 컬렉션에 저장할 타입 정보를 전달하고, @SuppressWarning하여 타입 안정성을 유지할 수 있다.
타입 토큰은 Class 객체를 통해 타입 정보를 전달하고 활용하는 메커니즘을 제공한다. 이를 통해 컴파일 타임과 런타임에서 타입 정보를 알아내고 활용할 수 있으며, 타입 안정성과 동적인 작업을 위한 유연성을 제공한다.
타입 안전 이(다를 이)종(씨 종) 컨테이너
타입 안전 이종 컨테이너는 말 그대로, 타입이 다른 원소를 타입 안전하게 담아주는 박스? 정도로 이해하면 된다.
타입 안전 이종 컨테이너 구현 방법
Java의 타입 안전 이종 컨테이너 패턴
[1] 컨테이너 대신 키를 매개변수화
[2] 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorites(Class<T> type, T instance) {
favorites.put(type, instance);
}
@SuppressWarnings("unchecked")
public <T> T getFavorites(Class<T> type) {
return (T) favorites.get(type);
}
}
public class Main {
public static void main(String[] args) {
Favorites favorites = new Favorites();
favorites.putFavorites(Integer.class, 1);
int integer = favorites.getFavorites(Integer.class);
favorites.putFavorites(String.class, "가나다");
System.out.println("Integer.class : " + integer);
System.out.println("String.class : " + favorites.getFavorites(String.class));
}
}
위 코드는 정상동작할까?
favorites.putFavorites((Class)Integer.class, 1); // 로타입으로 캐스팅
클라이언트 코드에서 로타입으로 캐스팅한다면, 런타임에서 타입 안정성이 보장되지 않는다(ClassCastException). 이를 해결하기 위해 Favorites 클래스의 메소드 내에 type.cast 메소드로 런타임에 타입 안정성을 제공할 수 있다.
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorites(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance)); // 수정
}
@SuppressWarnings("unchecked")
public <T> T getFavorites(Class<T> type) {
return type.cast( favorites.get(type)); // 수정
}
}
이렇게 수정할 경우, 클라이언트 코드에서 컴파일 에러가 발생하는 것이 아닌, Favorites 클래스 내에서 명시적으로 에러(ClassCastException)를 확인할 수 있다.
type.cast 메소드를 살펴보면, 동일한 클래스 타입인지 확인하는 코드를 볼 수 있다. 또한 cast 메소드에서 타입 안정성을 보장하고 있으니 Favorites 클래스에서 따로 어노테이션을 달아주지 않아도 된다.

즉, 정리하면 Class의 cast 메소드를 사용해 이 객체 참조를 Class 객체가 가르키는 타입으로 동적 형변환한다. cast 메소드는 형변환 연산자의 동적 버전이다. 단순히 주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지 검사한 후, 맞으면 인수를 그대로 반환하며, 아니면 ClassCastException을 던진다. cast의 반환 타입은 Class 객체의 타입 매개변수와 같다.
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorites(Class<T> type, T instance) {
favorites.put(Objects.requireNonNUll(type), instance);
}
@SuppressWarnings("unchecked")
public <T> T getFavorites(Class<T> type) {
return type.cast(favorites.get(type)); // cast 메소드
}
}
타입 안전 이종 컨테이너 한계
: 실체화 불가 타입에는 사용할 수 없다.
: 슈퍼 타입 토큰으로 어느정도 해결 가능
즉, String이나 String[]은 저장할 수 있어도 즐겨 찾는 List<String>은 저장할 수 없다. List<String>용 Class 객체를 얻을 수 없기 때문이다. List<String>.class라고 쓰면 문법 오류가 난다. List<String>과 List<Integer>는 List.class라는 같은 Class 객체를 공유하므로, 만약 List<String>.class와 List<Integer>.class를 허용해서 둘 다 똑같은 타입의 객체 참조를 반환한다면 망한다.
이를 해결하기 위한 방법으로 슈퍼 타입 토큰이 있으며, 스프링 프레임워크에선 이를 이용해 ParameterizedTypeReference 클래스를 구현해놓았지만 완벽한 해결법은 아니다.
한정적 타입 토큰
한정적 타입 매개변수나 한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입 토큰이다.
어노테이션 API(아이템 39)는 한정적 타입 토큰을 적극 사용한다. 예를 들어 다음은 AnnotatedElement 인터페이스에 선언된 메소드로, 대상 요소에 달려있는 어노테이션을 런타임에 읽어오는 기능을 한다. 이 메소드는 리플렉션의 대상이 되는 타입들, 즉 클래스, 메소드, 필드 같이 프로그램 요소를 표현하는 타입들에서 구현한다.
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
annotationType 인수는 어노테이션 타입을 뜻하는 한정적 타입 토큰이다. 이 메소드는 토큰으로 명시한 타입의 어노테이션이 대상 요소에 달려있다면 그 애노테이션을 반환하고, 없다면 null을 반환한다. 즉, 어노테이션된 요소는 그 키가 애노테이션 타입인, 타입 안전 이종 컨테이너인 것이다.
Class<?> 타입의 객체가 있고, 이를 한정적 타입 토큰을 받는 메소드에 넘기려면 어떻게 해야 할까? 객체를 Class<? extends Annotaion>으로 형변환할 수도 있지만, 이 형변환은 비검사이므로 컴파일 경고가 뜬다.(아이템 27) Class 클래스가 형변환을 안전하게 동적으로 수행하는 인스턴스 메소드인 asSubclass를 제공한다. 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환한다. 형변환된다는 것은 이 클래스가 인수로 명시한 클래스의 하위 클래스라는 의미다. 즉, 형변환에 성공하면 인수로 받은 클래스 객체를 반환하고, 실패하면 ClassCastException을 던진다.
컴파일 시점에는 타입을 알 수 없는 어노테이션을 asSubclass 메소드를 사용해 런타임에 읽어내는 예
// getAnnotation 메소드를 통해 주어진 AnnotatedElement 객체에서 특정 어노테이션을 가져오는 예시
static Annotation getAnnotation(AnnotatedElement element,String annotationTypeName) {
Class<?> annotationType = null; // 비한정적 타입 토큰
try {
annotationType = Class.forName(annotationTypeName); // annotationTypeName을 기반으로 어노테이션의 타입을 동적으로 가져온다. 하지만 컴파일 시점에서는 타입을 알 수 없으므로 Class<?> 타입의 객체로 가져오게 된다.
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
return element.getAnnotation( // element 객체에서 형변환된 어노테이션 클래스를 통해 해당 어노테이션을 가져온다. 이를 통해 런타임에 동적으로 어노테이션 타입을 처리할 수 있다.
annotationType.asSubclass(Annotation.class)); // annotationType을 Annotation 클래스의 하위 클래스로 형변환한다. 형변환 성공하면 해당 클래스 객체를 반환하고, 실패하면 ClassCastException이 발생한다.
}
즉, asSubclass 메소드를 사용하여 비한정적 타입 토큰인 Class<?>을 적절한 타입으로 형변환하여 런타임에 어노테이션 타입을 처리하는 예시이다. 이를 통해 컴파일 시점에는 알 수 없는 어노테이션을 런타임에 동적으로 처리할 수 있다.
'Language > Java' 카테고리의 다른 글
| [effective java] 아이템 35. ordinal 메서드 대신 인스턴스 필드를 사용하라. (0) | 2023.05.14 |
|---|---|
| [effective java] 아이템 34. int 상수 대신 열거 타입을 사용하라. (0) | 2023.05.14 |
| [effective java] 아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라. (0) | 2023.05.14 |
| [effective java] 아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라. (0) | 2023.05.14 |
| [effective java] 아이템 30. 이왕이면 제네릭 메서드로 만들라. (0) | 2023.05.09 |