clean architecture ํ๋ก์ ํธ๋ฅผ ๊ตฌ์ํ๊ณ ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด api error ์ฒ๋ฆฌ๋ฅผ ์ด๋ป๊ฒ ํ๊ณ ์๋๊ฐ?
์์ ์ ํ์๋ ๊ทธ๋ฅ ๋ฌด์ง์ฑ์ผ๋ก(?) presentation layer์ try catch๋ฅผ ์ด์ฉํด ์ฒ๋ฆฌ๋ฅผ ํ์์๋ค.
ํ์ง๋ง ๊ทธ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ์ ๋, ์ผ์ผ์ด try catch๋ฌธ์ ์์ฑํด์ผ ํ๋ ๋ถํธํจ๊ณผ presentation layer์์๋ง ์ฒ๋ฆฌํ๋ ๊ฒ์ด ๋ง๋ ์ด๋ฐ ์๋ฌธ๋ค์ด ๋ค๊ธฐ ์์ํ๋ค.
๊ทธ๋์ ์ด๋ฒ ํฌ์คํ ์์ clean architecture project์์ api error handling logic์ ์์ฑํ๋ ๋ฐฉ๋ฒ์ ๊ณต๋ถํ๊ณ ์ ์ด๋ณด์๋ค.
1. error handling ๋ก์ง์ ์ด๋ค layer์ ์์ฑ๋์ด์ผ ํ๋๊ฐ??
ํ์๋ data layer์์ http status code์ ๋ฐ๋ฅธ ์์ธ์ ๋ฐ๋ฅธ exception์ ๋ ๋ฆฌ๊ณ domain layer์ use case์์ try catch๋ฅผ ์ด์ฉํด presentation layer์ viewmodel์์ error์ ๋ฐ๋ฅธ state๋ฅผ ๋ถ์ฌํด์ ui์์ state์ ๋ฐ๋ฅธ ๋ก์ง์ ์ฒ๋ฆฌํ๊ฒ ์์ฑํด ๋ณด๊ฒ ๋ค.
2. data layer์์์ ์์
data layer์์์ ์์ ์ ๋ณด๋ฉด ์ผ๋จ network ๋ก์ง์ ๋ฐ์์์ ๊ทธ ๊ฒฐ๊ณผ์ ๋ฐ๋ฅธ ๋ก์ง์ try catch๋ก ์ฒ๋ฆฌํด ์ค๋ค.
suspend fun <T> safeApiCall(call: suspend () -> T): T {
return try {
call.invoke()
} catch (e: HttpException) {
val message = e.message
throw when(e.code()) {
400 -> BadRequestException(message = message)
401 -> UnauthorizedException(message = message)
403 -> ForbiddenException(message = message)
404 -> NotFoundException(message = message)
500, 501, 502, 503 -> ServerException(message = message)
else -> OtherHttpException(
code = e.code(),
message = message
)
}
} catch (e: SocketTimeoutException) {
throw TimeOutException(message = e.message)
} catch (e: UnknownHostException) {
throw InternetException()
} catch (e: Exception) {
throw UnknownException(message = e.message)
}
}
์๋ฒ์์ ์๋ ค์ฃผ๋ status ์ฝ๋์ ๋ฐ๋ผ ๋ค๋ฅธ exception์ throw ํ๋ค. ๋ค๋ฅธ ๋ฐ๋ก ์ฒ๋ฆฌํ exception๋ค์ด ์๋ค๋ฉด catch๋ฅผ ์ด์ด์ ์ฒ๋ฆฌํด๋ ๋๋ค.
exception์ ๋ํ ์ ๋ณด๋ HttpException ํ์ผ์์ ํ์ธํด ๋ณผ ์ ์๋ค.
package com.mvi.data.exception
/**
* 400: bad request
*/
class BadRequestException(
override val message: String?
): RuntimeException()
/**
* 401: unauthorized
*/
class UnauthorizedException(
override val message: String?
): RuntimeException()
/**
* 403: forbidden
*/
class ForbiddenException(
override val message: String?
): RuntimeException()
/**
* 404: not found
*/
class NotFoundException(
override val message: String?
): RuntimeException()
/**
* 500: server error
*/
class ServerException(
override val message: String?
): RuntimeException()
/**
* response time out
*/
class TimeOutException(
override val message: String?
) : RuntimeException()
/**
* other error
*/
class OtherHttpException(
val code: Int?,
override val message: String?
): RuntimeException()
/**
* unknown error
*/
class UnknownException(
override val message: String?
): RuntimeException()
3. domain layer์์์ ์์
domain์์์ ์์ ์ ๋ณด๋ฉด
usecase ์ชฝ์์ handling ์์ ์ ํ๋ค.
class OAuthTokenUseCase @Inject constructor(
private val twitchAuthRepository: TwitchAuthRepository
) {
suspend operator fun invoke(
clientId: String,
clientSecret: String,
grantType: String
) = kotlin.runCatching { twitchAuthRepository.oAuthToken(clientId, clientSecret, grantType) }
}
operator function์ ์ฌ์ฉํด runcatch๋ฌธ์ ๋๋ฆฐ๋ค.
runcatch์ ๊ฒฐ๊ณผ๋ presentation layer์์ ์ฒ๋ฆฌํ๋ค.
4. presentation layer์์์ ์์
presentation layer์์๋ viewModel์์ usecase์์ ๋ฐํ๋ runcatch ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ๊ฐ๊ฐ state๋ฅผ ํ ๋นํ๋ค.
class TwitchAuthViewModel @Inject constructor(
private val oAuthTokenUseCase: OAuthTokenUseCase
): ViewModel() {
private val _oAuth = MutableLiveData<ApiState<OAuthResponseDomain>>(ApiState.Loading)
val oAuth: LiveData<ApiState<OAuthResponseDomain>> = _oAuth
fun getOAuthToken(
clientId: String,
clientSecret: String,
grantType: String
) {
viewModelScope.launch {
oAuthTokenUseCase(clientId, clientSecret, grantType).onSuccess {
_oAuth.value = ApiState.Success(data = it)
}.onFailure {
it.exceptionErrorHandling(
badRequestAction = { _oAuth.value = ApiState.BadRequest },
forbiddenAction = { _oAuth.value = ApiState.Forbidden },
timeOutAction = { _oAuth.value = ApiState.TimeOut },
serverAction = { _oAuth.value = ApiState.Server },
unknownAction = { _oAuth.value = ApiState.Unknown }
)
}
}
}
}
๊ธฐ์กด์ liveData๋ง ์ด์ฉํ๋ ๋ณ์์ ApiState๊ฐ ์ถ๊ฐ๋์๋ค.
ํจ์ ๋ก์ง์์๋ useCase์์์ runcatch ๊ฒฐ๊ณผ์ ๋ฐ๋ผ apiState์์ state๋ฅผ ์ฃผ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
ApiState ํ์ผ์์๋ ๋ค์ํ state๋ฅผ ์ ์ธํด ๋์๋ค.
sealed interface ApiState<out T> {
object Loading: ApiState<Nothing>
data class Success<T>(val data: T? = null): ApiState<T>
object BadRequest: ApiState<Nothing>
object Unauthorized: ApiState<Nothing>
object Forbidden: ApiState<Nothing>
object NotFound: ApiState<Nothing>
object TimeOut: ApiState<Nothing>
object Server: ApiState<Nothing>
object Unknown: ApiState<Nothing>
}
๋ํ api ํต์ ์ ์คํจํ์ ๋์๋ data layer์์ throw ๋ exception์ ๋ง๊ฒ ์ํ๋๋ ๊ณ ์ฐจ ํจ์๋ค๋ก ์ด๋ฃจ์ด์ ธ ์๋ค.
ExceptionHandling ํ์ผ์์ ํ์ธํด ๋ณผ ์ ์๋ค.
fun Throwable.exceptionErrorHandling(
badRequestAction: () -> Unit = {},
unauthorizedAction: () -> Unit = {},
forbiddenAction: () -> Unit = {},
notFoundAction: () -> Unit = {},
timeOutAction: () -> Unit = {},
serverAction: () -> Unit = {},
unknownAction: () -> Unit = {},
) {
when (this) {
is BadRequestException -> badRequestAction()
is UnauthorizedException -> unauthorizedAction()
is ForbiddenException -> forbiddenAction()
is NotFoundException -> notFoundAction()
is TimeOutException -> timeOutAction()
is ServerException -> serverAction()
else -> unknownAction()
}
}
์ ๋ฆฌ
์ด๋ฒ ํฌ์คํ ์์๋ clean architecture ๊ด์ ์์์ api error handling ๋ฐฉ๋ฒ์ ๋ํด์ ์ ๋ฆฌํด ๋ณด์๋ค.
๊ธฐ์กด์ ์ ๋๋ก ์์ง ๋ชปํ๊ณ ์ฌ์ฉํ๋ api error handling์ ๋ ์์ธํ ์๊ฒ ๋์๋ ์๊ฐ์ด์๋ค.
๋ํ ์ง์ ์ฌ์ฉํด ๋ณด๋ฉด์ ๊ตฌํํด ๋์ ๋ก์ง์ ui์์ ๊ฐ๋จํ ์ฌ์ฉํ ์ ์์ด์ ์ข์๋ ๊ฒ ๊ฐ๋ค.
์ด ๋ฐฉ๋ฒ ๋ง๊ณ ๋ ๋ ํจ์จ ์ข์ ๋ฐฉ๋ฒ์ ์์๋ ๋ถ๋ค์ ๋๊ธ๋ก ์๋ ค์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค.
'๐ฑ| Android > ๐ | ๊ธฐ๋ก' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Android, Kotlin] exoplayer์ media3๋ฅผ ์ด์ฉํด ์์ ์ฌ์ํ๊ธฐ (2) | 2023.08.25 |
---|---|
[Android] MVI ํจํด์ ๋ฌด์์ผ๊น? (0) | 2023.08.21 |
[Android, Kotlin] Android MVVM ๋ค๋ค๋ณด๊ธฐ (1) | 2023.03.18 |
[Android, Kotlin] Zxing ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก QR์ฝ๋ ์ค์บํ๊ธฐ (4) | 2023.03.11 |
[Android, Kotlin] android custom dialog ๋ง๋ค๊ธฐ (5) | 2022.11.20 |