0. 들어가며


ObjectMapper 관련 코드를 쓰다보면 아래와 같이 TypeReference를 사용하곤한다.

ObjectMapper objectMapper = jacksonObjectMapper();
objectMapper.readValue(json, new TypeReference<List<User>>() {});

자바의 제네릭은 컴파일 이후 타입소거가 이루어지는데 위와 같은 코드는 어떻게 동작할 수 있는거지 싶어 해당 내용에 대해 찾아본 내용을 정리한다.

1. 타입 소거 간단 요약


자바는 제네릭을 도입할 때 하위 호환성을 위해 타입 소거라는 메커니즘을 도입했다. 컴파일 이후 제네릭에 bound가 있었다면 해당 타입으로, 그렇지 않다면 Object 타입으로 치환되며, 이로 인해 바이트 코드 상에서는 제네릭 정보가 사라진다. 이러한 소거 과정을 바로 타입 소거라고 한다. 이러한 이유로 인해 JVM은 객체 레벨에서 제네릭 타입 인자를 알 수 없다.

2. 그럼 TypeReference는 왜 타입 소거가 안되는 것 같지?


앞서 말한 것처럼 자바는 컴파일 이후 제네릭 타입 정보가 모두 사라진다. 즉, 런타임에는 List<String>, List<User>와 같은 정보가 존재하지 않고 똑같은 List 타입일 뿐이다. 그런데 처음에 소개했던 jacksonObjectMapper 코드는 어떻게 List 안에 타입이 User인 줄 알고 User객체로 역직렬화를 하는걸까?

일단 TypeReference를 쓰지 않고 해보려고 아래와 같이 써볼 수 있는데.. 이러한 코드는 동작하지 않는다.

ObjectMapper objectMapper = jacksonObjectMapper();
List<User> users = objectMapper.readValue(json, List<User>.class); // ❌ 컴파일 에러

왜냐하면 자바에서 .class는 실제 런타임 타입을 가리키는데 List<User>는 컴파일러 수준에서만 존재하는 제네릭 타입 표현식이기 때문에 리터럴 형태(List<User>.class)로는 접근할 수 없다.

그럼 아래와 같이 쓰면?

ObjectMapper objectMapper = jacksonObjectMapper();
List<User> users = objectMapper.readValue(json, List.class); // ⚠️ 컴파일은 통과

이렇게 쓰면 컴파일은 된다. 하지만 예상하는 동작과는 다르게 처리된다.