공모전 준비중인 MyOrder가 거의 개발을 끝냈다. 시연동영상을 제출해야하기 때문에 시연 동영상 시나리오를 짜고 마감전에 미리 시나리오 테스트를 진행해보려 한다. 근데 제목과 같이 자꾸 예외가 발생했다. 원인을 글 맨 밑의 링크에서 쉽게 찾을 수 있었다. 이 내용을 짧게 정리하고자 한다.
@Test
public void scenarioTest() throws Exception {
List<Demand> demands = testRepository.insertAll();
//사용자 가져오기
MvcResult mvcResult = mvc.perform(get("/get-test-customer"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.jwt").exists())
.andExpect(jsonPath("$.uuid").exists())
.andReturn();
String reponse = mvcResult.getResponse().getContentAsString();
LoginResponseDTO cusLoginResponseDTO = Mapper.objectMapper.readValue(reponse, LoginResponseDTO.class);
// 가게 리스트 가져오기
StoreListRequestDTO listRequestDTO = StoreListRequestDTO.builder()
.location(PusanLocation.DONGLAE)
.offset(0)
.limit(6)
.build();
String s = Mapper.objectMapper.writeValueAsString(listRequestDTO);
MvcResult mvcResult1 = mvc.perform(post("/store/list")
.contentType(MediaType.APPLICATION_JSON)
.content(s))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").value("솔루션 메이커"))
.andReturn();
String StoreListResponseDTOJSON = mvcResult1.getResponse().getContentAsString();
List<StoreListResponseDTO> li = (List<StoreListResponseDTO>) Mapper.objectMapper.readValue(StoreListResponseDTOJSON, List.class);
System.out.println("테스트 : "+li.get(0));
System.out.println(li.getClass()); //class java.util.ArrayList
System.out.println(li.get(0).getClass());//웃긴건 readValue(), li.get(0) 할 때가 아니라 여기서 .getClass()하면 예외남.
<원인과 해결책>
Jackson은 serialize/deserialize JSON, XML 라이브러리이다.
Mapper.objectMapper.readValue(json, List<StoreListResponseDTO>.class) 이렇게 코드를 작성할 수 없다. 그래서 Jackson은 어떤 List에 넣어야할지 알지 못한다.
예외메시지를 살펴 보자. java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class pnu.problemsolver.~ 이 말은 즉슨 LinkedHashMap을 내가 지정한 StoreListReponseDTO 타입으로 바꿀 수 없다는 뜻이다.
왜 LinkedHashMap일까? 타입이 명시되어 있지 않을 때 Jackson은 LinkedHashMap을 기본으로 사용한다. 그리고 readValue()에 List.class를 넘겼을 때 jack은 ArrayList를 클래스를 만드는 것을 위의 코드에서 확인할 수 있다. 즉 두 사실을 종합하면 jackson은 readValue()의 결과로 ArrayList<LinkedHashMap> 을 만들어 낸다. 이는 아래의 사진에서도 확인할 수 있다. 변수 li의 타입은 ArrayList이고 각각의 요소는 LinkHashMap이라고 되어있다.
이제 해결책을 알아보자. com.fasterxml.jackson.core.type.TypeReference<T>객체를 함께 넘겨주면 된다. 위의 코드를 아래로 바꿔주면 된다. List.class 대신 TypeReference객체를 넘긴다. 그럼 잘 동작함.
List<StoreListResponseDTO> li = Mapper.objectMapper.readValue(StoreListResponseDTOJSON, new TypeReference<List<StoreListResponseDTO>>() {});
//또는 아래와 같이 타입을 생략할 수도 있다.
List<StoreListResponseDTO> li = Mapper.objectMapper.readValue(StoreListResponseDTOJSON, new TypeReference<>() {});
근데 갑자기 든 생각이 Map을 사용자 정의 클래스로 바인딩 못시켜주나?? 할 수 있을 것 같은데... 그래서 찾아봤다... 당연히 된다. Map<->Object 양방향 가능하다.
주의할 점이 Object에는 getter가 필요하다. setter는 없어도 된다.
Getter가 없을 때 아래와 같은 예외를 띄운다. 좀 직관적이진 않은듯.
No serializer found for class pnu.problemsolver.myorder.Tmp1 and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
@Test
public void test() throws JsonProcessingException {
Tmp1 t = new Tmp1(1, 2);
Map map1 = Mapper.objectMapper.convertValue(t, Map.class);
System.out.println(map1);//{a=1, b=2}
System.out.println(map1.getClass());//class java.util.LinkedHashMap
HashMap<String, Object> map = new HashMap();
map.put("a", 1);
map.put("b", 2);
System.out.println(map);//{a=1, b=2}
Tmp1 tmp1 = Mapper.objectMapper.convertValue(map, Tmp1.class);
System.out.println(tmp1);//Tmp1(a=1, b=2)
}
}
@NoArgsConstructor
@Data//Getter가 필수이다. Getter없으면 예외 띄움
class Tmp1 {
Integer a;
Integer b;
public Tmp1(int a, int b) {
this.a = a;
this.b = b;
}
}
Map- Object는 변환가능하다. 그럼 왜 앞서 예외를 띄운 것일까? 우리는 Jackson의 convert()함수를 사용한 것이 아니라 java기본 캐스팅을 사용했다. 상속관계가 아니기 때문에 캐스팅 되지 않는 것은 당연함.
아래의 링크에 여러 방법이 있으니 참조
https://www.baeldung.com/jackson-linkedhashmap-cannot-be-cast
Jackson: java.util.LinkedHashMap cannot be cast to X | Baeldung
Learn why the "java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to X" exception occurs and how to solve the problem
www.baeldung.com
<요약>
- TypeReference 사용
- CollectionType colType = objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, StoreListResponseDTO.class); 사용
- objectMapper.convertValue()로 Map<->object변환해서 사용
- Creating a Generic Deserialization Method //좋은 아이디어!! 읽어보자.
- xml사용
아래는 jackson pdf라는데 언젠가 읽어보자.!!
'코딩 > Java, SpringBoot' 카테고리의 다른 글
presentation layer, service layer 명확히 구분 짓는 코드 (0) | 2022.08.13 |
---|---|
이건 해결해야겠다. save() 2번 호출했을 때 왜 연관된 엔티티가 영속상태가 아닌데도 save()에서 예외가 발생하지 않는가? (0) | 2022.08.13 |
형식을 알지 못하는 JSON을 클래스로 만들 수 있을까? (0) | 2022.08.05 |
[SpringBoot] 엔티티에는 setter를 두지 않는다. 확장성 있는 함수구성 (0) | 2022.08.05 |
SpringBoot static final에 DI하기? (0) | 2022.08.02 |