본문 바로가기
공부방/오류

java.lang.ClassCastException

by SmartCow 2025. 11. 12.

오늘 배포 직전에 java.lang.ClassCastException: kr.co.aaaa.vo.userVO cannot be cast to java.util.Map 이런 오류가 발생하더라. 빨리 배포해야하는데 눈앞이 아찔... 다행히 금방 수정해서 오류없이 배포완료!

1. 에러 요약 & 재현 코드

이 에러는 말 그대로 userVO라는 객체를 java.util.Map으로 캐스팅하려다가 실패한 거야. java.lang.ClassCastException: kr.co.aaaa.vo.userVO cannot be cast to java.util.Map 처럼 정확히 찍히고, 보통은 컨트롤러에서 응답 만들 때나, 서비스에서 레거시 유틸을 부를 때, 또는 MyBatis에서 결과를 Map으로 받는다고 착각했을 때 오류발생.

에러 메시지 해석

 java.lang.ClassCastException: kr.co.aaaa.vo.userVO cannot be cast to java.util.Map at com.example.DemoController.list(DemoController.java:42) 

언제 오류가 발생하는지

  • Object로 받은 값을 무심코 (Map)으로 다운캐스팅
  • 제네릭스 빠진 List/Map 사용으로 실제 타입이 userVO인데 Map으로 오인
  • Jackson으로 역직렬화할 때 Map 기대했지만 VO로 매핑되거나 그 반대
  • MyBatis에서 resultType="map"인 줄 알고 사용했지만 실제 매핑은 VO

최소 재현 코드

 class userVO { String name; int count; /* getter/setter */ }

Object obj = new userVO();
// 잘못된 캐스팅
Map<String, Object> map = (Map<String, Object>) obj; // 여기서 ClassCastException

로그 찍는 요령

 log.info("type={}", obj == null ? "null" : obj.getClass().getName()); 

오류나기 직전에 타입 찍어보면 바로 감 잡힘. 운영에서도 이 한 줄로 원인을 빠르게 찾음.

2. 근본 원인 4가지

① 잘못된 캐스팅(Downcast)

컴파일은 되지만 런타임에 타입이 맞지 않아 실패. instanceof 체크 없이 바로 (Map)으로 캐스팅한 전형적인 케이스.

② 제네릭스 누락/Raw 타입

 List list = service.getThings(); // Raw 타입 Object first = list.get(0); // 실제로는 userVO Map m = (Map) first; // 여기서 펑 

③ JSON 파싱 타입 미스매치

 // 기대: Map Map<String, Object> m = objectMapper.readValue(json, Map.class); // 실제: userVO로 바인딩되거나 반대 상황 → 이후 캐스팅에서 터짐 

④ MyBatis resultType/resultMap 혼선

 <select id="selectItem" resultType="kr.co.aaaa.vo.userVO"> ... </select> // 자바쪽에서 Map으로 받으면 당연히 ClassCastException 

3. 바로 적용 가능한 해결 패턴

패턴 A: 컨트롤러/서비스 반환 타입 고정

 //나쁜 예 
Object find() { return service.getSomething(); }

// 좋은 예
userVO find() { return service.getSomething(); }

패턴 B: Map 대신 DTO

JSON 응답은 DTO로 잡고, 필요하면 Map.of(...)로 래핑.

 record ResultDto(String name, int count) {} 

패턴 C: Jackson 타입 안정화

 Map<String, Object> m = objectMapper.readValue(json, new com.fasterxml.jackson.core.type.TypeReference<Map<String,Object>>(){}); 

패턴 D: MyBatis 매핑 정정

 // VO로 받을 거면 <select id="selectItem" resultType="kr.co.aaaa.vo.userVO"> ... </select>

// Map으로 받을 거면 ... 

 

 

 

 

 

4. 실무 체크리스트 & 코드 예제

① 컨트롤러 계층

 @RestController @RequestMapping("/api/items") class ItemController {

private final ItemService service;

@GetMapping("/{id}")
public ResponseEntity get(@PathVariable Long id) {
userVO vo = service.find(id);
// 아래처럼 Map으로 캐스팅 금지
// Map<String,Object> m = (Map<String,Object>) (Object) vo; // ← 여기서 java.lang.ClassCastException: kr.co.aaaa.vo.userVO cannot be cast to java.util.Map
return ResponseEntity.ok(vo);
}
}

② 서비스/리포지토리 계층

 @Service class ItemService { private final ItemMapper mapper;

public userVO find(Long id) {
return mapper.selectItem(id); // Mapper가 VO를 리턴하도록 계약
}
}

③ MyBatis 매퍼

 public interface ItemMapper { userVO selectItem(Long id); } 
 <mapper namespace="com.example.ItemMapper"> <resultMap id="ResultMap" type="kr.co.aaaa.vo.userVO"> <id property="id" column="id"/> <result property="name" column="name"/> <result property="count" column="count"/> </resultMap> <select id="selectItem" parameterType="long" resultMap="ResultMap"> SELECT id, name, count FROM items WHERE id = #{id} </select> </mapper> 

④ 테스트/로깅

 @Test void typeCheck() { Object obj = service.find(1L); System.out.println(obj.getClass()); // class kr.co.aaaa.vo.userVO assertTrue(obj instanceof userVO); } 

5. FAQ & 마무리

Q1. 로컬은 되고 서버에서만 java.lang.ClassCastException: kr.co.aaaa.vo.userVO cannot be cast to java.util.Map?

빌드 아티팩트/의존성 버전 차이, 프로파일별 매퍼 설정 차이(resultType 누락) 확인. 서버에선 AOP로 응답 포맷터가 Map 전제하는 경우도 봤어.

Q2. 기존 코드가 전부 Map 기반인데 당장 다 못 고치면?

 // 임시 래핑 static Map<String,Object> wrap(Object o){ return java.util.Collections.singletonMap("data", o); } // 컨트롤러에서 ResponseEntity.ok(wrap(vo)) 형태로 점진적 이행 

Q3. Map과 VO 둘 다 지원하려면?

 if (obj instanceof Map) { handleMap((Map<String,Object>) obj); } else if (obj instanceof userVO) { handleVo((userVO) obj); } else { throw new IllegalArgumentException("지원하지 않는 타입: " + obj.getClass()); } 

Q4. 한 줄 결론?

계약 먼저, 캐스팅은 나중. 반환 타입을 명확히 고정하고 매퍼/직렬화 타입을 일치시키면 java.lang.ClassCastException: kr.co.aaaa.vo.userVO cannot be cast to java.util.Map 는 바로 사라진다.

내 기준으로 실제 이슈를 밟으면서 느낀 점 정리했고, 중간에 헷갈렸던 MyBatis 매핑 포인트는 문서 다시 보고 고쳐서 통과. 같은 에러(java.lang.ClassCastException: kr.co.aaaa.vo.userVO cannot be cast to java.util.Map) 만나면 위 체크리스트부터 다시 확인해봐야겠다..