오늘 배포 직전에 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) 만나면 위 체크리스트부터 다시 확인해봐야겠다..