728x90

오늘은 살짝 신기한 에러 상황을 만나게 되어 가져와 보았습니다.

서버에서 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);

위와 같이 코드를 수정할 경우 오류 없이 정상적으로 데이터를 꺼내 사용할 수 있으니 편한 방법으로 사용하시면 될 것 같습니다.

728x90

'Error' 카테고리의 다른 글

[Mysql] max_allowed_packet  (0) 2024.04.08

+ Recent posts