1. 자바 제네릭의 한계?
자바 제네릭은 클래스에서 사용할 타입을 외부(사용부)에서 사용하게 해주는 일반적인 기법을 의미한다.
이러한 제네릭을 사용하면, 타입만 다르고 공통된 기능을 가지고 있는 클래스나 메서드들에 파라미터로 타입을 외부에서 전달받아 조금 더 범용적인 코드를 만들 수 있다. 이러한 제네릭이 없었다면, 자바 객체의 최고 조상인 Object 로 전달받아 객체 타입에 따라 캐스팅해 로직을 별도로 만들어야 되는데 이러면 오히려 런타임 시 예기치 못한 오류가 더 많이 발생하고 별도의 타입으로 만들어둔 클래스 및 함수들과 다를 바 없다.
public class Generic {
public List<String> list = new ArrayList<>();
public void addString(String str) {
list.add(str);
}
public String getString(int index) {
return list.get(index);
}
}
// 제네릭이 아니라면, 이렇게 Object 로 전달
public class NonGeneric {
public List list = new ArrayList();
public void addString(Object obj) {
list.add(obj);
}
// 원하는 타입으로 캐스팅 해 반환
public String getString(int index) {
return (String) list.get(index);
}
}
그런데 자바 제네릭은 다른 언어의 제네릭과 달리 다른 점이 존재한다.
제네릭 도입 이후, 컴파일 타임 때까지는 타입 안전성을 얻을 수 있었지만, Type Erasure 기능으로 인해 컴파일 이후에는 타입이 소거가 되기 때문이다. 반쪽 제네릭이라고 불리우는 이유이다.
Class Test<T> {
private T value;
public Test(T t) {
this.value = t;
}
public T get() {
return value;
}
}
// -> 컴파일 이후
Class Test {
private Object value;
public Test(Object t) {
this.value = t;
}
public Object get() {
return value;
}
}
컴파일 타임 이후에 Object 로 변환이 된 것을 볼 수 있다. 컴파일 때까지만 타입 안전성을 제공하고, 그 이후에는 Object 로 변환이 되기 때문에 런타임 시에는 오류가 발생할 수도 있다.
자바 진영에서는 동세대 언어인 C# 과 다르게 보급률이 좋아서 현업 프로젝트들이 월등히 많았고 이러한 프로젝트들을 지키고자 하위 호완성을 위해 Type Erasure 를 도입했다고 한다. 컴파일 타임 때 Type Erasure 를 수행하면서 제네릭이 없던 하위 버전들과 동일한 형태로 class 파일을 생성할 수 있었다.
2. 타입 토큰의 등장
자바언어 개발자였던 Neal Gafter는 JAVA JDK5에 generics를 추가할 때 java.lang.Class 가 generic type이 되도록 변경했다고 한다. 예를 들어, String.class 의 Type 이 Class<String> 되도록 만들어 주었다. 그리고 이를 타입 토큰이라고 불렀다.
클래스 리터럴과 타입 토큰의 차이
클래스 리터럴은 어떤 클래스인지 파라미터로 전달되는 정보로 타입 토큰의 클래스 정보이다. 타입 토큰과 동일한 개념으로 타입 토큰은 클래스의 타입을 명시하고, 클래스 리터럴은 그 타입 토큰과 매칭되는 매개변수로 보면 된다.
// 선언부 (타입 토큰)
void myMethod(Class<?> clazz) {
...
}
// 사용부 (클래스 리터럴)
myMethod(String.class);
이 타입 토큰은 컴파일 타임 이후에도 타입에 대한 안전성을 확보하고자 사용된다.
제네릭만 사용했을 때, Operation 이나 타입들의 메서드들이 동일해야 사용할 수 있다는 한계가 있긴 하지만 그래도 동일한 기능을 한 코드로 만들 수 있어 좋다. 그런데 원하는 타입으로 반환은 안 된다. 아까 위에서 언급한 것처럼 타입이 모두 Object 로 변환되기 때문에 타입 캐스팅이 되지 않는다. 이 때 타입 토큰이 유용하게 사용된다.
// 선언부
public <T> T readValue(String content, Class<T> valueType) {
...
}
// 사용부
ProductDto productDto = objectMapper.readValue(jsonString, ProductDto.class);
위 예제처럼, valueType 으로 클래스 타입을 매개변수로 전달해주면 json 을 읽어 그 타입으로 반환해줄 수 있다.
조금 더 나아가 THC (Typesafe Heterogenous Container) Pattern 으로 사용될 수 있다.
// 선언부
public class SimpleTypeSafeMap {
private Map<Class<?>, Object> map = new HashMap<>();
public <T> void put(Class<T> k, T v) {
map.put(k, v);
}
public <T> T get(Class<T> clazz) {
return clazz.cast(map.get(clazz));
}
}
// 사용부
simpleTypeSafeMap.put(String.class, "테스트");
simpleTypeSafeMap.put(Integer.class, 1);
String string1 = simpleTypeSafeMap.get(String.class);
Integer integer1 = simpleTypeSafeMap.get(Integer.class);
타입 토큰을 키로 사용해 타입끼리 저장하고 다시 그 타입 토큰으로 캐스팅해서 가져올 수 있다. 타입 안전 이종 컨테이너라고 불리우며 영어로는 THC (Typesafe Heterogenous Container) 라고 한다. 컨테이너가 타입을 체크해서 컴파일 여부를 결정하는 것이 아니라 타입 토큰을 가지고 프로그래머가 직접 핸들링하는 개념이다.
3. 슈퍼 타입 토큰의 등장
타입 토큰의 한계
이 타입 토큰에도 한계는 있다. 제네릭 타입의 클래스 리터럴과 타입 토큰은 존재하지 않는다. Type Erasure 때문에 <> 내부에 있는 타입들이 제거되기 때문이다. 조금 더 쉽게 생각해보면 이중 삼중... 다중 타입에 대한 정보들을 가지고 있지 않아서 그렇다. 이러한 타입 토큰의 한계를 극복한 개념이 슈퍼 타입 토큰이다.
슈퍼 타입 토큰
다중 타입에 대한 정보들을 리터럴로 표현할 수 있으면 타입 토큰처럼 안전성을 확보할 수 있을 것이다.
Class.getGenericSuperclass() 와 ParameterizedType.getActualTypeArguments() 를 사용해서 전체 타입에 대한 정보와 실제 타입을 가져올 수 있다.
class Super<T> {}
class MyClass extends Super<List<String>> {}
MyClass myClass = new MyClass();
Type typeOfGenericSuperclass = myClass.getClass().getGenericSuperclass();
// ~~~$1Super<java.util.List<java.lang.String>> 출력됨
System.out.println(typeOfGenericSuperclass);
Super 껍데기 클래스의 파라미터로 해당 타입을 전달하면 getGenericSuperclass 메서드로 전체 타입에 대한 정보를 가져올 수 있다. Class (Java Platform SE 8 ) (oracle.com)
getGenericSuperclass 메서드에 대한 내용을 번역해보면 다음과 같다.
해당 객체의 상위 클래스 타입을 반환하는 메서드. 만약 상위 클래스가 매개변수화된 타입이라면 소스코드에서 사용된 실제 타입들을 반영해 반환해야 한다. 매개변수화된 타입에 대한 내용은 별도의 문서를 확인하라고 적혀 있다.(ParameterizedType은 제네릭 타입을 가지고 있는 타입을 말한다.)
ParameterizedType (Java Platform SE 8 ) (oracle.com)
제네릭 타입이라면, getActualTypeArguments 메서드를 통해 이 제네릭의 실제 타입을 가져올 수 있다.
class Super<T> {}
class MyClass extends Super<List<String>> {}
MyClass myClass = new MyClass();
Type typeOfGenericSuperclass = myClass.getClass().getGenericSuperclass();
// ~~~$1Super<java.util.List<java.lang.String>> 출력됨
System.out.println(typeOfGenericSuperclass);
// 수퍼 클래스가 ParameterizedType 이므로 ParameterizedType으로 캐스팅 가능
// ParameterizedType의 getActualTypeArguments()으로 실제 타입 파라미터의 정보를 구함
Type actualType = ((ParameterizedType) typeOfGenericSuperclass).getActualTypeArguments()[0];
// java.util.List<java.lang.String>가 출력됨
System.out.println(actualType);
슈퍼 타입 토큰 예제
// 선언부
public abstract class TypeReference<T> {
private final Type type;
protected TypeReference() {
Type superClassType = getClass().getGenericSuperclass();
if (superClassType instanceof ParameterizedType) {
this.type = ((ParameterizedType)superClassType).getActualTypeArguments()[0];
} else {
throw new IllegalArgumentException("TypeReference는 항상 실제 타입 파라미터 정보가 있어야 합니다.");
}
}
public Type getType() {
return type;
}
}
// 사용부
public class TypeSafeMap {
private final Map<Type, Object> map = new HashMap<>();
public <T> void put(TypeReference<T> k, T v) {
map.put(k.getType(), v);
}
public <T> T get(TypeReference<T> k) {
final Type type = k.getType();
final Class<T> clazz;
if (type instanceof ParameterizedType) {
clazz = (Class<T>) ((ParameterizedType) type).getRawType();
} else {
clazz = (Class<T>) type;
}
return clazz.cast(map.get(type));
}
}
Class.getGenericSuperclass() 와 ParameterizedType.getActualTypeArguments() 를 활용해서 실제 RawType 를 가져온다.
4. 프레임워크의 슈퍼 타입 토큰
이렇게 매번 직접 만들기에는 수고스럽다. 프레임워크에서도 이런 슈퍼 타입 토큰을 지원한다.
Spring 의 ParameterizedTypeReference
Spring 프레임워크에서도 동일하게 런타임 시 발생하는 타입 안정성 문제를 해결하기 위해 ParameterizedTypeReference라는 클래스를 만들었다. 매개변수화된 타입에 대한 정보를 가져올 때 사용하면 된다. 클래스 리터럴을 전달하는 대신 아래와 같이 사용하면 된다. new ParameterizedTypeReference<List<User>>()
앞에서 살펴본 구현방식과 유사하다. 보통 다른 서버에서 자원을 가져올 때 사용한다고 한다.
ObjectMapper 에서는 ParameterizedTypeReference 를 사용하지 않고 직접 위에서 소개한 것처럼 구성하여 만들고, Spring Cloud feign 에서는 자체적으로 타입을 넘겨주지 않더라도 Reflection 기능을 통해 확인한다고 한다.
'JAVA' 카테고리의 다른 글
[JAVA] Stream Collector groupingBy (0) | 2023.02.26 |
---|