๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ“ฑ| Android/๐Ÿ“˜ | ๊ธฐ๋ก

[Android, Kotlin] Android clean architecture ํ”„๋กœ์ ํŠธ์—์„œ api ์—๋Ÿฌ ํ•ธ๋“ค๋งํ•˜๊ธฐ

by immgga 2023. 8. 1.

 

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์—์„œ ๊ฐ„๋‹จํžˆ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์„œ ์ข‹์•˜๋˜ ๊ฒƒ ๊ฐ™๋‹ค.

์ด ๋ฐฉ๋ฒ• ๋ง๊ณ ๋„ ๋” ํšจ์œจ ์ข‹์€ ๋ฐฉ๋ฒ•์„ ์•„์‹œ๋Š” ๋ถ„๋“ค์€ ๋Œ“๊ธ€๋กœ ์•Œ๋ ค์ฃผ์‹œ๋ฉด ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

728x90

๋Œ“๊ธ€