[Effective Java] 아이템 33 : 타입 안전 이종 컨테이너를 고려하라(feat. ApplicationContext of Spring)
0. 들어가며
33번은 타입 안전 이종 컨테이너
에 대한 이야기를 하는 아이템입니다.
이 단어를 하나 하나 분해하여 의미를 파악해보겠습니다.
- 타입 안전 : 제네릭을 이용하여 타입 안정성을 꾀함
- 이종(異種) : 다른 종류
- 컨테이너 : 무언가를 담는 개체(그릇, 용기, 화물 컨테이너 등)
이를 합쳐서 생각해보면 다음과 같습니다.
타입 안전 이종 컨테이너 : 제네릭을 이용하여 타입 안정을 꾀하지만, 여러 타입을 담을 수 있는 개체
의미를 알았으니 더 자세한 내용은 아래에서 알아보겠습니다.
1. 타입 안전 이종 컨테이너(비한정적 타입 토큰)
먼저 책에서 타입 이종 컨테이너를 설명하기위해 제시한 코드를 살펴봤습니다.
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T>, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorit(Class<T> type) {
return type.cast(favorites.get(type));
}
}
이 코드에서 Map 의 Key 로 Class<?>
를 받는데요.
이를 책에서는 타입 토큰 이라고 표현하고, 더 정확히는 '비한정적 타입 토큰'이라고 부를 수 있습니다.
저는 이 코드를 보고 가장 먼저 일급 컬렉션이 떠올랐습니다.
Favorites 클래스는 Map 을 감싼 일급 컬렉션이고, 타입(타입 토큰)이 key 가 되고 해당 인스턴스가 value 로 되어있는 형태입니다.
그런데 이 예제 코드를 보고 뭔가 익숙한 하나가 더 떠올랐는데요.
제가 익숙함을 느낀 그 대상은 스프링에서의 빈 컨테이너입니다.
1.1 스프링 빈 컨테이너
보통 스프링 빈을 자동 주입받아서 사용하지만, 아래의 예시처럼 가끔 ApplicationContext 를 통해 직접 가져오기도 합니다.
@Autowired
AnnotationConfigApplicationContext context;
public void something() {
SomeComponent component = context.getBean(SomeComponent.class, "someComponent");
// do something ...
}
context.getBean
을 호출하면 대략 다음과 같은 클래스들을 타고 이동하며 bean 을 가져옵니다.
(설명을 위해 아주 많은 부분을 생략하고 간소화한 그림이니 참고 부탁드립니다.)
여기서 A, B를 합치면 책에서 소개한 타입 이종 컨테이너 Favorites 예제와 같은 구조가 됩니다.
하지만 스프링에서 이렇게 두 단계로 한 이유중 하나는 bean 이름을 붙이는 기능을 지원하기 위함입니다.
1.2 Collectinos
이외에도 이를 활용한 사례들을 책에서 소개해주었습니다.
java.utils.Collections 의 checkedSet, checkedList, checkedMap 과 같은 메서드들을 인데요.
소스를 까보니 그냥 모든 작업에 typeCheck 작업이 들어가있는 구현체를 반환하는 메서드였습니다.
// java.utils.Collections.CheckedCollection 클래스에 있는 메서드
E typeCheck(Object o) {
if (o != null && !type.isInstance(o))
throw new ClassCastException(badElementMsg(o));
return (E) o;
}
그래서 제네릭만 제대로 써도 필요없는것 아닌가 했는데, 조금 더 읽어보니 로 타입을 위한 기능이라고 합니다.
로 타입은 제네릭이 자바에 정착하기전에 생겼던 안티패턴이지만 하위호환성을 위해 지원하는 문법이라고 합니다.
(자세한 내용은 아이템 26. 로 타입은 사용하지 말라 참고)
2. 슈퍼 타입 토큰
그런데 앞에서 살펴본 형태의 타입 안전 이종 컨테이너는 제네릭 타입은 담지 못한다는 단점이 있습니다.
List<Object> objects = new ArrayList<>();
Map<Class<?>, Object> container = new ConcurrentHashMap<>(64);
container.put(List.class, objects);
container.put(List<Object>.class, objects); // `List<Object>.class` 에서 컴파일 에러 발생
위와 같은 경우에는 가령 List<String>
혹은 Set<Integer>
등의 타입을 key 로 하고싶더라도 불가능합니다.
이를 해결할 수 있는 방법으로 슈퍼 타입 토큰이라는 개념을 소개했는데요.
이 내용은 설명이 너무 길어질 수 있어 슈퍼타입토큰(Super Type Token) 글을 참고하시는걸 강추드립니다.
(추가로 책에서는 닐 개프터의 글도 추천합니다.)
3. 한정적 타입 토큰
1번에서 살펴봤던 예제에서는 제네릭에 와일드카드만 사용되었는데요.
와일드카드 대신 한정적 타입을 선언하는 방법도 있습니다.
// java.lang.reflect.AnnotatedElement 에 선언된 메서드
public <T extends Annotation> getAnnotation(Class<T> annotationType);
이 메서드는 객체에 해당 타입의 어노테이션이 달려있다면 그 어노테이션을 반환하고, 아니라면 null 을 반환합니다.
즉, 이는 타입 안전 이종 컨테이너인 것이죠.
3.1 Class 클래스의 asSubClass 메서드
만약 Class<?> 객체를 3번에서처럼 한정적 타입만 받는 메서드에 넘기려면 어떻게 해야할까요.
Class<?> clazz = ...;
Class<? extends Annotation> castedClass = (Class<? extends Annotation>) clazz;
이런 형태도 가능하긴 하지만 비검사 형변환이기 때문에 컴파일시 경고가 발생합니다.
Class<?> clazz = ...;
annotatedElement.getAnnotation(clazz.asSubclass(Annotation.class));
이렇게 한다면 컴파일 경고 없이 넘길 수 있습니다.