토비님의 스프링 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을 쓰다 보면, 귀찮은게 한 두가지가 아닌 것 같다.
혹시나, 강의 듣다가 잘 안된다고 생각 된다면, 도움이 되었으면 좋겠습니다.
반응형
'프로그래밍 > Kotlin' 카테고리의 다른 글
Rest API와 Grpc API 서버를 하나로 서비스 할 수 있다고?! (5) | 2024.08.28 |
---|---|
ktlint lint 적용 (0) | 2024.02.08 |
kotest 관련 @Transactional rollback 안되는 이슈 (0) | 2023.12.14 |
Kapt 관련 오류시 참조 (superclass access check failed) (2) | 2023.12.08 |
build.gradle -> build.gradle.kts 변경시 (build.gradle.kts script configuration not loaded) 오류 (0) | 2023.11.24 |