Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat/result call integration]: API 통신 Response를 Result를 활용한 예외처리 로직으로 Refact #279

Open
wants to merge 23 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
422786b
[feat]: CustomResultCall 클래스 생성
kangyuri1114 Dec 31, 2024
aa12a0b
[feat]: CustomResultCallAdapterFactory 클래스 생성 및 Retrofit 빌더에 추가
kangyuri1114 Dec 31, 2024
13264b3
[feat]: CustomResultCallAdapterFactory 주입을 위한 @Inject 생성자 추가
kangyuri1114 Dec 31, 2024
86f4255
[feat]: CustomResultCall 클래스 생성
kangyuri1114 Dec 31, 2024
42871a5
[feat]: CustomResultCallAdapterFactory 클래스 생성 및 Retrofit 빌더에 추가
kangyuri1114 Dec 31, 2024
ef257e3
[feat]: CustomResultCallAdapterFactory 주입을 위한 @Inject 생성자 추가
kangyuri1114 Dec 31, 2024
297c7c2
Merge remote-tracking branch 'origin/feat/add-CustomCallAdapter' into…
kangyuri1114 Jan 2, 2025
24b176b
[refact]: 메서드 순서, 클래스명 변경
kangyuri1114 Jan 2, 2025
d53d8e8
[refact]: 클래스명 변경
kangyuri1114 Jan 2, 2025
2bae9fe
[add]: ErrorResponse 생성
kangyuri1114 Jan 2, 2025
bd97926
[add]: ErrorResponse 생성
kangyuri1114 Jan 2, 2025
1ec4536
[add]: provideResultCallAdapterFactory 메서드 추가
kangyuri1114 Jan 2, 2025
ad713d5
[feat]: ApiException 클래스에서 message를 errorResponse로 변경
kangyuri1114 Jan 4, 2025
5cfb0be
[feat]: getTemporaryErrorResponse 메서드 추가 후 body가 null인 경우 관련 코드 수정
kangyuri1114 Jan 4, 2025
f800a87
[fix]: 코드 정리
kez-lab Jan 4, 2025
a5eee1a
[feat]: BaseResponse<T> data 추출 확장함수 생성
kangyuri1114 Jan 11, 2025
550df51
[feat]: getUserInfo API Result를 활용한 예외처리 로직으로 수정
kangyuri1114 Jan 11, 2025
cd21296
[feat]: getBanner API Result를 활용한 예외처리 로직으로 수정
kangyuri1114 Jan 11, 2025
8056fe6
[feat]: 확장함수 getResult() 내 onFailure 시 throwable 반환이 아닌 errorResponse…
kangyuri1114 Jan 11, 2025
9557ea7
[feat]: ResultCall에서 BaseResponse<T>를 직접 처리하도록 수정
kangyuri1114 Jan 12, 2025
90fbc3c
Merge branch 'develop' into feat/result_call_integration
kangyuri1114 Feb 1, 2025
a15978a
[feat]: UserInfoResponse 기본값 추가
kangyuri1114 Feb 2, 2025
d517498
[feat]: call 데이터형 수정 Call<BaseResponse<T>> -> Call<T>
kangyuri1114 Feb 2, 2025
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
@@ -0,0 +1,5 @@
package com.hmh.hamyeonham.core.network.call

import com.hmh.hamyeonham.core.network.model.ErrorResponse

class ApiException(val errorResponse: ErrorResponse) : Exception()
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.hmh.hamyeonham.core.network.call

import com.hmh.hamyeonham.core.network.model.BaseResponse
import com.hmh.hamyeonham.core.network.model.ErrorResponse
import okhttp3.Request
import okio.IOException
import okio.Timeout
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import retrofit2.Retrofit
import timber.log.Timber

class ResultCall<T>(
private val call: Call<T>,
private val retrofit: Retrofit,
) : Call<Result<T>> {

// enqueue() : HTTP 요청을 비동기적으로 실행. 요청 결과를 전달받기 위해 Callback<Result<T>>를 인자로 받음
// enqueue() 메서드를 Custom
override fun enqueue(callback: Callback<Result<T>>) {

// Callback<Result<T>> 객체를 인자로 받아 Call 객체의 enqueue() 메서드 호출
call.enqueue(object : Callback<T> {

// onResponse() : HTTP 요청 성공 시 호출
override fun onResponse(call: Call<T>, response: Response<T>) {
val responseBody = response.body()

// response.isSuccessful : HTTP 응답 코드가 200~299 사이인지 여부 반환(성공 여부)
if (response.isSuccessful) {
// HTTP 요청은 성공했지만 body가 빈 경우
if (responseBody != null) {
callback.onResponse(
this@ResultCall,
Response.success(Result.success(responseBody))
)
} else { // HTTP 요청 성공
callback.onResponse(
this@ResultCall,
Response.success(Result.failure(RuntimeException("응답 body가 비었습니다.")))
)
}
} else { // HTTP 요청 실패
val errorResponse =
retrofit.responseBodyConverter<ErrorResponse>(
ErrorResponse::class.java,
ErrorResponse::class.java.annotations
).convert(response.errorBody()!!) ?: getBodyNullErrorResponse(response)

callback.onResponse(
this@ResultCall,
Response.success(Result.failure(ApiException(errorResponse)))
)

Timber.tag("ResultCall - onResponse").e("ErrorResponse: $errorResponse")
}
}

// onFailure() : HTTP 요청 실패 시 호출
override fun onFailure(call: Call<T>, t: Throwable) {
val message = when (t) {
is IOException -> "네트워크 연결이 불안정합니다. 다시 시도해주세요."
is HttpException -> "${t.code()} : 서버 통신 중 문제가 발생했습니다. 다시 시도해주세요."
else -> t.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해주세요."
}

// ApiException 객체 생성
callback.onResponse(
this@ResultCall,
Response.success(Result.failure(RuntimeException(message)))
)

Timber.tag("ResultCall - onFailure").e("onFailure: $message")
}
})
}

// 응답 Body가 null인 경우 임시 ErrorResponse 객체 생성
private fun getBodyNullErrorResponse(response: Response<T>) = ErrorResponse(
status = response.code(),
message = "response body is null"
)

// clone() : 동일 요청 수행하는 새로운 Call 객체 반환
override fun clone(): Call<Result<T>> {
return ResultCall(call.clone(), retrofit)
}

// execute() : HTTP 요청을 동기적으로 실행. 성공 시 Response<Result<T>> 반환, 실패 시 Exception 발생
// 주로 테스트 용도
override fun execute(): Response<Result<T>> {
return Response.success(Result.failure(RuntimeException("execute() 메서드는 사용할 수 없습니다.")))
}

// isExecuted() : HTTP 요청이 이미 실행되었는지 여부 반환.
// 한 번 이상 실행되면 true, 그렇지 않으면 false 반환
override fun isExecuted(): Boolean {
return call.isExecuted
}

// cancel() : 실행 중인 HTTP 요청 취소
override fun cancel() {
call.cancel()
}

// isCanceled() : HTTP 요청이 취소되었는지 여부 반환
override fun isCanceled(): Boolean {
return call.isCanceled
}

// request() : Call 객체에 대한 HTTP 요청 반환
override fun request(): Request {
return call.request()
}

// timeout() : Call 객체에 대한 Timeout 객체 반환
override fun timeout(): Timeout {
return call.timeout()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.hmh.hamyeonham.core.network.call

import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import javax.inject.Inject

// CallAdapter 객체 생성을 위한 Factory 클래스
class ResultCallAdapterFactory @Inject constructor(): CallAdapter.Factory() {

// retrofit 호출 결과를 변환하기 위해 호출
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
// 변환 타입 검증 (기본 Type이 Call인지, 제네릭 타입인지)
if (getRawType(returnType) != Call::class.java || returnType !is ParameterizedType) {
return null
}

// Retrofit의 반환 타입이 Call<Result<*>>인지 확인
val upperBound = getParameterUpperBound(0, returnType)

return if (upperBound is ParameterizedType && upperBound.rawType == Result::class.java) {
object : CallAdapter<Any, Call<Result<*>>> {
override fun responseType(): Type = getParameterUpperBound(0, upperBound)

// 기존 Call 객체를 ResultCall 객체로 변환
override fun adapt(call: Call<Any>): Call<Result<*>> =
ResultCall(call, retrofit) as Call<Result<*>>
}
} else {
// 반환 객체가 Call<Result<*>>이 아닌 경우 null 반환
null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.hmh.hamyeonham.core.network.call.util

import com.hmh.hamyeonham.core.network.call.ApiException
import com.hmh.hamyeonham.core.network.model.BaseResponse
import com.hmh.hamyeonham.core.network.model.ErrorResponse
import retrofit2.HttpException

fun <T> Result<BaseResponse<T>>.getResult(): Result<T> {
return this.fold(
onSuccess = { response ->
response.data?.let { Result.success(it) }
?: Result.failure(IllegalStateException("Response body is null"))
},
onFailure = { throwable ->
val errorResponse = when (throwable) {
is HttpException -> {
val errorBody = throwable.response()?.errorBody()?.string()
ErrorResponse(
status = throwable.code(),
message = errorBody ?: "Unknown server error"
)
}
else -> ErrorResponse(
status = -1,
message = throwable.message ?: "Unknown error occurred"
)
}
Result.failure(ApiException(errorResponse))
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.hmh.hamyeonham.common.qualifier.Secured
import com.hmh.hamyeonham.common.qualifier.Unsecured
import com.hmh.hamyeonham.core.network.auth.authenticator.HMHAuthenticator
import com.hmh.hamyeonham.core.network.auth.interceptor.HeaderInterceptor
import com.hmh.hamyeonham.core.network.call.ResultCallAdapterFactory
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import dagger.Binds
import dagger.Module
Expand Down Expand Up @@ -87,16 +88,22 @@ object NetModule {
.writeTimeout(15, TimeUnit.SECONDS)
.build()

@Singleton
@Provides
fun provideResultCallAdapterFactory(): ResultCallAdapterFactory = ResultCallAdapterFactory()

@Singleton
@Provides
@Secured
fun provideRetrofit(
@Secured client: OkHttpClient,
converterFactory: Converter.Factory,
resultCallAdapterFactory: ResultCallAdapterFactory,
): Retrofit = Retrofit.Builder()
.baseUrl(HMHBaseUrl)
.client(client)
.addConverterFactory(converterFactory)
.addCallAdapterFactory(resultCallAdapterFactory)
.build()

@Singleton
Expand All @@ -105,10 +112,12 @@ object NetModule {
fun provideRetrofitNotNeededAuth(
@Unsecured client: OkHttpClient,
converterFactory: Converter.Factory,
resultCallAdapterFactory: ResultCallAdapterFactory,
): Retrofit = Retrofit.Builder()
.baseUrl(HMHBaseUrl)
.client(client)
.addConverterFactory(converterFactory)
.addCallAdapterFactory(resultCallAdapterFactory)
.build()

@Module
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import retrofit2.http.GET

interface MainService {
@GET("api/v2/banner")
suspend fun getBanner(): BaseResponse<BannerResponse>
suspend fun getBanner(): Result<BaseResponse<BannerResponse>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.hmh.hamyeonham.core.network.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ErrorResponse(
@SerialName("status")
val status: Int,
@SerialName("message")
val message: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import retrofit2.http.GET

interface MyPageService {
@GET("api/v1/user")
suspend fun getUserInfo(): BaseResponse<UserInfoResponse>
suspend fun getUserInfo(): Result<BaseResponse<UserInfoResponse>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class UserInfoResponse(
@SerialName("name")
val name: String,
val name: String = "",
@SerialName("point")
val point: Int
val point: Int = 0
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.hmh.hamyeonham.data.main

import com.hmh.hamyeonham.core.network.call.util.getResult
import com.hmh.hamyeonham.core.network.main.MainService
import com.hmh.hamyeonham.domain.main.MainRepository
import com.hmh.hamyeonham.domain.main.banner.model.Banner
Expand All @@ -10,8 +11,8 @@ class DefaultMainRepository @Inject constructor(
private val mainService: MainService
) : MainRepository {
override suspend fun getBanner(): Result<Banner> {
return runCatching {
mainService.getBanner().data.toBanner()
return mainService.getBanner().getResult().mapCatching { response ->
response.toBanner()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ class DefaultUserInfoRepository @Inject constructor(
private val myPageService: MyPageService
) : UserInfoRepository {
override suspend fun getUserInfo(): Result<UserInfo> {
return runCatching {
myPageService.getUserInfo().data.toUserInfo()
}
return myPageService.getUserInfo()
.mapCatching { baseResponse ->
baseResponse.data.toUserInfo()
}
}
}
}