Java

[Effective Java] 아이템 33 : 타입 안전 이종 컨테이너를 고려하라(feat. ApplicationContext of Spring)

jwKim96 2023. 1. 28. 21:47

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 을 가져옵니다.

image

(설명을 위해 아주 많은 부분을 생략하고 간소화한 그림이니 참고 부탁드립니다.)

여기서 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));

이렇게 한다면 컴파일 경고 없이 넘길 수 있습니다.