이번 글에서는 scala에서 Option[Long]과 같은 Option 타입을 사용하여 Jackson 라이브러리를 이용한 역직렬화를 할 때 발생했던 이슈에 대해서 공유할 예정이다.
링크를 참고해보면, jackson-module-scala 사용하여 역직렬화를 할 때 발생할 수 있는 Known issue 임을 확인 했다.
현재 아래와 같이 scala 2.11 버전을 사용하고 있다.
implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-scala_2.11', version: '2.9.4'
아래와 같이 문제를 재현해보자.
case class Ticket
(
id: Long,
mergeTicketId: Option[Long]
)
class SomeService {
def someMethod(value: Long): Long = {
value
}
}
class JacksonDeserializeTest extends FunSpec {
val objectMapper = buildMapper
val service = new SomeService
describe("JacksonDeserializeTest") {
it("java.lang.Integer cannot be cast to java.lang.Long") {
val json = """{"id" : 1, "mergeTicketId" : 2 }"""
val ticket = objectMapper.readValue[Ticket](json)
assert(ticket.mergeTicketId.isDefined === true)
assert(service.someMethod(ticket.mergeTicketId.get) === 2)
}
}
def buildMapper: ObjectMapper with ScalaObjectMapper = {
val mapper = new ObjectMapper() with ScalaObjectMapper
mapper.registerModule(DefaultScalaModule)
mapper
}
}
Output
java.lang.Integer cannot be cast to java.lang.Long
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.Long
at scala.runtime.BoxesRunTime.unboxToLong(BoxesRunTime.java:105)
자바와 스칼라 차이, 그리고 Jackson 처리방식을 확인해 보면 문제를 확인할 수 있다.
아래는 type T의 Generic class 예이다.
List<int> // not allowed
List<Integer> // use the boxed type
스칼라는 자바와 달리 primitive 타입을 generic class에 type argument로 사용하는 것을 지원한다.
// 여기서 Int 와 Long은 scala에서 primitive type 이다.
Option[Int]
Option[Long]
하지만, JVM 에서는 generics에 primitive type을 지원하지 않으며, 이를 Option[Object]로 나타낸다.
Jackson은 java library이며, scala module을 지원하지만
Option과 같은 타입에 대해서는 java reflection을 사용한다.
즉, Jackson은 java.lang.Integer, java.lang.Long, java,math.BigInteger, java.lang.String 과 같이 Object 타입을 사용한다.
다시 말하면, 스칼라 컴파일러는 primitive 타입으로 알고 있지만 JVM과 Jackson은 이를 Object 타입으로 다룬다.
문제가 되는 상황은 아래와 같다.
val example = objectMapper.readValue[ExampleClass](jsonString)
val longValue = example.value.get // Attempting to get the value as Long
// Long으로 값을 unbox 시도하지만, 실제 object는 java.lang.Long이 아닌 java.lang.Integer 이기 때문에 에러가 발생한다.
즉, 이 문제는 스칼라 타입과 JVM의 limitation과 Jackson's Java-centric type resolution 때문에 발생하게 된다.
위에서 언급한 버그를 해결하기 위한 방법은 아래와 같다.
더 자세한 내용은 링크를 참고하자.
Option[java.lang.Integer], Option[java.lang.Long]과 같은 타입을 사용한다.
case class Ticket
(
id: Long,
mergeTicketId: Option[java.lang.Long]
)
@JsonDeserialize 어노테이션을 추가하여 해당 타입 사용하도록 한다.
case class Ticket
(
id: Long,
@JsonDeserialize(contentAs=classOf[java.lang.Long])
mergeTicketId: Option[Long]
)
기본적으로 jackson-module-scala는 java reflection을 사용하기 때문에 scala reflection을 사용하도록 아래와 같이 추가한다.
사용방법은 jackson-scala-reflect-extensions를 objectMapper에 추가한다.
val mapper = new ObjectMapper() with ScalaReflectExtensions
mapper.registerModule(DefaultScalaModule)
Reference
https://github.com/FasterXML/jackson-module-scala/wiki/FAQ
https://github.com/FasterXML/jackson-module-scala/issues/62
https://stackoverflow.com/questions/19379967/strange-deserializing-problems-with-generic-types-using-scala-and-jackson-and-ja
https://github.com/FasterXML/jackson-module-scala/issues/213