본문 바로가기
public void static main()/Book

[Effective Java] 아이템 33 (이거 보지마세여! 다시 정리할꺼에요!)

by 햄리뮤 2023. 1. 25.
반응형

[책 규칙]

노란색 - 자세히 알아보고싶은 부분

초록색 - 핵심 개념 (블로그 정리시는 검은색으로!)

빨간색 - 무슨말인지 모르겠는 부분

[아이템 33] 타입 안전 이종 컨테이너를 고려하라!

타입 안전 이종 컨테이너?

  • 한 타입의 객체만 담을 수 있는 컨테이너가 아니라 여러 다른 타입 (이종)을 담을 수 있는 타입 안전한 컨테이너.

타입 안전 이종 컨테이너 패턴 예제

즐겨찾는 인스턴스를 저장하고 검색할 수 있는 Favorites 클래스로 패턴을 보자!

// 타입 안전 이종 컨테이너 패턴 - 구현
public class Favorite {
// 비한정적 와일드카드 타입이라 이 맵 안에 아무것도 넣을 수 없다고 생각할 수 있지만 그 반대다. 
// 와일드카드 타입이 중첩(nested) 되었다는 점을 깨달아야 한다.
// 맵이 아니라 키가 와일드카드 타입인 것이다. 
// 이는 모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻으로 첫번째는 Class, 두번째는 Class식으로 될 수 있다. 
// 다양한 타입을 지원하는 힘은 여기서 나온다.
	private Map<Class<?>, Object> favorites = new HashMap<>();

	public <T> void putFavorite(Class<T> type, T instance) {
		favorites.put(Objects.requireNonNull(type), type.cast(instance));
	}
    / **
    cast 메소드는 주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지를 검사하고, 
    맞으면 인수를 아니면 ClassCastException을 던진다.
    
    ast 메소드가 단순히 인수를 그대로 반환하는데도 cast 메소드를 사용하는 이유는 
    cast 메소드의 시그니처 Class 클래스가 제네릭이라는 이점을 완벽히 활용하기 때문이다.
    */
	public <T> T getFavorite(Class<T> type) {
		return type.cast(favorites.get(type));
	}
}
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());
}

위 코드에서 각 타입의 Class 객체를 매개변수화한 키 역할로 사용하고 있는데 이 방식이 동작하는 이유는 class의 클래스가 제네릭이기 때문이다. class 리터럴 타입은 Class가 아닌 Class<T> 이다!

Favorites 인스턴스가 안전한 이유는 String을 요청했는데 Integer를 반환하는 일이 없기 때문이다! 또한 모든 키의 타입이 제각각이라 일반적인 맴과 달리 여러가지 타입의 원소를 담을 수 있다!
따라서 Favorites는 타입 안전 이종 컨테이너라 할 만하다!

잠깐! 제약 사항이 있다!

  • 첫번째는! 악의적인 클라이언트가 Class 객체를 row 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다!
    • 이렇게 짜여진 클라이언트 코드에서는 비검사 경고가 뜰것이다! 아래 코드를 실행하면 ClassCastException을 던진다! 이것은 HashSet, HashMap 등의 일반 컬렉션도 가지고 있는 문제다! HashSet의 row 타입을 사용하면 HashSet에 String을 넣는건 아주 쉽다!
f.putFavorite((Class)Integer.class, "Integer의 인스턴스가 아닙니다."); int favoriteInteger = f.getFavorite(Integer.class);

Favorites가 타입 불변식을 어기지 않게 만드는 방법은 없을까?

  • 동적 형변환을 사용하면 된다!
  • 이와 같은 방법을 사용하는 컬렉션들은 checkedSet, checkedList, checkedMap 같은 컬렉션 래퍼들이 있다.
public <T> void putFavorite(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), Objects.requireNonNull(type.cast(instance)));
}
  • 두번째는! 실체화가 불가 타입에는 사용할 수 없다!
    •  String이나 String[]은 사용할 수 있어도 List<String>이나 List<Integer>는 저장할 수 없다는 말이다!
      • List<String>를 저장하려해도 컴파일 되지 않는다. List<String>용 Class 객체를 얻을 수 없기 때문이다. (List<String>.class 문법 오류가 난다! => List<String>이나 List<Integer>나 List.class 라는 같은 Class 객체를 공유하기 때문!)

한정적 타입 토큰

Favorites가 사용하는 타입 토큰은 비한정적이다. 즉, getFavorites와 putFavorites는 어떤 Class 객체든 받아들인가는 뜻이다! 타입을 제한하고 싶을 때는 한정적 타입 토큰을 사용하면 된다!

@Override
public <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationClass) {
    Objects.requireNonNull(annotationClass);

    AnnotationData annotationData = annotationData();
    return AnnotationSupport.getAssociatedAnnotations(annotationData.declaredAnnotations, this, annotationClass);
}

여기서 annotationType인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰이다. 이 메서드는 토큰으로 명시한 타입의 에너테이션이 대상 요소에 달려 있다면 그 애너테이션을 반환하고 없다면 null을 반환한다. 즉, 애너테이션된 요소는 그 키가 애너테이션 타입인, 타입 안전 이종 컨테이너이다.

Class<?> 타입의 객체가 있고, 이를 (getAnnotation처럼) 한정적 타입 토큰을 받는 메서드에 넘기려면 어떻게 해야 할까?
객체를 Class<? extends Annotation>으로 형변환할 수도 있지만 이 형변환은 비검사이므로 컴파일하면 경고가 뜻 것이다. 운 좋게도 Class 클래스가 이런 형변환을 안전하게 수행해주는 인스턴스 메서드를 제공한다. 바로 asSubClass 메서드로, 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환한다. (형변환된다는 것은 이 클래스가 인수로 명시한 클래스의 하위 클래스라는 뜻이다.)
// asSubclass를 사용해 한정적 타입 토큰을 안전하게 형변환한다.
@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));
}

위 코드는 컴파일 시점에는 타입을 알 수 없는 어노테이션을 asSubclass 메소드를 사용해 런타임에 읽어내는 예이다.

핵심 정리
컬렉션 API 로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수 의 수가 고정되어있다. 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는타입 안전 이종 컨테이너를 만들수 있다. 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다.

 

다시 정리할 예정 !!!!

 

https://velog.io/@chullll/33.-%ED%83%80%EC%9E%85-%EC%95%88%EC%A0%84-%EC%9D%B4%EC%A2%85-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EB%A5%BC-%EA%B3%A0%EB%A0%A4%ED%95%98%EB%9D%BC

 

33. 타입 안전 이종 컨테이너를 고려하라

제네릭은 Set<E>, Map<K,V> 등의 컬렉션과 ThreadLocal<T>, AtomicReference<T> 등의 단일 원소 컨테이너에도 흔히 쓰인다. 하지만 이처럼 클래스 레벨에서 매개변수화 할 수 있는 타입의 수는 제한적이다. (e.g

velog.io

 

** 그냥 하루하루 개인 공부한 것을 끄적 거리는 공간입니다.

이곳 저곳에서 구글링한 것과 강의 들은 내용이 정리가 되었습니다.

그림들은 그림밑에 출처표시를 해놓았습니다.

문제가 될시 말씀해주시면 해당 부분은 삭제 하도록하겠습니다. **

반응형

'public void static main() > Book' 카테고리의 다른 글

[Effective Java] 아이템 41  (0) 2023.01.31
[Effective Java] 아이템 35  (0) 2023.01.30
[Effective Java] 아이템 32  (1) 2023.01.25
[Effective Java] 아이템 23  (0) 2023.01.24
[Effective Java] 아이템 18  (0) 2023.01.24

댓글