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

Loopy·2022년 8월 13일
0

이펙티브 자바

목록 보기
32/76
post-thumbnail

제네릭은 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.
예를 들어, Set<E>에는 원소의 타입을 뜻하는 단 하나의 타입 매개변수만 있으면 되며, Map<K,V> 에는 키와 값을 뜻하는 2개만 필요하다.

하지만 컨테이너 자체가 아닌 "키"를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공한다면 더 유연하게 여러 타입을 받을 수 있지 않을까?

바로 이러한 설계 방식을, 우리는 타입 안전 이종 컨테이너 패턴이라고 한다.

public <T> void putFavorite(Class<T> type, T instance);   // 키가 매개변수화 됨
public <T> T getFavorite(Class<T> type);

☁️ 타입 안전 이종 컨테이너 개념

사실 class 의 타입이 Class<T> 제네릭이기 때문에, 각 타입의 Class 객체를 매개변수화한 키 역할로 사용하는 것이 가능해진다. ex) String.class 타입은 Class<String>

가장 중요한 것은, 우리는 이를 통해 제네릭에서의 컴파일타임 타입 안전성이 아닌 런타임 타입 안전성을 얻을 수 있다는 것이다.

타입 안전 이종 컨테이너 구현

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));
    }
}
  1. Map<Class<?>, Object>
    키를 비한정적인 와일드카드 타입으로 선언하였기 때문에, 이를 통해서 다양한 매개변수화 타입의 키를 허용할 수 있게 되었다. 만약 Map<Class<T>, Object> 였다면 오직 한가지 타입의 키만 담을 수 있었을 것이다.

  2. Class.cast
    valueObject 타입이므로 맵에 넣을때 값이 키 타입의 인스턴스라는 것이 보장되지 않는다. 따라서 맵에서 가져올때는 cast 메서드를 사용해 이 객체 참조를 class 객체가 가리키는 T 타입으로 동적 변환하고 있다.

🔖 cast 메서드
형변환 연산자의 동적 버전으로, 주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지 검사한 다음, 맞다면 반환하고 아니면 ClassCastException 을 던진다. 이를 활용하면, T 로 비검사 형변환을 하지 않아도 된다.

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);
        
     // Java cafebabe Favorites 출력
     System.out.printf("%s %x %s%n", favoriteString,
                favoriteInteger, favoriteClass.getName()); 
 }

이렇게 Favorites 인스턴스는 타입 안전하며 일반적인 맵과 달리 여러 가지 타입의 원소를 담을 수 있기 때문에, 타입 안전 이종 컨테이너라고 할 수 있다.

☁️ 타입 안전 이종 컨테이너 한계

1. Class 객체를 제네릭이 아닌 로 타입으로 넘기면, 타입 안전성이 쉽게 깨진다.

아래 코드에서는 Class 로 타입으로 다시 캐스팅하여 전달했으니, 컴파일 타임에서는 문제 없이 Map 에 저장이 된다. 이후 꺼내올때 String 객체를 Integer 로 캐스팅 하려 하니 런타임에서 예외가 발생한다.

f.putFavorite((Class)Integer.class, "문자열");
int result = f.getFavorite(Integer.class);  // ClassCastException

첨고로 실제 개발할때는 컴파일 과정에서 비검사 경고가 발생하기 때문에, 지켜만 진다면 런타임에 타입 안전성이 보장될 것이다.

이처럼 인스턴스가 타입 불변식을 어기는 일이 없도록 보장하려면, 다음과 같이 동적 형변환(cast)을 통해 인수로 주어진 instance 의 타입이 type 으로 명시한 타입과 같은지 확인하면 된다.

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

활용

java.util.CollectionscheckedSet(), checkedList(), checkedMap() 메서드들도 해당 방식을 적용한 컬렉션 래퍼들이다. 해당 클래스들은 모두 CheckedCollection 을 상속 받았고, typeCheck 메서드를 통해 추가 연산시에 타입을 체크하여 안전성을 보장한다.

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

List<String> 과 같이 제네릭 타입은 하나의 List.class 를 공유하지 고유의 Class 객체가 없기 때문에, String 이나 String[] 와 다르게 키로 저장하려 하면 컴파일 에러가 날 것이다.

List<String> = List<Integer> = List.class

그럼에도 반드시 제네릭을 저장하고 싶다면, 대안으로 스프링에서는 슈퍼 타입 토큰 즉 ParameterizeTypeReference 라는 클래스로 제공하고 있다.

TypeReference 은 아래 예제와 같이 주로 Json 파일 입출력 시 변환 과정에서 사용하면 편리하다. List<T>Map<T, T> 형태로 파싱이 필요한 경우가 많기 때문이다.


inline fun <reified T: Any> readFileData(readPath: String): T = run {
    try {
         FileReader(readPath).use {
             val typeRef = object : TypeReference<T>() {} // TypeReference
             return objectMapper.readValue(it, typeRef)
         }
    } catch (e: IOException) {
        throw CommonException(ResponseCode.INTERNAL_SERVER_ERROR, e)
    }
}

☁️ 한정적 타입 토큰

Favorites가 어떤 Class 객체든 받아들이므로 비한정적 타입 토큰이라 할 수 있다. 이 메서드들이 허용하는 타입을 제한하고 싶다면, 한정적 타입 토큰을 활용하자.

🔖 한정적 타입 토큰이란?
한정적 타입 매개변수( E extends Delayed )나 한정적 와일드카드( ? extends Delayed )을 사용하여 표현 가능한 타입을 제한하는 토큰

// 대상 요소에 달려있는 애너테이션을 런타임에 읽어 오는 기능
public <T extends Annotation> T getAnnotation(Class<T> annotationType);

이 메서드는 토큰으로 명시한 타입의 애너테이션이 대상 요소에 달려 있다면 그 애너테이션을 반환하고, 없다면 Null을 반환한다. (타입 안전 이종 컨테이너)

Q. Class<?> 타입의 객체를 위와 같은 한정적 타입 토큰을 받는 메서드에 넘기려면 어떻게 해야 할까?

Class<? extends Annotation> 으로 형변환 하는 방식은 비검사 경고가 뜰 것이다. 대신, 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() : 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환한다.(형변환은 자식 -> 부모로만 가능하다)

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

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글