거래소와 웹소켓 연결을 통해 가격 데이터를 수신하는 과정에서, 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
문제
거래소와 웹소켓 연결을 통해 가격 데이터를 수신하는 과정에서, ObjectMapper를 사용한 역직렬화(deserialize) 중 문제가 발생했습니다.
거래소에서 가격뿐만 아니라 다양한 데이터를 전송하기 때문에, 특정 형식의 Response 타입과 일치하지 않을 경우 역직렬화에 실패하도록 설계했습니다.
실패 시 Optional.empty()를 반환하도록 코드를 작성했으나, 예상과 달리 원하는 형식이 아닌 Response에서 Optional.empty()가 반환되지 않고, Response 객체의 필드가 null로 채워진 채 반환되어 문제가 발생했습니다.
문제2
문제를 해결하기 위해 테스트 코드를 작성하고 실행해 보았으나, 실제 운영 상황과는 다르게 동작하는 문제가 발생했습니다. 실제 운영 환경과 동일한 상황이었다면 해당 테스트 코드는 실패해야 하지만, 테스트 실행 결과 성공으로 나타났습니다.
원인
결론적으로
Spring Boot 진영에서는 API의 견고함의 원칙을 지니는 것을 중요시합니다. Jackson 진영에서는 API는 명확해야하고 문서와 일치되어야함을 중요시합니다.
서로 다른 입장 차이때문에 Spring Boot에서는 ObjectMapper를 그대로 사용하지 않고 커스터마이징하여 빈으로 등록하기 때문에 문제가 발생했습니다.
해결
ObjectMapper를 한번더 커스텀해서 Bean을 등록하는 방식은 이후에 협업 과정 등에서 야기할 가능성이 있기때문에 Response에
@JsonIgnoreProperties
를 붙여 주는 방식으로 문제를 해결했습니다.참고
https://sabarada.tistory.com/237 https://sabarada.tistory.com/236