프로그래밍/Kotlin

kotlin - jackson 관련 이슈 (토비의 스프링 6 강의)

seungdols 2024. 7. 5. 00:26

토비님의 스프링 6 강의를 듣다가, 아래와 같은 코드를 입력 했었다. (자바로 안하고, 코틀린으로 작성 했을때의 문제이다)

data class Payment(
  val orderId: Long,
  val currency: String,
  val foreignCurrencyAmount: BigDecimal,
  val exchangeRate: BigDecimal,
  val convertedAmount: BigDecimal,
  val validUntil: LocalDateTime,
)
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import java.math.BigDecimal

@JsonIgnoreProperties(ignoreUnknown = true)
data class ExRateData(
  val result: String,
  val rates: Map<String, BigDecimal>,
)
class PaymentService {
  fun prepare(
    orderId: Long,
    currency: String,
    foreignCurrencyAmount: BigDecimal,
  ): Payment {
    // 환율 가져오기 https://api.exchangerate-api.com/v4/latest/USD
    val url = URL("https://open.er-api.com/v6/latest/$currency")
    val httpURLConnection = url.openConnection() as HttpURLConnection
    val exRateData =
      httpURLConnection.inputStream.bufferedReader().use {
        val message = it.readLines().toString()

        val objectMapper = ObjectMapper()
        objectMapper.readValue(message, ExRateData::class.java)
      }
    // 금액 계산
    // 유효 시간 계산
    return Payment(orderId, currency, foreignCurrencyAmount, BigDecimal.ZERO, BigDecimal.ZERO, LocalDateTime.now())
  }
}

특히 환율 정보를 가져오는 api에서 필요한 부분은 총 두가지, rates map형태와 result 부분이라, ExRateData 클래스에서 그 외 필요 없는 부분을 위해서 @JsonIgnoreProperties(ignoreUnknown = true) 추가 해주었다.

그럼에도 불구하고, 아래와 같은 오류가 발생 했다.

Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `com.dev.seungdols.payment.vo.ExRateData` from Array value (token `JsonToken.START_ARRAY`)

그리하여, objectMapper 설정을 추가 했다.

objectMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)

그럼에도 불구하고 동일한 오류가 발생 한다.

아래처럼 역직렬화의 타입을 변경 해준다.

        val objectMapper = ObjectMapper()
        objectMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
        objectMapper.readValue(message, listOf<ExRateData>()::class.java)

하지만, 오류가 발생하게 되는데, 이는 코틀린의 특성 때문인데, listOf는 불변 list를 반환 하게 된다.

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Operation is not supported for read-only collection (through reference chain: kotlin.collections.EmptyList[0])

그래서 아래처럼 바꿔주면 된다.

class PaymentService {
  fun prepare(
    orderId: Long,
    currency: String,
    foreignCurrencyAmount: BigDecimal,
  ): Payment {
    // 환율 가져오기 https://api.exchangerate-api.com/v4/latest/USD
    val url = URL("https://open.er-api.com/v6/latest/$currency")
    val httpURLConnection = url.openConnection() as HttpURLConnection
    val exRateData =
      httpURLConnection.inputStream.bufferedReader().use {
        val message = it.readLines().toString()

        val objectMapper = ObjectMapper()
        objectMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
        objectMapper.readValue(message, mutableListOf<ExRateData>()::class.java)
      }
    // 금액 계산
    // 유효 시간 계산
    return Payment(orderId, currency, foreignCurrencyAmount, BigDecimal.ZERO, BigDecimal.ZERO, LocalDateTime.now())
  }
}

그런데, 문제가 또 발생하게 된다.

Exception in thread "main" java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.dev.seungdols.payment.vo.ExRateData (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; com.dev.seungdols.payment.vo.ExRateData is in unnamed module of loader 'app')

Jackson 라이브러리 같은 JSON 처리 라이브러리를 사용할 때, JSON 객체가 기본적으로 LinkedHashMap으로 역직렬화되는데, 이를 사용자 정의 클래스로 직접 역직렬화하려고 할 때 클래스 타입이 정확히 지정되지 않아 발생할 수 있습니다.

objectMapper.readValue(message, object : TypeReference<List<ExRateData>>() {})

위 처럼 TypeReference로 받아야 합니다.

그러나 또 문제가 발생한다.

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.dev.seungdols.payment.vo.ExRateData` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

이는 data class의 문제라 jackson에 kotlin 모듈을 등록 해주어야 한다.

class PaymentService {
  fun prepare(
    orderId: Long,
    currency: String,
    foreignCurrencyAmount: BigDecimal,
  ): Payment {
    // 환율 가져오기 https://api.exchangerate-api.com/v4/latest/USD
    val url = URL("https://open.er-api.com/v6/latest/$currency")
    val httpURLConnection = url.openConnection() as HttpURLConnection
    val exRateData =
      httpURLConnection.inputStream.bufferedReader().use {
        val message = it.readLines().toString()

        val objectMapper = ObjectMapper()
        objectMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
        objectMapper.registerKotlinModule()
        objectMapper.readValue(message, object : TypeReference<List<ExRateData>>() {})
      }.first()
    // 금액 계산
    val exchangeRate = exRateData.rates["KRW"]
    requireNotNull(exchangeRate) { "환율 정보가 없습니다." }
    val convertedAmount = foreignCurrencyAmount.multiply(exchangeRate)
    // 유효 시간 계산
    val validUntil = LocalDateTime.now().plusMinutes(30)
    return Payment(orderId, currency, foreignCurrencyAmount, exchangeRate, convertedAmount, validUntil)
  }
}

kotlin을 쓰다 보면, 귀찮은게 한 두가지가 아닌 것 같다.

혹시나, 강의 듣다가 잘 안된다고 생각 된다면, 도움이 되었으면 좋겠습니다.

반응형