오늘은 살짝 신기한 에러 상황을 만나게 되어 가져와 보았습니다.
서버에서 RequestBody로 데이터를 받아 처리를 하거나 아니면 시스템상 타입을 추론하기 어려운 상황에서 발생할 수 있는 오류입니다.
문제의 해결은 문제를 이해하는 것부터 시작한다는 말이 있습니다.
그 말대로 우리는 오류를 직접 발생시켜 봄으로써 해당 오류를 더욱 자세하게 알아보도록 하겠습니다.
1. 데이터를 받을 객체 정의
시스템상 데이터 타입을 추적하지 못하는 케이스를 만들기 위해서는 데이터를 담을 수 있는 특정 타입 즉 클래스가 필요하기 때문에 하나 생성해 주도록 합니다.
public class TestData {
private String name;
private String description;
private TestField field;
}
2. 테스트
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
String data = "{\"name\": \"홍길동\", \"description\": \"데이터 파싱 테스트입니다.\", \"field\": {\"data\": \"파싱이 될까요?\"}}";
Object dd = objectMapper.readValue(data, Object.class);
TestData field = ((TestData)dd);
}
위와 같이 작성하여 테스트를 진행해 보시면 바로 아래와 같은 오류가 발생하는 것을 확인하실 수 있습니다.
문제 발생원인
여기서 왜 오류가 발생했는지 그리고 사용한 적도 없는 LinkedHashMap이 갑자기 왜 나온 것인이 의아할 수 있습니다.
이는 직렬화와 역직렬화를 도와주는 Jackson에 의해 발생하는 오류입니다.
역직렬화를 할 경우 Jackson은 데이터 타입을 받아 해당 데이터 타입으로 역직렬화를 진행하게 되는데 Jackson이 객체의 타입이 불명확하게 인지할 경우 기본적으로 LinkedHashMap을 사용하여 객체로 만들어 주게 됩니다.
실제로 위 코드에서 dd 로 정의한 객체의 타입을 찍어보면 위와 같이 LinkedHashMap으로 정의되어 있는 것을 확인할 수 있습니다.
이러한 상황은 전부 Jackson이 객체 타입을 추론하지 못해 발생한 오류 상황인 것입니다.
해결방안
해결방법은 생각보다 간단합니다.
그저 데이터를 역직렬화할 때 명확한 데이터 타입을 명시해 주면 되는 것입니다.
위에 작성한 코드로만 본다면 아래와 같이 타입을 Object처럼 포괄적인 의미가 아닌 명확한 객체 타입을 정해주면 해당 오류가 발생하지 않습니다.
{
...
TestData dd = objectMapper.readValue(data, TestData.class);
...
}
다른 에러 발생 케이스
하지만 위에 작성된 케이스는 오류를 일부러 발생하기 위해 간단하게 테스트한 경우이고 실제 업무에서는 어떤 상황일 때 발생할까요?
아무래도 실제 사용하는 케이스가 필요하다 생각합니다.
그리하여 일반적인 API호출하는 것처럼 Get API를 하나 만들어 주도록 하겠습니다.
@RestController
@RequestMapping("/error")
public class ErrorController {
@Autowired
private ObjectMapper objectMapper;
@GetMapping
public QueryData<TestData> test() throws JsonProcessingException {
String data = "{\"name\": \"홍길동\", \"description\": \"데이터 파싱 테스트입니다.\", \"field\": {\"data\": \"파싱이 될까요?\"}}";
QueryData<TestData> queryData = new QueryData<>();
queryData.setResponse(objectMapper.readValue(data, TestData.class));
return queryData;
}
}
일반적인 값을 반환해주는 API입니다.
public class QueryData<T> {
private T response;
}
Response를 Wrapping할 때 사용한 클래스입니다.
단순하게 response 를 담기 위해 만들었습니다.
@SpringBootTest
@AutoConfigureMockMvc
public class ErrorControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
public void dataParsingTest() throws Exception {
mockMvc.perform(get("/error").contentType(MediaType.APPLICATION_JSON))
.andDo((result) -> {
QueryData<TestData> queryData = objectMapper.readValue(result.getResponse().getContentAsString(Charset.defaultCharset()), QueryData.class);
//에러 발생 부분
TestData testData = queryData.getResponse();
})
.andExpect(status().isOk());
}
}
테스트 코드입니다.
SpringBoot환경에서 Mock을 사용하여 테스트 진행하였습니다.
위 테스트 코드를 실행해 보시면 response로 받은 데이터를 역직렬화하여 사용하였으나 제너릭에 명시한 타입으로 QueryData의 response가 TestData가 아닌 LinkedHashMap으로 타입이 정의되어 캐스팅할 때 에러가 발생하는 것을 확인할 수 있습니다.
제가 경험했던 오류 케이스도 이와 같았으며, Response Data가 한번 객체로 감싸져 있으며, 타입이 명확하지 않아 Object로 인지되어 역직렬화가 된경우 데이터를 꺼내 사용할 때 캐스팅이 이뤄지게 되는데 이때 에러가 발생합니다.
또다른 해결 책입니다.
ObjectMapper의 convertValue를 사용하여
TestData testData = objectMapper.convertValue(queryData.getResponse(), TestData.class);
위와 같이 코드를 수정할 경우 오류 없이 정상적으로 데이터를 꺼내 사용할 수 있으니 편한 방법으로 사용하시면 될 것 같습니다.
'Error' 카테고리의 다른 글
[Mysql] max_allowed_packet (0) | 2024.04.08 |
---|