본문 바로가기
토비의봄

2. 슈퍼 타입 토큰

by 이석준석이 2021. 2. 20.

1. 제너릭

 

아래의 코드에서 s.value

  • 컴파일 타임에서는 컴파일러가 미리 체크를하고, 캐스팅해준다.
    • s.value = "String" ==(컴파일러)==> s.value = (String) "String";
  • 런타임에서 value 의 타입은 Object이다.
    • type erasure 가 타입 파라미터를 지운다.
public class TypeToken {
    static class Generic<T> {
        T value;
        void set(T t) {}
        T get() {return null;}
    }

    public static void main(String[] args) throws Exception {
        Generic<String> s = new Generic<>();
        s.value = "String";

        Generic<Integer> i = new Generic<>();
        i.value = 1;
    }
}

 

용어

  • 타입 파라미터 : Generic<String> 에서 String 를 타입 파라미터라고 부른다.

2. 강제 캐스팅

 

강제 캐스팅은 컴파일내에는 오류를 발생시키지 않지만, 런타임시에 오류를 만들 수 있으므로 위험하다.

// ClassCastException 발생
public class TypeToken {
    public static void main(String[] args) {
        Object o = "String";
        Integer i = (Integer) o;
        System.out.println("i = " + i);
    }
}

 

반면에 제너릭정보와 클래스정보를 사용하면 안전한 방식으로 (type safe) 사용할 수 있다.

public class TypeToken {
    <T> T create(Class<T> clazz) throws Exception {
        return clazz.newInstance();
    }
}

3. 안전한 Map을 만들어보자.

 

가정

  • Map 안에 여러타입을 넣을수 있는 Map을 한번 만들어보자.
public class TypeToken {
    static class TypeSafeMap {
        Map<String, Object> map = new HashMap<>();
        
        void run() {
            map.put("a", "a");
            map.put("b", 1);
            
            Integer i = (Integer) map.get("b");
            String s = (String) map.get("a");
        }
    }
}

위의 코드는 강제 타입변환을 하므로 위험하다. 더 안전하게 만드려면 어떻게해야할까?


이번에도 Map을 만들지만 Map의 key 를 클래스타입(Class<?>)으로 만들어서 만들어보자.

  • 클래스타입에 대한 키는 하나밖에 못만들겠지만 일단 진행
  • key 에는 클래스타입을, value 에는 key 의 타입에 맞는 값을 넣는다는 목표로 만든다.

아래의코드는 컴파일오류로는 체크하지못하는, 런타임시에 에러가 생기는 코드이다.

public class TypeToken {
    static class TypeSafeMap {
        Map<Class<?>, Object> map = new HashMap<>();

        void put(Class<?> clazz, Object value) {
            map.put(clazz, value);
        }
    }

    public static void main(String[] args) {
        TypeSafeMap m = new TypeSafeMap();
        // 조금 안전한가 싶지만 아래의 코드도 컴파일 오류를 일으키지 않는다.
        m.put(Integer.class, "Value");
    }
}

개선된 코드

  • 아래의 코드처럼 put 함수에 제너릭을 사용하고나니, 컴파일오류가 발생한다.
  • 컴파일러가 "Value" 를 (Integer) 로 캐스팅하려하니 에러가 나는 모습!
public class TypeToken {
    static class TypeSafeMap {
        Map<Class<?>, Object> map = new HashMap<>();

        <T> void put(Class<T> clazz, T value) {
            map.put(clazz, value);
        }
    }

    public static void main(String[] args) {
        TypeSafeMap m = new TypeSafeMap();
        // 컴파일오류가 발생한다.
        m.put(Integer.class, "Value");
    }
}

위의 코드정도면 안전하다고 생각하여 get() 메소드를 만들어보니

  • map 의 value 타입이 Object 이기때문에 타입캐스팅을 강제해야한다. -> 안전하지 못한 코드
public class TypeToken {
    static class TypeSafeMap {
        Map<Class<?>, Object> map = new HashMap<>();

        <T> void put(Class<T> clazz, T value) {
            map.put(clazz, value);
        }

        <T> T get(Class<T> clazz) {
            return (T) map.get(clazz);
        }
    }

    public static void main(String[] args) {
        TypeSafeMap m = new TypeSafeMap();
        m.put(Integer.class, 1);
        m.put(String.class, "String");
        m.put(List.class, Arrays.asList(1, 2, 3));
    }
}

(T) map.get(clazz) 를 사용하지말고 Class 타입이 제공하는 cast 를 사용하자

  • 결과까지 출력되는 모습을 보니 이제는 강제캐스팅이 없는 TypeSafe 한 Map을 만들었다.
public class TypeToken {
    static class TypeSafeMap {
        Map<Class<?>, Object> map = new HashMap<>();

        <T> void put(Class<T> clazz, T value) {
            map.put(clazz, value);
        }

        <T> T get(Class<T> clazz) {
            return clazz.cast(map.get(clazz));
        }
    }

    public static void main(String[] args) {
        TypeSafeMap m = new TypeSafeMap();
        m.put(Integer.class, 1);
        m.put(String.class, "String");
        m.put(List.class, Arrays.asList(1, 2, 3));

        System.out.println(m.get(Integer.class));
        System.out.println(m.get(String.class));
        System.out.println(m.get(List.class));
    }
}

 

이처럼 특정 타입의 클래스정보를 넘겨서 타입안정성을 꿰하는 기법

  • Type Token 기법이라고 한다.

4. Super Type Token

 

이후, 위의 TypeSafeMap에 아래와같이 리스트를 추가하려고한다.

  • 하지만 List<Integer> / List<String> 은 추가할 수 없다.
public class TypeToken {
    public static void main(String[] args) {
        TypeSafeMap m = new TypeSafeMap();
        m.put(List<Integer>.class, Arrays.asList(1, 2, 3));
        m.put(List<String>.class, Arrays.asList("a", "b", "c"));
    }
}

 

아래의 코드를 수행해보자

  • Object 가 출력된다.
  • 이유는 type erasure 가 타입 파라미터의 정보를 런타임시에는 지워버리기 때문이다.
    • 과거의 java에는 List (List<String>, List<Integer>) 등이 아닌 그냥 List 를 사용
    • 이에 대한 호환성을 맞추기위해서 자바는 타입파라미터정보를 런타임에는 지우는 type erasure 방식을 사용
public class SuperTypeToken {

    static class Sup<T> {
        T value;
    }

    public static void main(String[] args) throws NoSuchFieldException {
        // erasure 에 의해서 Object 만 출력됨, 리플렉션으로도 타입의 정보를 알 수 없음 (바이트코드 호환성문제에 의해서 삭제됨)
        Sup<String> s = new Sup<>();
        System.out.println(s.getClass().getDeclaredField("value").getType());
    }
}

하지만 아래의 코드에서는 type erasure 가 동작하지 않는다.

  • 출력의 결과는 String 이 반환된다.
    • Sup<List<String>> 이면 List<String> 이 반환 (중첩된 정보도 반환된다.)
  • 새로운 타입을 정의하면서 슈퍼클래스를 Generic 클래스로 타입을 지정하여 상속하면 런타임에도 정보가 남아있음
public class SuperTypeToken {

    static class Sup<T> {
        T value;
    }

    static class Sub extends Sup<String> {

    }

    public static void main(String[] args) throws NoSuchFieldException {
        // erasure에 의해서 없어지지 않음
        Sub b = new Sub();
        Type t = b.getClass().getGenericSuperclass();
        ParameterizedType ptype = (ParameterizedType) t;
        System.out.println(ptype.getActualTypeArguments()[0]);
    }
}

5. Local / Anonymous Local Class

 

그렇다면 Local 클래스로 만들어서 타입정보를 가져오면 되겠구나

public class SuperTypeToken {

    static class Sup<T> {
        T value;
    }

    public static void main(String[] args) {
        class Sub extends Sup<List<String>> {
        }

        Sub b = new Sub();
        Type t = b.getClass().getGenericSuperclass();
        ParameterizedType ptype = (ParameterizedType) t;
        System.out.println(ptype.getActualTypeArguments()[0]);
    }
}

한걸음 더 나아가서, 익명 Local 클래스로 만들어서 타입정보를 가져올 수 있다.

public class SuperTypeToken {

    static class Sup<T> {
        T value;
    }

    public static void main(String[] args) {
        Sup b = new Sup<List<String>>() {
        };

        Type t = b.getClass().getGenericSuperclass();
        ParameterizedType ptype = (ParameterizedType) t;
        System.out.println(ptype.getActualTypeArguments()[0]);
    }
}

 

아래와같이 사용할 수 있다.

public class TypeToken {
    static class TypeReference<T> {
        Type type;

        public TypeReference() {
            Type stype = getClass().getGenericSuperclass();
            if(stype instanceof ParameterizedType) {
                this.type = ((ParameterizedType)stype).getActualTypeArguments()[0];
            }
            else throw new RuntimeException();
        }
    }

    public static void main(String[] args) {
        TypeReference t = new TypeReference<String>(){
        };
        System.out.println(t.type);
    }
}

6. TypeReference<T> 를 상속한 익명클래스를 이용한 Map

 

TypeReference<T> 를 상속한 익명클래스를 Key 로 저장

  • 이를통해 value 를 따로 가져오는 코드이다.
  • 자세한 설명은 토비의봄 수퍼타입토큰을..
public class TypeToken {
    static class TypeSafeMap {
        Map<Type, Object> map = new HashMap<>();

        <T> void put(TypeReference<T> tr, T value) {
            map.put(tr.type, value);
        }

        <T> T get(TypeReference<T> tr) {
            if(tr.type instanceof Class<?>) {
                return ((Class<T>)tr.type).cast(map.get(tr.type));
            } else {
                return ((Class<T>)((ParameterizedType)tr.type).getRawType()).cast(map.get(tr.type));
            }
        }
    }

    static class TypeReference<T> {
        Type type;

        public TypeReference() {
            Type stype = getClass().getGenericSuperclass();
            if(stype instanceof ParameterizedType) {
                this.type = ((ParameterizedType)stype).getActualTypeArguments()[0];
            }
            else throw new RuntimeException();
        }
    }

    public static void main(String[] args) {
        TypeSafeMap m = new TypeSafeMap();
        m.put(new TypeReference<Integer>(){}, 1);
        m.put(new TypeReference<String>(){}, "String");
        m.put(new TypeReference<List<Integer>>(){}, Arrays.asList(1, 2, 3));
        m.put(new TypeReference<List<String>>(){}, Arrays.asList("a", "b", "c"));

        System.out.println(m.get(new TypeReference<Integer>(){}));
        System.out.println(m.get(new TypeReference<String>(){}));
        System.out.println(m.get(new TypeReference<List<Integer>>(){}));
        System.out.println(m.get(new TypeReference<List<String>>(){}));
    }
}

이처럼 TypeReference 의 제너릭을 상속하여 익명클래스로 만들어 타입의 정보를 가져오는 방식을 

  • 슈퍼타입토큰 방식이라고 한다.

언제쓰는가?

public class SpringTypeReference {
    public static void main(String[] args) {
        RestTemplate rt = new RestTemplate();
//        List<User> users = rt.getForObject("http://localhost:8080", List.class); // 타입토큰방식
//        System.out.println("users.get(0).getName() = " + users.get(0).getName()); // <- 에러

        List<User> supertypeUsers = rt.exchange("http://localhost:8080",
                HttpMethod.GET, null, new ParameterizedTypeReference<List<User>>(){}).getBody();
        supertypeUsers.forEach(System.out::println);
    }
}

7. Resolvable Type

 

  • Reflection.Type 을 이용하여 타입 정보를 가져오는 것이 예외처리도 너무 많고 불편함
  • 이를 스프링에서 추상화하여 사용할 수 있도록 만들었다.

예시) TypeReference 는 위에서 구현한 클래스를 재사용

public static void main(String[] args) {
    // ResolvableType
    ResolvableType rt = ResolvableType.forInstance(new TypeReference<List<String>>(){});
    System.out.println("rt.Type = " + rt.getSuperType().getGeneric(0).getType());
    System.out.println("rt.nestedType = " + rt.getSuperType().getGeneric(0).getNested(1).getType());

    // 자바에서 type erasure 을 통해 타입이 지워지므로 unResolved
    System.out.println("unResolved = " + ResolvableType.forInstance(new ArrayList<String>()).hasUnresolvableGenerics());

    // 가지고오려면 superclass 를 사용해야함
    System.out.println("resolved = " + ResolvableType.forInstance(new TypeReference<ArrayList<String>>(){}).hasUnresolvableGenerics());

}

'토비의봄' 카테고리의 다른 글

4.2. Reactive Streams  (0) 2021.02.28
4.1. Reactive Streams  (0) 2021.02.28
3.2. Generics  (0) 2021.02.25
3.1. Generics  (0) 2021.02.24
1. 재사용성과 다이나믹 디스패치, 더블 디스패치  (0) 2021.02.20