제네릭 타입 정보가 컴파일 후 사라지는 자바의 구현 방식(type erasure).
무슨 일이 일어나는가
// 우리가 쓴 코드
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);
// 컴파일 후 (바이트코드 수준)
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 컴파일러가 형변환을 삽입
제네릭은 컴파일 타임에만 존재하고, 런타임에는 사라진다.
List<String>과 List<Integer>는 런타임에 똑같은 List 다.
new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass() // true
왜 이렇게 만들었나 — 하위 호환성
제네릭은 자바 5에 도입됐다. 그전 코드가 산더미처럼 있었다.
선택지 ① 런타임에도 타입을 유지 (C#의 방식)
- 기존 라이브러리와 호환되지 않는다. 생태계가 갈라진다
선택지 ② 컴파일 후 지운다
- 기존 List 코드와 그대로 섞여 동작한다 ← 자바의 선택
"기존 코드를 깨지 않는다" 를 최우선으로 한 결과다. 편의를 얻고 런타임 타입 정보를 잃은 거래였다.
그래서 생기는 제약들
① 런타임에 타입을 알 수 없다
if (list instanceof List<String>) { } // ✗ 컴파일 에러
② 제네릭 타입으로 객체 생성 불가
class Box<T> {
T create() { return new T(); } // ✗ T가 뭔지 런타임에 모른다
T[] array = new T[10]; // ✗
}
→ Class<T>를 받아 clazz.getDeclaredConstructor().newInstance() 로 우회한다.
③ static 컨텍스트에서 사용 불가
class Box<T> {
static T value; // ✗ 인스턴스마다 다른 T인데 static은 하나
}
④ 오버로딩 불가
void f(List<String> a) { }
void f(List<Integer> a) { } // ✗ 소거 후 시그니처가 같아진다
⑤ 제네릭 예외 불가 — catch는 런타임에 타입을 비교해야 하기 때문이다.
힙 오염과 @SafeVarargs
List<String>[] arrays = new List<String>[10]; // ✗ 배열은 런타임 타입을 유지
배열은 소거되지 않고 제네릭은 소거되므로 둘의 성질이 어긋난다. 그래서 제네릭 배열 생성이 금지된다.
static <T> void unsafe(List<T>... lists) { // 가변인자는 내부적으로 배열
Object[] objs = lists;
objs[0] = List.of(1, 2); // 컴파일러가 못 막는다
String s = lists[0].get(0); // ClassCastException!
}
경고가 뜨며, 안전하다고 확신할 때만 @SafeVarargs를 붙인다.
소거되지 않는 곳 — 리플렉션으로 읽을 수 있다
변수의 타입은 지워지지만, 클래스·필드·메서드 선언의 제네릭 정보는 클래스 파일에 메타데이터로 남는다.
class StringList extends ArrayList<String> { }
// 부모의 타입 인자 String 은 남아 있다
이 성질을 이용한 것이 슈퍼 타입 토큰이다.
// Jackson, Spring 에서 흔히 보는 패턴
new TypeReference<List<User>>() {} // 익명 클래스를 만들어 타입을 남긴다
new ParameterizedTypeReference<List<User>>() {}
왜 {}를 붙이나 — 익명 하위 클래스를 만들면 "이 클래스는
TypeReference<List<User>>를 상속한다"는 정보가 클래스 파일에 기록된다.
런타임에 그것을 읽어 List<User>를 복원하는 것이다.
이 트릭을 설명할 수 있으면 타입 소거를 제대로 이해한 것이다.
실무 영향
// JSON 역직렬화에서 타입을 명시해야 하는 이유
objectMapper.readValue(json, new TypeReference<List<User>>() {});
// readValue(json, List.class) 는 List<LinkedHashMap> 을 준다
List<User>로 받았다고 생각했는데 원소가 LinkedHashMap이라
ClassCastException이 나는 것이 대표적인 사고다.