Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ annotation class Naver
@Retention(AnnotationRetention.BINARY)
annotation class TokenInterceptor

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ResponseInterceptor

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class NaverAuthInterceptor
Expand Down
54 changes: 7 additions & 47 deletions data/src/main/kotlin/com/acon/acon/data/di/NetworkModule.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
package com.acon.acon.data.di

import android.content.Context
import com.acon.acon.data.BuildConfig
import com.acon.acon.data.datasource.local.TokenLocalDataSource
import com.acon.acon.data.error.NetworkErrorResponse
import com.acon.acon.data.error.RemoteError
import com.acon.acon.data.remote.ReissueTokenApi
import com.acon.acon.core.common.Auth
import com.acon.acon.core.common.Naver
import com.acon.acon.core.common.NaverAuthInterceptor
import com.acon.acon.core.common.NoAuth
import com.acon.acon.core.common.ResponseInterceptor
import com.acon.acon.core.common.TokenInterceptor
import com.acon.acon.data.BuildConfig
import com.acon.acon.data.SessionManager
import com.acon.acon.data.datasource.local.TokenLocalDataSource
import com.acon.acon.data.error.RemoteErrorCallAdapterFactory
import com.acon.acon.data.remote.ReissueTokenApi
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -59,7 +57,6 @@ internal object NetworkModule {
@Singleton
@Provides
fun provideAuthClient(
@ResponseInterceptor responseInterceptor: Interceptor,
@TokenInterceptor authInterceptor: Interceptor,
refreshAuthenticator: Authenticator,
): OkHttpClient {
Expand All @@ -74,7 +71,6 @@ internal object NetworkModule {
})
}
}
.addInterceptor(responseInterceptor)
.addInterceptor(authInterceptor)
.authenticator(refreshAuthenticator)
.build()
Expand All @@ -83,9 +79,7 @@ internal object NetworkModule {
@NoAuth
@Singleton
@Provides
fun provideNoAuthClient(
@ResponseInterceptor responseInterceptor: Interceptor
): OkHttpClient {
fun provideNoAuthClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
Expand All @@ -96,7 +90,7 @@ internal object NetworkModule {
level = HttpLoggingInterceptor.Level.BODY
})
}
}.addInterceptor(responseInterceptor)
}
.build()
}

Expand Down Expand Up @@ -127,6 +121,7 @@ internal object NetworkModule {
.baseUrl(BuildConfig.BASE_URL)
.client(client)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.addCallAdapterFactory(RemoteErrorCallAdapterFactory(json))
.build()
}

Expand Down Expand Up @@ -182,39 +177,4 @@ internal object NetworkModule {
reissueTokenApi: ReissueTokenApi,
sessionManager: SessionManager
): Authenticator = AuthAuthenticator(tokenLocalDataSource, reissueTokenApi, sessionManager)

@ResponseInterceptor
@Singleton
@Provides
fun providesResponseInterceptor() : Interceptor {
return Interceptor { chain: Interceptor.Chain ->
val response = chain.proceed(chain.request())

if (response.isSuccessful.not()) { // response 실패 시 실행
val errorBody = response.body?.string()
val errorResponse = try {
errorBody?.let {
Json.decodeFromString<NetworkErrorResponse>(it)
}
} catch (e: Exception) {
null
}

throw RemoteError(
statusCode = response.code,
errorCode = errorResponse?.code ?: 0,
message = errorResponse?.message ?: response.message,
httpErrorMessage = when(response.code) {
400 -> "Bad Request: 잘못된 요청입니다."
401 -> "Unauthorized: 인증되지 않은 사용자입니다."
403 -> "Forbidden: 접근 권한이 없습니다."
404 -> "Not Found: 요청한 리소스를 찾을 수 없습니다."
in 500 until 600 -> "Internal Server Error: 서버 내부 오류입니다."
else -> "Unknown Error: 알 수 없는 오류입니다."
},
)
}
response
}
}
}
100 changes: 100 additions & 0 deletions data/src/main/kotlin/com/acon/acon/data/error/ErrorCallAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.acon.acon.data.error

import kotlinx.serialization.json.Json
import okhttp3.Request
import okio.Timeout
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type

class RemoteErrorCallAdapterFactory(
private val json: Json = Json
) : CallAdapter.Factory() {

override fun get(
returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit
): CallAdapter<Any, Call<Any>>? {
if (getRawType(returnType) != Call::class.java) return null

val responseType = (returnType as ParameterizedType).actualTypeArguments[0]
return RemoteErrorCallAdapter(responseType, json)
}

private class RemoteErrorCallAdapter<R>(
private val responseType: Type, private val json: Json
) : CallAdapter<R, Call<R>> {

override fun responseType(): Type = responseType

override fun adapt(call: Call<R>): Call<R> {
return object : Call<R> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 Call을 사용하신 이유가 궁금합니다.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떤 이유라기 보다는 CallAdapter를 확장할 때, 타입으로 원래 Call를 지정해주어야 하는 것으로 알고 있습니다.

override fun enqueue(callback: Callback<R>) {
call.enqueue(object : Callback<R> {
override fun onResponse(call: Call<R>, response: Response<R>) {
if (response.isSuccessful) {
if (response.body() != null)
callback.onResponse(call, response)
else {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기에서 response의 body가 null이라는 것은 홈 화면의 가게 리스트가 emptyList 이런 것이 아닌 body자체가 null이란 뜻인건가요??

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 body가 null이라서 정상적인 상황은 아님을 나타냅니다.

callback.onFailure(
call, RemoteError(
response = response,
errorCode = 0,
message = "Empty body",
)
)
}
} else {
val errJson = response.errorBody()?.string()
val errResp = try {
errJson?.let { json.decodeFromString<NetworkErrorResponse>(it) }
} catch (_: Exception) {
null
}
callback.onFailure(
call, RemoteError(
response = response,
errorCode = errResp?.code ?: 0,
message = errResp?.message ?: response.message(),
)
)
}
}

override fun onFailure(call: Call<R>, t: Throwable) {
callback.onFailure(call, t)
}
})
}

override fun execute(): Response<R> {
val response = call.execute()
if (response.isSuccessful) return response
val errJson = response.errorBody()?.string()
val errResp = try {
errJson?.let { json.decodeFromString<NetworkErrorResponse>(it) }
} catch (_: Exception) {
null
}
throw RemoteError(
response = response,
errorCode = errResp?.code ?: 0,
message = errResp?.message ?: response.message(),
)
}

override fun clone(): Call<R> = adapt(call.clone())
override fun isExecuted(): Boolean = call.isExecuted
override fun cancel() = call.cancel()
override fun isCanceled(): Boolean = call.isCanceled
override fun request(): Request = call.request()
override fun timeout(): Timeout = call.timeout()


}
}
}
}
3 changes: 3 additions & 0 deletions data/src/main/kotlin/com/acon/acon/data/error/ErrorUtils.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.acon.acon.data.error

import com.acon.acon.domain.error.RootError
import timber.log.Timber
import kotlin.coroutines.cancellation.CancellationException

internal inline fun <R> runCatchingWith(
Expand All @@ -10,12 +11,14 @@ internal inline fun <R> runCatchingWith(
return try {
Result.success(block())
} catch (e: RemoteError) {
Timber.d("RemoteError: $e")
definedErrors.find { definedError ->
e.errorCode == definedError.code
}?.let { Result.failure(it) } ?: Result.failure(e)
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Timber.d("Throwable: $e")
Result.failure(e)
}
}
24 changes: 20 additions & 4 deletions data/src/main/kotlin/com/acon/acon/data/error/RemoteError.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
package com.acon.acon.data.error

import java.io.IOException
import retrofit2.HttpException
import retrofit2.Response

/**
* 400, 500번대 에러 발생 시 던져지는 에러
*/
data class RemoteError(
val statusCode: Int,
val response: Response<*>,
val errorCode: Int,
override val message: String,
val httpErrorMessage: String
) : IOException()
) : HttpException(response) {

val statusCode: Int = response.code()
val httpErrorMessage: String = mapHttpError(statusCode)
}

private fun mapHttpError(code: Int) = when (code) {
400 -> "Bad Request: 잘못된 요청입니다."
401 -> "Unauthorized: 인증되지 않은 사용자입니다."
403 -> "Forbidden: 접근 권한이 없습니다."
404 -> "Not Found: 요청한 리소스를 찾을 수 없습니다."
in 500 until 600 -> "Internal Server Error: 서버 내부 오류입니다."
else -> "Unknown Error: 알 수 없는 오류입니다."
}
Copy link
Copy Markdown
Member

@1971123-seongmin 1971123-seongmin Apr 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 RemoteError.kt를 사용하는 구조로 변경했을 때 실제 동작에서의 차이도 있을까요??

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아뇨 차이없습니당.
클래스 구조를 좀 더 효율적으로 변경하였습니다.