-
Notifications
You must be signed in to change notification settings - Fork 1
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
refactor: CallAdapter 적용 및 검증 테스트 작성 #558 #574
Changes from all commits
4e11110
c992a87
d17777f
3f60c5b
eb2ef9f
ffe84f1
2ce3a7d
431975d
b058b24
d2e5e9d
84b464f
535a3e9
b1e90fa
b334d4a
5c815fc
fc26942
7501253
3b534c2
9b9ad03
f846206
b32ecc8
be27563
7419d07
8fcf08d
a9b13eb
63a9d7f
d01b05c
d5c359c
334d563
f1605ee
ca5b48e
76d4517
6e7253b
a59d4d8
06ebf59
202f50c
ea6d815
037b169
1ecc3d8
7f398af
bead6ca
0ff8359
ae28100
2e021d6
908db7e
10f23b6
3f41b05
8e1e687
7768209
ce8cfe5
2ece854
e26952f
3b8ff3d
178357c
7ebc13b
f9b5dfd
f49d9f9
05cface
35e890e
805b7fa
c8dc262
31e9c9f
e841063
eeaf574
9e1b3cf
14a7886
f1d0c31
07eebb6
8c99e4b
3dca2c4
2d610db
b2b759f
9361ce8
d3bfb04
102d843
36ec3e0
936f8b7
35ba213
5eec9a6
762a010
63b2b33
5e96090
e869b50
b533341
c414f4e
12452ae
1d5e3e7
7650d8c
71fee22
b39c759
51954c3
a554e09
5db7be4
8a26c95
084f788
372aab1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package com.on.staccato.data | ||
|
||
import com.on.staccato.data.dto.Status | ||
|
||
sealed interface ApiResult<T : Any> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sealed interface ResponseResult<T : Any> {
class Success<T : Any>(val data: T) : ResponseResult<T>
class ServerError<T : Any>(val status: Status, val message: String) : ResponseResult<T>
class Exception<T : Any>(val e: Throwable, val message: String) : ResponseResult<T>
} 기존에는 위처럼 ResponseResult의 중괄호 안에 포함되는 구조였는데, 모든 클래스가 최상위 레벨에 정의되면서 직접 접근이 가능해졌네요! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sealed interfacce 의 장점을 잘 살린 것 같습니다! |
||
|
||
class Success<T : Any>(val data: T) : ApiResult<T> | ||
|
||
class ServerError<T : Any>(val status: Status, val message: String) : ApiResult<T> | ||
|
||
sealed class Exception<T : Any> : ApiResult<T> { | ||
class NetworkError<T : Any> : Exception<T>() | ||
|
||
class UnknownError<T : Any> : Exception<T>() | ||
} | ||
|
||
inline fun <T : Any, R : Any> ApiResult<T>.handle(convert: (T) -> R): ApiResult<R> = | ||
when (this) { | ||
is Exception.NetworkError -> Exception.NetworkError() | ||
is Exception.UnknownError -> Exception.UnknownError() | ||
is ServerError -> ServerError(status, message) | ||
is Success -> Success(convert(data)) | ||
} | ||
|
||
fun ApiResult<Unit>.handle(): ApiResult<Unit> = | ||
when (this) { | ||
is Exception.NetworkError -> Exception.NetworkError() | ||
is Exception.UnknownError -> Exception.UnknownError() | ||
is ServerError -> ServerError(status, message) | ||
is Success -> Success(data) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package com.on.staccato.data | ||
|
||
import com.on.staccato.StaccatoApplication.Companion.retrofit | ||
import com.on.staccato.data.StaccatoClient.getErrorResponse | ||
import com.on.staccato.data.dto.ErrorResponse | ||
import com.on.staccato.data.dto.Status | ||
import okhttp3.Request | ||
import okhttp3.ResponseBody | ||
import okio.Timeout | ||
import retrofit2.Call | ||
import retrofit2.HttpException | ||
import retrofit2.Response | ||
import java.io.IOException | ||
|
||
class ApiResultCall<T : Any>( | ||
private val delegate: Call<T>, | ||
) : Call<ApiResult<T>> { | ||
override fun enqueue(callback: retrofit2.Callback<ApiResult<T>>) { | ||
delegate.enqueue( | ||
object : retrofit2.Callback<T> { | ||
override fun onResponse( | ||
call: Call<T>, | ||
response: Response<T>, | ||
) { | ||
val networkResult: ApiResult<T> = handleApiResponse { response } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DataSource의 메서드마다 적용해주던 handleApiResponse를 ApiResultCall 내부로 이동시켜 중복 코드를 없앴네요! 속시원~ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 속시원~~ 😆 |
||
callback.onResponse(this@ApiResultCall, Response.success(networkResult)) | ||
} | ||
|
||
override fun onFailure( | ||
call: Call<T>, | ||
throwable: Throwable, | ||
) { | ||
val exception = handleException<T>(throwable) | ||
callback.onResponse(this@ApiResultCall, Response.success(exception)) | ||
} | ||
}, | ||
) | ||
} | ||
|
||
override fun execute(): Response<ApiResult<T>> = throw NotImplementedError() | ||
|
||
override fun clone(): Call<ApiResult<T>> = ApiResultCall(delegate.clone()) | ||
|
||
override fun isExecuted(): Boolean = delegate.isExecuted | ||
|
||
override fun cancel() { | ||
delegate.cancel() | ||
} | ||
|
||
override fun isCanceled(): Boolean = delegate.isCanceled | ||
|
||
override fun request(): Request = delegate.request() | ||
|
||
override fun timeout(): Timeout = delegate.timeout() | ||
} | ||
|
||
private const val CREATED = 201 | ||
private const val NOT_FOUND_ERROR_BODY = "errorBody를 찾을 수 없습니다." | ||
|
||
private fun <T : Any> handleApiResponse(execute: () -> Response<T>): ApiResult<T> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CallAdapter가 직접 네트워크 호출의 대한 응답을 Call로 변환해주는 줄 알았는데, Call로 변환해주는 역할은 ApiResultCall에게 위임되었군요! 😲 한가지 궁금증이 생겼습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 응답에 따라 결과를 처리하는 부분은 Call의 역할이 아니라고 생각해서 |
||
return try { | ||
val response: Response<T> = execute() | ||
val body: T? = response.body() | ||
|
||
when { | ||
response.isSuccessful && response.code() == CREATED -> Success(body as T) | ||
response.isSuccessful && body != null -> Success(body) | ||
else -> { | ||
val errorBody: ResponseBody = | ||
response.errorBody() | ||
?: throw IllegalArgumentException(NOT_FOUND_ERROR_BODY) | ||
val errorResponse: ErrorResponse = retrofit.getErrorResponse(errorBody) | ||
ServerError( | ||
status = Status.Message(errorResponse.status), | ||
message = errorResponse.message, | ||
) | ||
} | ||
} | ||
} catch (httpException: HttpException) { | ||
ServerError(status = Status.Code(httpException.code()), message = httpException.message()) | ||
} catch (throwable: Throwable) { | ||
handleException<T>(throwable) | ||
} | ||
} | ||
|
||
private fun <T : Any> handleException(throwable: Throwable) = | ||
when (throwable) { | ||
is IOException -> Exception.NetworkError<T>() | ||
else -> Exception.UnknownError<T>() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.on.staccato.data | ||
|
||
import retrofit2.Call | ||
import retrofit2.CallAdapter | ||
import java.lang.reflect.Type | ||
|
||
class ApiResultCallAdapter( | ||
private val resultType: Type, | ||
) : CallAdapter<Type, Call<ApiResult<Type>>> { | ||
override fun responseType(): Type = resultType | ||
|
||
override fun adapt(call: Call<Type>): Call<ApiResult<Type>> = ApiResultCall(call) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package com.on.staccato.data | ||
|
||
import retrofit2.Call | ||
import retrofit2.CallAdapter | ||
import retrofit2.Retrofit | ||
import java.lang.reflect.ParameterizedType | ||
import java.lang.reflect.Type | ||
|
||
class ApiResultCallAdapterFactory : CallAdapter.Factory() { | ||
override fun get( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로그를 찍어보니 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
returnType: Type, | ||
annotations: Array<out Annotation>, | ||
retrofit: Retrofit, | ||
): CallAdapter<*, *>? { | ||
if (getRawType(returnType) != Call::class.java) { | ||
return null | ||
} | ||
|
||
val responseType = getParameterUpperBound(0, returnType as ParameterizedType) | ||
|
||
if (getRawType(responseType) != ApiResult::class.java) { | ||
return null | ||
} | ||
|
||
val resultType = getParameterUpperBound(0, responseType as ParameterizedType) | ||
return ApiResultCallAdapter(resultType) | ||
} | ||
|
||
companion object { | ||
fun create(): ApiResultCallAdapterFactory = ApiResultCallAdapterFactory() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. companion object 안에 팩토리 메서드 create()를 구현하신 이유가 있을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코드의 가독성을 위해서일까요...? 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ApiResultCallAdapterFactory의 생성 방식을 StaccatoClient가 구체적으로 알 필요가 없다고 생각했습니다. 이러한 이유로 Retrofit2의 ScalarsConvertFactory와 OptionalConverterFactory, KotlinSerializationConverterFactory 등도
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package com.on.staccato.data | ||
|
||
import com.on.staccato.presentation.util.ExceptionState | ||
|
||
inline fun <T : Any> ApiResult<T>.onSuccess(action: (T) -> Unit): ApiResult<T> = | ||
apply { | ||
if (this is Success<T>) action(data) | ||
} | ||
|
||
inline fun <T : Any> ApiResult<T>.onServerError(action: (message: String) -> Unit): ApiResult<T> = | ||
apply { | ||
if (this is ServerError<T>) action(message) | ||
} | ||
|
||
inline fun <T : Any> ApiResult<T>.onException(action: (exceptionState: ExceptionState) -> Unit): ApiResult<T> = | ||
apply { | ||
if (this is Exception<T>) { | ||
when (this) { | ||
is Exception.NetworkError -> action(ExceptionState.NetworkError) | ||
is Exception.UnknownError -> action(ExceptionState.UnknownError) | ||
} | ||
} | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,23 +27,19 @@ object StaccatoClient { | |
|
||
private val jsonBuilder = Json { coerceInputValues = true } | ||
|
||
private val provideRetrofit = | ||
fun initialize(): Retrofit = | ||
Retrofit.Builder() | ||
.baseUrl(BASE_URL) | ||
.client(provideHttpClient) | ||
.addConverterFactory( | ||
jsonBuilder.asConverterFactory("application/json".toMediaType()), | ||
) | ||
.addCallAdapterFactory(ApiResultCallAdapterFactory.create()) | ||
.build() | ||
|
||
fun getErrorResponse(errorBody: ResponseBody): ErrorResponse { | ||
return provideRetrofit.responseBodyConverter<ErrorResponse>( | ||
fun Retrofit.getErrorResponse(errorBody: ResponseBody): ErrorResponse = | ||
responseBodyConverter<ErrorResponse>( | ||
Comment on lines
+40
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 정말..... 전혀 예상치 못한 곳에서 CI 에러가 나타난 원인이 여기에 있었다뇨... 😂 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🫠🫠🫠🫠🫠 |
||
ErrorResponse::class.java, | ||
ErrorResponse::class.java.annotations, | ||
).convert(errorBody) ?: throw IllegalArgumentException("errorBody를 변환할 수 없습니다.") | ||
} | ||
|
||
fun <T> create(service: Class<T>): T { | ||
return provideRetrofit.create(service) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
retrofit 초기화 위치가 StaccatoClient에서 StaccatoApplication의 동반 객체로 변경된 이유가 궁금합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CI 오류 발생 원인과 관련 있습니다!
CI가 오류가 발생했던 시점을 돌이켜 보면, 테스트에서는 MockWebServer와 연결한 Retrofit을 참조하고 서버에서 들어오는 오류 응답을 변환하는 부분에서는 실제 서버와 연결된 레트로핏을 참조하고 있었습니다. 이로 인해 StacccatoClient을 초기화할 수 없는 문제가 발생했었습니다.
즉, 기존의 코드는 StaccatoClient 내부에서 Retrofit을 초기화하고 있었기 때문에 환경 별로 다른 Retrofit을 사용할 수 없는 구조였습니다. 이 문제를 해결하기 위해 StaccatoApplication의 동반 객체에서 retrofit을 지연 초기화하도록 변경했습니다.