String타입의 Json을 우아하게 DataClass로 파싱하기

SSY·2023년 1월 28일
0

Kotlin

목록 보기
2/8
post-thumbnail

시작하며

현재 진행중인 프로젝트에서 String타입의 Json 데이터를 받아오는 일이 있었다. 예를 들면 Push메시지로 들어오는 Json이나, Qr코드를 촬영하여 수신받는 Json이 있다.

그리고 이들은 각각의 Data Class로 파싱해 주어야 한다. 그리고 이를 수행하기 위한 기존의 코드는 다음과 같았다.

In Case Push : Json(String) -> Data Class

private fun String.convertToPushData(): PushData {
        return Gson().fromJson(this, object : TypeToken<PushData>() {}.type)
    }
쓰임새 : val PushData = "{... Json ...}".convertTo()

String타입의 Json을 Data Class로 파싱해주는 경우가 위 한가지 경우만 있다면? 그냥 위 함수를 재사용해주면 되었을 것이다. 하지만 그러지 않았기에 지금 이 글을 쓰는 것이다.

문제상황??

개발을 해보니 다음과 같은 사항들이 많아졌다.

In Case Register : Json(String) -> Data Class

    private fun String.convertToRegisterData(): XXXData {
        return Gson().fromJson(this, object : TypeToken<XXXData>() {}.type)
    }
쓰임새 : val XXXData = "{... Json ...}".convertToRegisterData()

In Case Request : Json(String) -> Data Class

    private fun String.convertToOOOData(): OOOData {
        return Gson().fromJson(this, object : TypeToken<OOOData>() {}.type)
쓰임새 : val OOOData = "{... Json ...}".convertToOOOData()

즉, 다른 타입의 DataClass로 파싱해주어야할 일이 생기면 위와 같은 함수를 또 다시 복붙 구현해야 한다는 크나큰 문제가 있었다.

그래서 어떻게 하면 이런 중복을 줄일 수 있을지 고민해 보았다. 그러던 와중, 지금 공부하고 있는 Kotlin In Action의 여러 문법적인 개념을 쓰면 아주 좋을것 같다는 생각이 들었다.

생각해낸 해결 방안??

적용한 개념은 다음과 같다.
1. Inline Function
2. Extends Function
3. Infix Function
4. Generic(Reified, 타입 상한)
5. Class Reflection!

결과물??

그리고 위의 이를 적용하여 최종적으로 만든 함수는 다음과 같다.

In Case All : Json(String) -> Data Class

private inline infix fun <reified T: KClass<B>, reified B: Serializable> String.convertFinal(classType: T): B {
        return Gson().fromJson(this, object : TypeToken<B>() {}.type)
    }

쓰임새

val AAAData = "{... Json ...}" convertTo AAAData::class
val BBBData = "{... Json ...}" convertTo BBBData::class
val CCCData = "{... Json ...}" convertTo CCCData::class

위 하나의 함수만으로 어떤 형식의 String타입의 Json이 넘어오든 모두 파싱할 수 있게 만들었다. 공부했던 여러 문법적인 지식들을 조합해서 만든 함수이기에 나름 뿌듯하긴 하다.

또한 파라미터로 받을 클래스의 데이터 타입은 Serializable을 상속한 dataClass만 받을것이기에 Generic클래스의 타입을 위와 같이 지정도 해줘봤다.

결과물을 만들기까지의 어려움??

하지만 위를 만들기 전 놓친 부분이 있었다. 바로, Generic의 Class Type은 런타임에는 소거된다는 사실이었다. 그래서 위 함수를 정의할땐 처음엔 아래와 같이 정의했었다.

private infix fun <T: KClass<B>, B: Serializable> String.convertFinal(classType: T): B {
        return Gson().fromJson(this, object : TypeToken<B>() {}.type)
    }

차이는 inline함수가 적용되지 않았다는 점Generic Type으로 reified를 적용해주지 않았다는 점이다. 그래서 위의 함수로 테스트를 진행하던 도중, 다음 에러를 만났었다.

처음엔 어이가 없었다. 아니.. 분명히 컴파일러는 해당 부분의 타입을 정확히 명시해 주고 있는데...? 아래와 같이 말이다.

분명히... testVal가 PushData타입이라고 했잖여.. 컴파일러 자식아.. 근데 이상하게 빌드를 해보면 쌩뚱맞은 타입인 LinkedTreeMap타입이라고 나오는 것이었다. 그리고 로그도 찍어봤고, 이전에 작성했던 메소드와 비교해 보았다.

위 사진에서, 첫 번째 줄의 type은 이번에 새로 작성한 메소드의 타입이다. 그리고 두 번째줄은 이전에 작성해놓았던 메소드였다. 즉, 로그가 최종적인 진실을 말해주고 있었다.

즉, 새로 작성한 메소드의 반환 타입은, 컴파일러 상에선 분명히 PushData였다. 고민을 헀다.

고민.........

그러다가 문득, Generic의 타입 파라미터는 런타임에 지워진다는 사실이 떠올랐다. 그리고 이를 방지해주기 위한게 바로 Generic의 reified타입 파라미터의 선언이라는 점도 떠올랐다. 더 나아가 이를 사용하려면 함수를 inline으로 선언해야 한다는 것도 떠올랐다.

그래서 함수를 개선했다. 타입 파라미터가 RunTime에 소거되지 않고 남아있도록 말이다. reified를 선언해 주었으며 이를 가능케 하기 위해 inline함수로 바꾸어줬다.

개선점
1. Generic의 타입을 reified로 선언
2. 1번을 가능하게 하기 위해 함수를 inline으로 선언

private inline infix fun <reified T: KClass<B>, reified B: Serializable> String.convertFinal(classType: T): B {
        return Gson().fromJson(this, object : TypeToken<B>() {}.type)
    }

그 결과 새로 만든 메소드의 형변환이 잘 되는걸 로그상으로 확인할 수 있었다.

작동도 잘 한다.

아쉬운 점??

하지만 아쉬운 점이 딱하나 있다. 위 함수에서 쓰이는 파라미터 'className' 이녀석이 함수 내부에서 아예 쓰이지 않는다.

컴파일러는 이를 지워달라고 하고 있다. 그래서 지워보니 당연히 함수 동작이 되지 않는다.

위 파라미터는 단순히 '타입 형태만 사용'하려는 의도로 받는 것이다. 파라미터에서 해당 객체를 호출하려고 사용하는게 아니란 뜻이다. 하지만 그럼에도 쓰이지 않고 다음과 같이 보이는게 참 찝찝하긴 하다.

마치며

이 글을 읽으시는 분들이 만약 좋은 의견이 있으시다면 댓글로 남겨주시면 참으로 감사할것 같습니다. 긴 글 읽어주셔서 고맙습니다. ( ㅡ.ㅡ )

profile
불가능보다 가능함에 몰입할 수 있는 개발자가 되기 위해 노력합니다.

0개의 댓글