꾸준하게, 차근차근

[Error] java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to... 본문

Error

[Error] java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to...

jn4624 2025. 5. 8. 23:45
반응형

Spring에서는 공통 인터페이스에 제네릭을 사용해 도메인마다 다른 타입을 주입받는 구조를 자주 활용한다.

구조도 깔끔하고 재사용성도 높아서 자주 쓰이는 방식인데, 문제는 FeignClient에 이 구조를 적용하려고 할 때 발생한다.

 

실무로는 처음 다뤄보는 FeignClient, 하루하루를 고군분투하는 이제 막 입사한 한달차 새내기 개발자는 이 구조가 적용되어 있다는 사실을 인지하지 못한채 로컬 테스트 도중 예외를 만나게 되었고, 디버깅과 구글링으로 삽질을 하다 깨달음에 도달했다.

 

따라서 이번 글에서는 삽질의 깨달음을 잊지 않기 위해서 FeignClient에서 제네릭 타입이 유지되지 않는 이유, 그로 인해 만났던 예외, 이를 해결하기 위한 방법에 대해 정리하려 한다.

 

1. 문제 상황: ResponseEntity<Page> 응답에서 캐스팅 예외 발생

ResponseEntity<Page<ResponseDTO>> response = feignClient.getPagedData();

 

FeignClient에서 위와 같이 응답을 받아 사용했다.

 

java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.example.dto.ResponseDTO...

 

그리고 응답 바디를 꺼내서 Page.getContent()로 아이템을 순회하려고 했더니 위와 같은 예외가 발생했다. 😱😱😱😱

 

문제의 핵심은 ResponseDTO로 역직렬화되길 기대했던 객체가 실제로는 Gson의 기본 Map 타입인 LinkedTreeMap으로 반환되었다는 점이었다. 이건 Java의 제네릭 타입 소거(Type Erasure)와 Gson의 동작 방식이 맞물리면서 생긴 런타임 문제였다.

 

2. 왜 이런 시련이 생긴건데?

Java는 제네릭 타입을 컴파일 타임까지만 유지하고, 런타임에는 모두 소거된다. 즉, Page<ResponseDTO>는 런타임에 단순한 Page로만 인식된다는 의미이다.

 

FeignClient는 내부적으로 JSON을 역직렬화할 때 Gson(혹은 Jackson)을 사용하는데, 이 과정에서 제네릭 타입 정보가 사라졌기 때문에 내부의 ResponseDTO는 역직렬화되지 않고 기본 타입인 LinkedTreeMap으로 변환된 것이다.

 

따라서 아래와 같은 구조로 데이터를 직접 꺼내고 캐스팅하려고 하면 예외가 발생한다. 😭😭

 

final Page<ResponseDTO> responsePage = response.getBody();
responsePage.getContent().forEach(item -> "비즈니스 로직 처리 중");

 

3. 문제 해결 전략: Page<?> -> ObjectMapper 수동 변환

이 문제를 해결하기 위해 응답을 받을 때 명확한 제네릭 타입을 지정하지 않고, 먼저 Page<?>로 응답을 받고 수동으로 역직렬화를 시도하는 전략을 사용했다.

 

1단계: Page<?>로 바디 받기

final ResponseEntity<Page<ResponseDTO>> response = feignClient.getPagedData();
final Page<?> rawPage = response.getBody();

 

이 시점에서 rawPage.getContent()에 있는 요소들은 모두 LinkedTreeMap이다.

 

2단계: ObjectMapper로 content 리스트를 수동 변환

List<ResponseDTO> content = rawPage.stream()
    .map(item -> objectMapper.convertValue(item, ResponseDTO.class))
    .collect(Collectors.toList());

 

이렇게 하면 LinkedTreeMap을 기대한 ResponseDTO 타입으로 안전하게 변환할 수 있다. 핵심은 ObjectMapper의 convertValue를 이용해 수동 역직렬화를 수행하는 것이다.

 

4. 제네릭 타입을 그대로 믿지 말자!

FeignClient와 제네릭을 함께 사용하면 겉보기에는 타입이 잘 유지될 것 같지만, 실제로 IDE에서도 유지되는 것으로 보여줬다구 😔😔😔

실제로는 런타임 타입 소거로 인해 역직렬화 문제가 발생할 수 있다.

  • Page<T>, List<T> 같은 제네릭 컬렉션을 직접 응답으로 받을 때
  • FeignClient 내부에서 Gson 또는 Jackson이 사용하는 디폴트 역직렬화 타입이 Map일 때
  • Gson 사용시 기본적으로 LinkedTreeMap, Jackson 사용시 LinkedHashMap으로 매핑

이럴 땐, 제네릭을 포기하고, Object로 받은 다음, 명시적으로 타입을 변환해주는 접근이 훨씬 안전하려나 🤔

 

END.

반응형