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

wisdom·2022년 8월 19일
0

Effetctive Java

목록 보기
33/80
post-thumbnail

✔️ 타입 안전 이종 컨테이너 패턴

제네릭은 컬렉션과 단일원소 컨테이너에 흔히 쓰인다. 이때 매개변수화되는 대상은 원소가 아닌 컨테이너 자신이다. 따라서 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.

하지만 더 유연한 수단이 필요할 때도 종종 있다.

예를 들어, 데이터베이스의 행은 임의 개수의 열을 가질 수 있는데 모두 열을 타입 안전하게 이용할 수 있다면 멋질 것이다.

Q. 하나의 컨테이너에서 매개변수화할 수 있는 타입 개수를 제한하지 않고 사용할 수 있는 방법은 없을까?

해법은
1️⃣ 컨테이너 대신 키를 매개변수화한 다음,
2️⃣ 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하는 것이다.

이러한 설계 방식을 타입 안전 이종 컨테이너 패턴(type safe heterogeneous container pattern)이라고 한다.


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

즐겨찾는 인스턴스를 저장하고 검색할 수 있는 Favorites 클래스 예제를 보자.

public class Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<>();

    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));
    }
}

키 타입 Class<?>

  • 각 타입의 Class 객체를 매개변수화한 키로 사용하며, 이러한 Class 객체를 타입 토큰이라고 한다.
  • 타입 토큰(type token): 컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴.
  • 키 타입에 비한정적 와일트카드 타입을 사용하여, 모든 키가 서로 다른 매개변수화 타입일 수 있도록 한다.

값 타입 Object

  • 값 타입으로 Object를 사용하여, 키와 값 사이의 타입 관계를 보증하지 않는다.
  • 하지만 우리는 이 관계가 성립함을 안다.

putFavorite 메서드

  • Class 객체와 인스턴스를 favorites에 추가한다.
  • 이때 키와 값 사이의 타입 링크(type linkage) 정보는 버려진다.

getFavorite 메서드

  • Class의 cast 메서드를 사용해 객체 참조를 Class 객체가 가리키는 타입으로 동적 형변환한다.
  • cast 메서드
    • 형변환 연산자의 동적 버전
    • 주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지를 검사한 다음
      맞다면 그 인수를 그대로 반환하고, 아니면 ClassCastException을 던진다.
      public class Class<T> {
      		T cast(Object obj);
      }

이를 사용하는 클라이언트 코드는 다음과 같다.

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 cafebabe Favorites
}

Favorites 클래스의 제약 사항

1. Class 객체를 (제네릭이 아닌) 로 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다.

다음에 나오는 동적 형변환을 통해 해결 가능하다.

2. 실체화 불가 타입에는 사용할 수 없다.

예를 들어, String, String[]은 저장할 수 있지만 List<String>은 저장할 수 없다.

이 제약에 대한 완벽히 만족스러운 우회로는 없다.


✔️ 동적 형변환

Favorites 클래스에는 Class 객체를 (제네릭이 아닌) 로 타입으로 넘기면 Favorites 인스턴스의 타입 안전성이 쉽게 깨진다는 제약이 존재한다.

Q. 그렇다면, Favorites가 타입 불변식을 어기는 일이 없도록 보장하려면 어떻게 해야할까?

동적 형변환을 통해 인수로 주어진 인스턴스의 타입이 type으로 명시한 타입과 같은지 확인하면 된다!

public <T> void putFavorite(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), type.cast(instance));
}

+) java.utils.Collections에는 이 방식을 적용한 컬렉션 래퍼들이 존재한다.
checkedSet, checkedList, checkedMap 메서드


✔️ 한정적 타입 토큰

Favorites가 사용하는 타입 토큰은 비한정적이다.

그러나 때로는 이 메서드들이 허용하는 타입을 제한하고 싶을 수 있는데, 이때는 한정적 타입 토큰을 활용할 수 있다.

💡 한정적 타입 토큰

한정적 타입 매개변수한정적 와일드카드를 사용하여 표현 가능한 타입을 제한하는 타입 토큰이다.

애너테이션 API는 한정적 타입 토큰을 적극적으로 사용한다.

public <T extends Annotation> T getAnnotation(Class<T> annotationType);
  • annotationType 인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰이다.
  • 애너테이션된 요소는 키가 애너테이션 타입인 타입 안전 이종 컨테이너다.

Q. Class<?> 타입의 객체가 있고, 이를 한정적 타입 토큰을 받는 메서드에 넘기려면 어떻게 해야 할까?

메서드에 넘기기 위해서는 객체를 Class<? extends Annotation>으로 형변환해야 한다.

운 좋게도, Class 클래스는 이런 형변환을 안전하고 동적으로 수행해주는 메서드를 제공한다.

asSubclass 메서드

  • 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환한다.
  • 형변환에 성공하면 인수로 받은 클래스 객체를 반환하고, 실패하면 ClassCastException을 던진다.
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));
}



📌 핵심 정리

  • 컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다.
  • 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다.
  • 타입 안전 이종 컨테이너 는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다.
  • 또한, 직접 구현한 키 타입을 쓸 수 있다.
  • 예컨대 데이터베이스의 행(컨테이너)를 표현한 DatabaseRow 타입에는 제네릭 타입인 Column<T>를 키로 사용할 수 있다.
profile
백엔드 개발자

0개의 댓글