diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 63b72c64..dc7b0239 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -12,13 +12,13 @@ -keep public class * extends java.lang.Exception # --- DTO --- -# Gson 리플렉션으로 역직렬화되는 DTO 클래스의 필드명 보존 -# (BaseResponse, ErrorResponse 등이 Gson과 Kotlin Serialization 양쪽에서 사용됨) +# RetrofitV2/RetrofitFlow가 GsonConverterFactory를 사용하므로 +# Gson 리플렉션으로 직렬화되는 DTO 필드명 보존 필요 -keepclassmembers class com.runnect.runnect.data.dto.** { ; } # --- Retrofit + kotlin.Result --- -# Retrofit이 리턴 타입의 제네릭 정보를 리플렉션으로 읽으므로 Signature 유지 필요 -# kotlin.Result는 inline class라 R8이 타입 정보를 최적화할 수 있음 +# kotlin.Result는 inline class라 R8이 제네릭 타입 정보를 최적화함 +# Retrofit이 Call>의 타입 파라미터를 리플렉션으로 읽지 못해 CallAdapter 생성 실패 # https://github.com/square/retrofit/issues/3880 -keep class kotlin.Result { *; } -keepattributes Signature diff --git a/app/src/main/java/com/runnect/runnect/data/network/calladapter/ResultCall.kt b/app/src/main/java/com/runnect/runnect/data/network/calladapter/ResultCall.kt index 556da262..e121068d 100644 --- a/app/src/main/java/com/runnect/runnect/data/network/calladapter/ResultCall.kt +++ b/app/src/main/java/com/runnect/runnect/data/network/calladapter/ResultCall.kt @@ -1,8 +1,10 @@ package com.runnect.runnect.data.network.calladapter -import com.google.gson.Gson -import com.runnect.runnect.data.dto.response.base.ErrorResponse import com.runnect.runnect.domain.common.RunnectException +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import retrofit2.Call import retrofit2.Callback import retrofit2.Response @@ -11,7 +13,7 @@ import okio.Timeout class ResultCall(private val call: Call) : Call> { - private val gson = Gson() + private val json = Json { ignoreUnknownKeys = true } override fun execute(): Response> { throw UnsupportedOperationException("ResultCall doesn't support execute") @@ -46,23 +48,17 @@ class ResultCall(private val call: Call) : Call> { } private fun parseErrorResponse(response: Response<*>): RunnectException { - val errorJson = response.errorBody()?.string() + val errorString = response.errorBody()?.string() return runCatching { - val errorBody = gson.fromJson(errorJson, ErrorResponse::class.java) - val message = errorBody?.run { - message ?: error ?: ERROR_MSG_COMMON - } - - RunnectException( - code = errorBody.status, - message = message - ) + val jsonObject = json.parseToJsonElement(errorString.orEmpty()).jsonObject + val status = jsonObject["status"]?.jsonPrimitive?.int ?: response.code() + val message = jsonObject["message"]?.jsonPrimitive?.content + ?: jsonObject["error"]?.jsonPrimitive?.content + ?: ERROR_MSG_COMMON + RunnectException(code = status, message = message) }.getOrElse { - RunnectException( - code = response.code(), - message = ERROR_MSG_COMMON - ) + RunnectException(code = response.code(), message = ERROR_MSG_COMMON) } } diff --git a/app/src/main/java/com/runnect/runnect/data/network/calladapter/flow/FlowCallAdapter.kt b/app/src/main/java/com/runnect/runnect/data/network/calladapter/flow/FlowCallAdapter.kt index 4d473b16..786f8baf 100644 --- a/app/src/main/java/com/runnect/runnect/data/network/calladapter/flow/FlowCallAdapter.kt +++ b/app/src/main/java/com/runnect/runnect/data/network/calladapter/flow/FlowCallAdapter.kt @@ -1,11 +1,12 @@ package com.runnect.runnect.data.network.calladapter.flow -import com.google.gson.Gson -import com.runnect.runnect.data.dto.response.base.ErrorResponse import com.runnect.runnect.domain.common.RunnectException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import retrofit2.Call import retrofit2.CallAdapter import retrofit2.Callback @@ -18,7 +19,7 @@ class FlowCallAdapter( private val responseType: Type ) : CallAdapter>> { - private val gson = Gson() + private val json = Json { ignoreUnknownKeys = true } override fun responseType() = responseType // Retrofit의 Call을 Result<>로 변환 @@ -58,15 +59,18 @@ class FlowCallAdapter( } ?: Result.failure(nullBodyException) } - // Response에서 오류를 파싱하여 RunnectException 객체를 생성 private fun parseErrorResponse(response: Response<*>): RunnectException { val errorBodyString = response.errorBody()?.string() - val errorResponse = errorBodyString?.let { - gson.fromJson(it, ErrorResponse::class.java) - } - val errorMessage = errorResponse?.message ?: errorResponse?.error ?: ERROR_MSG_COMMON - return RunnectException(response.code(), errorMessage) + return runCatching { + val jsonObject = json.parseToJsonElement(errorBodyString.orEmpty()).jsonObject + val message = jsonObject["message"]?.jsonPrimitive?.content + ?: jsonObject["error"]?.jsonPrimitive?.content + ?: ERROR_MSG_COMMON + RunnectException(response.code(), message) + }.getOrElse { + RunnectException(response.code(), ERROR_MSG_COMMON) + } } companion object { diff --git a/app/src/main/java/com/runnect/runnect/data/network/interceptor/ResponseInterceptor.kt b/app/src/main/java/com/runnect/runnect/data/network/interceptor/ResponseInterceptor.kt index 5b39ebec..b4b16436 100644 --- a/app/src/main/java/com/runnect/runnect/data/network/interceptor/ResponseInterceptor.kt +++ b/app/src/main/java/com/runnect/runnect/data/network/interceptor/ResponseInterceptor.kt @@ -1,11 +1,8 @@ package com.runnect.runnect.data.network.interceptor -import com.google.gson.Gson -import com.google.gson.JsonElement -import com.google.gson.JsonObject -import com.google.gson.JsonParseException -import com.google.gson.JsonSyntaxException -import com.runnect.runnect.data.dto.response.base.BaseResponse +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Response @@ -18,14 +15,14 @@ import timber.log.Timber */ class ResponseInterceptor : Interceptor { - private val gson = Gson() + private val json = Json { ignoreUnknownKeys = true } override fun intercept(chain: Interceptor.Chain): Response { val originalResponse = chain.proceed(chain.request()) if (!originalResponse.isSuccessful) return originalResponse val bodyString = originalResponse.peekBody(Long.MAX_VALUE).string() - val newResponseBodyString = jsonToBaseResponse(bodyString)?.let { + val newResponseBodyString = extractData(bodyString)?.let { it.toResponseBody("application/json".toMediaTypeOrNull()) } ?: return originalResponse @@ -42,26 +39,18 @@ class ResponseInterceptor : Interceptor { } } - private fun jsonToBaseResponse(body: String): String? { + private fun extractData(body: String): String? { return try { - val jsonElement: JsonElement = gson.fromJson(body, JsonElement::class.java) - if (!isBaseResponse(jsonElement.asJsonObject)) { - return null - } - - val baseResponse = gson.fromJson(body, BaseResponse::class.java) - gson.toJson(baseResponse.data) - } catch (e: JsonSyntaxException) { - null // JSON 구문 분석 오류 발생 시 원래 형식을 반환 - } catch (e: JsonParseException) { - null // JSON 파싱 오류 발생 시 원래 형식을 반환 + val jsonObject = json.parseToJsonElement(body).jsonObject + if (!isBaseResponse(jsonObject)) return null + jsonObject["data"].toString() } catch (e: Exception) { - null // 기타 예외 발생 시 원래 형식을 반환 + null } } private fun isBaseResponse(jsonObject: JsonObject): Boolean { val requiredFields = listOf("status", "success", "message", "data") - return requiredFields.all { jsonObject.has(it) } + return requiredFields.all { it in jsonObject } } } \ No newline at end of file