ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 33. 타입 안전 이종 컨테이너를 고려하라
    Book/Effective Java 3E 2022. 12. 23. 21:00
    반응형

      Set에는 원소의 타입을 뜻하는 단 하나의 타입 매개변수만 있으면 되며, Map에는 키와 값의 타입을 뜻하는 2개만 필요합니다. 데이터베이스의 경우에는 행(row)에 해당하는 임의 개수의 열(column)이 필요합니다. 이런 경우, 키를 매개변수화한 다음 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면 됩니다. 이러한 설계 방식을 타입 안전 이종 컨테이너 패턴(type safe heterogeneous container pattern)이라 합니다.

     

      다음은 간단한 예로 타입별로 즐겨 찾는 인스턴스를 저장하고 검색할 수 있는 Favorites 클래스입니다. 각 타입의 Class 객체를 매개변수화한 키 역할로 사용합니다.

    package effectivejava.chapter5.item33;
    import java.util.*;
    
    // 타입 안전 이종 컨테이너 패턴
    public class Favorites {
        // 코드 33-3 타입 안전 이종 컨테이너 패턴 - 구현
        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));
        }
    
    //    // 코드 33-4 동적 형변환으로 런타임 타입 안전성 확보
    //    public <T> void putFavorite(Class<T> type, T instance) {
    //        favorites.put(Objects.requireNonNull(type), type.cast(instance));
    //    }
    
        // 코드 33-2 타입 안전 이종 컨테이너 패턴 - 클라이언트
        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());
        }
    }

    Note. 자바의 printf가 C의 printf와 다른 점이 하나 있다. 이 코드에서는 만약 C였다면 \n을 썼을 곳에 %n을 썼는데, 이 %n은 플랫폼에 맞는 줄바꿈 문자로 자동으로 대체된다(대부분 플랫폼에서 \n이 되겠지만, 모든 플랫폼이 그렇지는 않다).

     

      cast 메서드는 형변환 연산자의 동적 버전입니다. 이 메서드는 단순히 주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지를 검사한 다음, 맞다면 그 인수를 그대로 반환하고, 아니면 ClassCastException을 던집니다.

    public class Class<T> {
      T cast(Object obj);
    }

     

    Favorites 클래스 제약 사항

    1. 악의적인 클라이언트가 Class 객체를 (제네릭이 아닌) 로 타입(Item 26)으로 넘기면 타입 안정성이 쉽게 깨집니다.

    f.pubFavorite((Class)Integer.class, "Integer의 인스턴스가 아닙니다.");
    int favoriteInteger = f.getFavorite(Integer.class);

      Favorites가 타입 불변식을 어기는 일이 없도록 보장하려면 위의 코드 33-4 같이 putFavorite 메서드에서 인수로 주어진 instance의 타입이 type으로 명시한 타입과 같은지 확인하면 됩니다.

     

    2. 실체화 불가 타입(Item 28)에는 사용할 수 없습니다. 다시 말해, 즐겨 찾는 String이나 String[]은 저장할 수 있어도 List<String>은 저장할 수 없습니다. List<String>과 List<Integer>는 List.class라는 같은 Class 객체를 공유하므로, 만약 List<String>.class와 List<Integer>.class를 허용해서 둘 다 똑같은 타입의 객체 참조를 반환하다면 Favorites 객체의 내부는 아수라장이 될 것입니다. 이 두 번째 제약에 대한 완벽히 만족스러운 우회로는 없습니다.

     

      두 번째 제약을 슈퍼 타입 토큰(super type token)으로 해결하려는 시도도 있습니다. 슈퍼 타입 토큰은 자바 업계의 거장인 닐 개프터(Neal Gafter)가 고안한 방식으로, 실제로 아주 유용하여 스프링 프레임워크에서는 아예 ParameterizedTypeReference라는 클래스로 미리 구현해 놓았습니다.

     

      Favorites에 슈퍼 타입 토큰을 적용하면 다음 코드처럼 제네릭 타입도 문제없이 저장할 수 있습니다.

    Favorites f = new Favorites();
    
    List<String> pets = Arrays.asList("개", "고양이", "앵무");
    
    f.putFavorite(new TypeRef<List<String>>(){}, pets);
    List<String> listofStrings = f.getFavorite(new TypeRef<List<String>>(){});

      이 방식의 원리는 닐 개프터의 글(http://bit.ly/2NGQi2S)을 참고하시면 됩니다.  

     

      Favorites가 사용하는 타입 토큰은 비한정적입니다. 때로는 이 메서드들이 허용하는 타입을 제한하고 싶을 수 있는데, 한정적 타입 토큰을 활용하면 가능합니다. 한정적 타입 토큰이란 단순히 한정적 타입 매개변수(Item 29)나 한정적 와일드카드(Item 31)를 사용하여 표현 가능한 타입을 제한하는 타입 토큰입니다.

     

      애너테이션 API(Item 39)는 한정적 타입 토큰을 적극적으로 사용합니다. 다음은 컴파일 시점에는 타입을 알 수 없는 애너테이션을 asSubClass 메서드를 사용해 런타임에 읽어내는 예입니다.

    package effectivejava.chapter5.item33;
    import java.lang.annotation.*;
    import java.lang.reflect.*;
    
    // 코드 33-5 asSubclass를 사용해 한정적 타입 토큰을 안전하게 형변환한다.
    public class PrintAnnotation {
        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));
        }
    
        // 명시한 클래스의 명시한 애너테이션을 출력하는 테스트 프로그램
        public static void main(String[] args) throws Exception {
            if (args.length != 2) {
                System.out.println(
                    "사용법: java PrintAnnotation <class> <annotation>");
                System.exit(1);
            }
            String className = args[0];
            String annotationTypeName = args[1]; 
            Class<?> klass = Class.forName(className);
            System.out.println(getAnnotation(klass, annotationTypeName));
        }
    }

     

    컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다.

    하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다.

    타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런 식으로 쓰이는 Class 객체를 타입 토큰이라 한다.

    또한, 직접 구현한 키 타입도 쓸 수 있다.

    예컨대 데이터베이스의 행(컨테이너)을 표현한 DatabaseRow 타입에는 제네릭 타입인 Column<T>를 키로 사용할 수 있다.

     

    [참고 정보]

    이펙티브 자바 Effective Java 3/E 도서 [조슈아 블로크 ]

    이펙티브 자바 깃허브 저장소

    <이펙티브 자바, 3판> 번역 용어 해설

    반응형

    댓글

Designed by Tistory.