thdwoqor / stable-coin-checker

0 stars 0 forks source link

Json 데이터 변환시 알수없는 property를 무시하는 문제 해결 #12

Closed thdwoqor closed 2 weeks ago

thdwoqor commented 2 weeks ago

문제

거래소와 웹소켓 연결을 통해 가격 데이터를 수신하는 과정에서, ObjectMapper를 사용한 역직렬화(deserialize) 중 문제가 발생했습니다.

거래소에서 가격뿐만 아니라 다양한 데이터를 전송하기 때문에, 특정 형식의 Response 타입과 일치하지 않을 경우 역직렬화에 실패하도록 설계했습니다.

실패 시 Optional.empty()를 반환하도록 코드를 작성했으나, 예상과 달리 원하는 형식이 아닌 Response에서 Optional.empty()가 반환되지 않고, Response 객체의 필드가 null로 채워진 채 반환되어 문제가 발생했습니다.

public <T> Optional<T> deserialize(String payload, Class<T> valueType) {
    try {
        T t = mapper.readValue(payload, valueType);

        return Optional.of(t);
    } catch (Exception e) {
        return Optional.empty();
    }
}
String message = "{\"status\":\"UP\"}"; //타입이 맞지 않는 경우
Optional<UpbitWebSocketResponse> deserialize = jsonUtils.deserialize(message, UpbitWebSocketResponse.class);

//UpbitWebSocketResponse(tradePrice=null, timestamp=null, code=null)

문제2

문제를 해결하기 위해 테스트 코드를 작성하고 실행해 보았으나, 실제 운영 상황과는 다르게 동작하는 문제가 발생했습니다. 실제 운영 환경과 동일한 상황이었다면 해당 테스트 코드는 실패해야 하지만, 테스트 실행 결과 성공으로 나타났습니다.

class JsonUtilsTest {

    @Test
    void 타입이_맞지않는경우_empty_를_반환한다() {
        final JsonUtils jsonUtils = new JsonUtils(new ObjectMapper());
        String message = "{\"status\":\"UP\"}";
        Optional<UpbitWebSocketResponse> deserialize = jsonUtils.deserialize(message, UpbitWebSocketResponse.class);

        Assertions.assertThat(deserialize).isNotPresent();
    }
}

원인

Jackson의 ObjectMapper에는 알 수 없는 프로퍼티가 하나라도 발견되면 역직렬화에 완전히 실패하는 이상한 기본 설정이 있습니다. 이는 서버가 여전히 예상 유형의 인스턴스를 성공적으로 생성할 수 있으므로 포스텔의 법칙에 위배됩니다. 사실 이런 종류의 탄력적인 동작은 사람들이 스키마가 지배적인 XML 세계 대신 JSON을 선택하는 이유이기도 합니다. https://github.com/spring-projects/spring-framework/issues/16510

제 주문에 제가 주문하지 않은 알 수 없는 물건이 들어 있다면, 네, 아마 반품할 겁니다. 하지만 확실히 그것에 대해 알고 싶을 겁니다. 알 수 없는 물건이 들어오면 예외를 던지는 것은 완벽하게 타당한 일입니다. 사실, 어떤 종류의 물건이든 맹목적으로 받아들이는 것은 종종(항상은 아니지만) 매우 나쁜 전략입니다. 제 경험에 따르면, 그것은 문제를 숨기고 더 일찍 찾았어야 할 것들을 찾는 것을 지연시킬 것입니다. https://github.com/FasterXML/jackson-databind/issues/493 https://groups.google.com/g/jackson-user/c/5TT1IpakGHU?pli=1

결론적으로

Spring Boot 진영에서는 API의 견고함의 원칙을 지니는 것을 중요시합니다. Jackson 진영에서는 API는 명확해야하고 문서와 일치되어야함을 중요시합니다.

서로 다른 입장 차이때문에 Spring Boot에서는 ObjectMapper를 그대로 사용하지 않고 커스터마이징하여 빈으로 등록하기 때문에 문제가 발생했습니다.

해결

ObjectMapper를 한번더 커스텀해서 Bean을 등록하는 방식은 이후에 협업 과정 등에서 야기할 가능성이 있기때문에 Response에 @JsonIgnoreProperties를 붙여 주는 방식으로 문제를 해결했습니다.

참고

https://sabarada.tistory.com/237 https://sabarada.tistory.com/236