https://angangmoddi.tistory.com/339
[Android] HttpsURLCoonection을 사용한 인터넷 통신
interface HttpClient { companion object { const val DEFAULT_CONNECTION_TIMEOUT = 15_000 const val DEFAULT_READ_TIMEOUT = 15_000 } fun doRequest( method: RequestMethod, path: String, outClass: KClass, headers: List = emptyList(), body: IN? = null ): OUT fun
angangmoddi.tistory.com
위 글에 이어서 라이브러리 없이 서버 통신에서 사용할 직렬화/역직렬화 기능을 구현해보자.
interface Converter {
companion object {
fun KClass<out Any>.isPrimitiveType(): Boolean {
return this == Int::class || this == Long::class || this == Double::class ||
this == Float::class || this == Char::class || this == Byte::class ||
this == Short::class || this == Boolean::class || this == String::class
}
}
fun<T: Any> serialize(input: T): String
fun<T: Any> deserialize(input: String, kClass: KClass<T>): T
}
기본 구조는 호출부에서 Converter 구현체를 지정할 수 있도록 구성했기 때문에 Converter interface를 추가한다.
isPrimitive함수는 변환시에 기본 타입 데이터인지 확인하는 용도로 사용한다.
internal class MyJsonConverter(
private val serializer: Serializer
private val deserializer: Deserializer
): Converter {
override fun<T: Any> serialize(input: T): String =
serializer.toJson(input)
override fun<T: Any> deserialize(input: String, kClass: KClass<T>): T =
deserializer.fromJson(input, kClass)
}
internal interface Serializer {
fun<T: Any> toJson(obj: T): String
}
internal interface Deserializer {
fun<T: Any> fromJson(json: String, kClass: KClass<T>): T
}
구현체는 생성자에서 serializer, deserializer를 주입받아 유연하게 사용할 수 있도록 구성한다.
internal class MySerializer : Serializer {
private fun convertValueToJson(value: Any?): Any? {
return when (value) {
null -> JSONObject.NULL
is Collection<*> -> {
val jsonArray = JSONArray()
value.forEach { element ->
jsonArray.put(convertValueToJson(element))
}
jsonArray
}
is Map<*, *> -> {
val jsonObject = JSONObject()
value.forEach { (k, v) ->
if (k is String) jsonObject.put(k, convertValueToJson(v))
}
jsonObject
}
else -> {
val kClass: KClass<out Any> = value::class
if (shouldConvert(kClass)) {
@Suppress("UNCHECKED_CAST")
toJSONObjectRecursive(value, kClass as KClass<Any>)
} else {
value
}
}
}
}
private fun shouldConvert(kClass: KClass<out Any>): Boolean {
return when {
kClass.isData -> true
kClass.isPrimitiveType() -> false
else -> true
}
}
}
override fun<T: Any> toJson(obj: T): String {
val jsonObject = toJSONObjectRecursive(obj, obj::class)
return jsonObject.toString(2)
}
json으로 변환시에는 일단 객체를 JsonObject로 변환한 후, 이를 string으로 변환한다.
private fun<T: Any> toJSONObjectRecursive(obj: T): JSONObject {
val kClass = obj::class
val jsonObject = JSONObject()
kClass.memberProperties
.forEach { prop ->
val rawValue = prop.get(obj, prop)
val convertedValue = convertValueToJson(rawValue)
jsonObject.put(prop.name, convertedValue)
}
return jsonObject
}
여기선 직렬화를 위해 리플렉션을 사용한다.
오브젝트의 kClass에서 memberProperties를 순회하면서
각 속성에 대해 따로 직렬화를 수행해서 jsonObject에 추가한다.
private fun convertValueToJson(value: Any?): Any? {
return when (value) {
null -> JSONObject.NULL
is Collection<*> -> {
val jsonArray = JSONArray()
value.forEach { element ->
jsonArray.put(convertValueToJson(element))
}
jsonArray
}
is Map<*, *> -> {
val jsonObject = JSONObject()
value.forEach { (k, v) ->
if (k is String) jsonObject.put(k, convertValueToJson(v))
}
jsonObject
}
else -> {
val kClass: KClass<out Any> = value::class
if (shouldConvert(kClass)) {
@Suppress("UNCHECKED_CAST")
toJSONObjectRecursive(value, kClass as KClass<Any>)
} else {
value
}
}
}
}
private fun shouldConvert(kClass: KClass<out Any>): Boolean {
return when {
kClass.isData -> true
kClass.isPrimitiveType() -> false
else -> true
}
}
json 변환은 총 5개 케이스로 나뉜다.
1. 데이터가 null인 경우 null 반환
2. list인 경우 각 element에 대해 직렬화 수행
3. map인 경우 그대로 jsonObject로 변환
4. shouldConvert를 통해 변환이 필요한 객체인 경우 직렬화 로직을 다시 수행한다.
5. 변환이 필요 없는 경우 값을 그대로 반환한다.
override fun<T: Any> fromJson(json: String, kClass: KClass<T>): T {
val jsonObj = JSONObject(json)
return fromJSONObject(jsonObj, kClass)
}
역직렬화시에는 json string을 먼저 jsonObject로 변환 후, 이를 T객체로 변환하는 작업을 수행한다.
이때 런타임시에 T에 대한 타입 정보는 런타임에 소거되기 떄문에 kClass를 따로 넣어준다.
fun<T: Any> fromJSONObject(jsonObject: JSONObject, kClass: KClass<T>): T {
val constructor = kClass.primaryConstructor // 데이터 클래스의 주 생성자
?: throw IllegalArgumentException("Class ${kClass.simpleName} must have a primary constructor for deserialization.")
val args = mutableMapOf<KParameter, Any?>()
constructor.parameters.forEach { param ->
val jsonKey = param.name ?: return@forEach
if (jsonObject.has(jsonKey)) {
val jsonValue = jsonObject.get(jsonKey)
val convertedArg = convertJsonValue(jsonValue, param.type)
args[param] = convertedArg
} else if (!param.isOptional && !param.type.isMarkedNullable) {
throw IllegalStateException("Missing required property: ${param.name}")
}
}
return constructor.callBy(args)
}
역직렬화에서도 마찬가지로 리플렉션을 사용하는데,
객체 생성을 위해 해당 데이터타입의 주 생성자를 먼저 가져온다.
그리고 주 생성자의 parameter를 순회하며 name에 대칭되는 값을 json에서 하나씩 꺼내서 사용한다.
이 값을 covnerJsonValue를 통해 객체에 추가할 실제 데이터로 변환해준다.
private fun convertJsonValue(jsonValue: Any?, kType: KType): Any? {
if (jsonValue == null || jsonValue == JSONObject.NULL) return null
val targetKClass = kType.classifier as? KClass<*> ?: return jsonValue
return when {
targetKClass.isSubclassOf(Collection::class) && jsonValue is JSONArray -> {
val elementType = kType.arguments.firstOrNull()?.type ?: return emptyList<Any?>()
val list = mutableListOf<Any?>()
for (i in 0 until jsonValue.length()) {
val elementJson = jsonValue.get(i)
list.add(convertJsonValue(elementJson, elementType))
}
list
}
shouldConvert(targetKClass) -> {
if (jsonValue is JSONObject) {
@Suppress("UNCHECKED_CAST")
fromJSONObject(jsonValue, targetKClass as KClass<Any>)
} else {
jsonValue
}
}
else -> jsonValue
}
}
private fun shouldConvert(kClass: KClass<out Any>): Boolean {
return when {
kClass.isData -> true
kClass.isPrimitiveType() -> false
else -> true
}
}
일단 kType을 사용해서 데이터의 kClass를 얻고,
이후 처리는 3개 케이스로 나뉜다.
1. 데이터가 Collection 또는 JSONArray인 경우 각 element에 대해 convertJsonValue를 수행한다.
2. shouldConverter에 따라 변환이 필요한 경우 역직렬화 로직을 처음부터 수행한다.
3. 변환이 필요 없는 경우 값을 그대로 반환한다.
마지막으로 위 함수를 통해 구해낸 args를 처음에 얻은 constructor에 대입하여 객체를 생성하고 반환한다.
'안드로이드 > 개발관련(Kotlin)' 카테고리의 다른 글
| [Android] gradle 관리를 위한 build-logic 패턴 (0) | 2025.11.30 |
|---|---|
| [Android] 라이브러리 없이 클라이언트에서 테스트 서버 구현 (0) | 2025.11.30 |
| [Android] HttpsURLCoonection을 사용한 인터넷 통신 (0) | 2025.11.30 |
| [Android] jvmTarget 지정 방식이 deprecated 된 이유 (0) | 2025.11.29 |
| [Android] TYPESAFE_PROJECT_ACCESSORS (1) | 2024.10.09 |