Android Network Handling in Clean Architecture

Jaeyoung·2022년 1월 2일
5

Network Handling

서론

Http 통신을 통한 Network Handling에 대해 효율적으로 처리하는 방법을 클린아키텍쳐 관점에서 정리해 보기 위해 작성된 글이다.

클린아키텍쳐에서 Android Network Handling은 어떤 Layer에서 해야하는가?

어떤 사람은 Presentation Layer에서 Network 처리에 대한 결과를 사용하기 때문에 Presentation Layer에서 Server Code에 따른 Network Handling을 해야한다고 생각 될 수 있지만 내 생각에는 Presentation Layer는 httpexception,Server Code에 관해 알지 못해야한다고 생각한다. 왜냐하면 Presentation Layer에서는 단순히 서버나 내부DB에서 데이터를 받아 그 데이터를 View에 그려주는 작업을 하는 Layer 이기때문에 처리된 결과만 받으면 된다고 생각했기 때문이다. 그렇기 때문에 다른 Layer 에서 Network 처리를 하고 그에 대한 결과를 Presentation Layer가 받아서 처리 하면된다. 그래서 data Layer 에서 Network 처리에 따라 Throw를 던져 Domain Layer에서 try catch로 Network 처리에 따른 Throw를 통해 NetworkResult를 반환하도록 처리 하였다.

Response와 NetworkResult 객체

data class Response<T>(
    val status: Boolean,
    val code: String,
    val message: String,
    val data: T
)

일단 Response 객체와 같은 경우에는 서버의 상태를 나타내는 값, 서버코드, 서버에서 내려주는 message, 실제 data가 있다. 이 같은 경우에는 서버에서 어떻게 내려주는지에 따라 변경하면 될것같다.

sealed class NetworkResult<out DATA> {
    data class Success<DATA>(var data : DATA) : NetworkResult<DATA>()
    data class Fail(val message : String) : NetworkResult<Nothing>()
    object TokenExpired : NetworkResult<Nothing>()
    data class Exception(val exception: java.lang.Exception) : NetworkResult<Nothing>()
}

NetworkResult 객체는 크게 Success, Fail, TokenExpired , Exception로 서버통신에서 발생할 수 있는 것들로 구성되어있다. 여기서 Success를 제외한 나머지의 NetworkResult의 타입을 보면 Nothing으로 되어있는데 Success를 제외한 나머지의 것들에서는 DATA타입을 강제할 필요가 없기때문에 Nothing으로 선언하였다.

Data Layer 에서의 작업

일단 Data Layer에서의 작업을 보자면 Datasource에서는 Response를 Api에서 받아와 NetworkResult로 변환 시켜주는 작업을 해준다.

suspend fun getMovie(): NetworkResult<MovieResponse> {
        return movieApi.getMovie().toNetworkResult()
}

movieApi.getMovie에서는 Response 객체로 Wrapping 되어서 반환된다. 그래서 Response로 Wrapping 되어있는 데이터를 toNetwokrResult 메소드를 통해 NetworkResult로 Mapping 시켜준다. toNetworkResult의 코드를 살펴보면 아래와 같이 구성되어있다.

const val REST_CODE_NO_SEARCH_MOVIE = "C000"
const val REST_SUCCESS = "E000"
const val REST_CODE_ERROR_TOKEN_EXPIRED = "E991"
const val REST_CODE_ERROR_JWT_REFRESH = "E995"

fun <T> Response<T>.toNetworkResult() : NetworkResult<T> {
    return when(code){
        REST_SUCCESS -> NetworkResult.Success(data)
        REST_CODE_ERROR_JWT_REFRESH -> throw JwtRefreshException(message)
        REST_CODE_ERROR_TOKEN_EXPIRED -> throw TokenExpireException(message)
        else -> throw ServerFailException(message)
    }
}

서버에서 내려온 서버 코드를 통해 Success , Jwt Refresh, Token Expire, Fail로 나눠지는 것을 볼수있다. 나중에 이야기 할거지만 networkhandling에서 일괄처리해주기 위해 Server 에서 내려준 코드에 따라 JwtRefreshException, TokenExpireException, ServerFailException 예외를 발생시킨다.

//Repository
suspend fun getMovie() : NetworkResult<Movie> {
        val movieResponseNetworkResult = movieDataSource.getMovies()
        return movieResponseNetworkResult.mapNetworkResult {
                it.toDomainModel()
        }
    }

위와 같이 Repository를 보면 DataSource에서 받아온 데이터를 domain 모델로 변환해서 내려줘야하는데 mapNetworkResult 메소드를 통해 도메인 모델로 변경시킨다.

mapNetworkResult의 코드를 살펴보면 아래와 같이 구성되어있다.

fun <T> NetworkResult<T>.toModel() : T =
    (this as NetworkResult.Success).data

private fun <R> changeNetworkData(replaceData: R): NetworkResult<R> {
    return NetworkResult.Success(replaceData)
}

suspend fun <T,R> NetworkResult<T>.mapNetworkResult(getData : suspend (T) -> R) : NetworkResult<R>{
    return changeNetworkData(getData(toModel()))
}

변환 하려는 NetworkResult가 Success 일경우에 getData에서 반환하는 domain Model 를 changeNetworkData를 통해 Domain Model로 변경된 NetworkResult.Success 객체를 반환한다. Success 객체가 아닐경우는 어차피 toNetworkResult에서 throw로 Exception을 발생시키기 때문에 따로 처리해 주지 않았다.

Domain Layer에서의 작업

class MovieInfoUseCase(private val movieRepository: MovieRepository) {
    suspend operator fun invoke() : NetworkResult<MovieInfo>{
        return networkHandling {
            movieRepository.getMovie().map { movie ->
                movieRepository.getMovieDetail().map { movieDetail ->
                    MovieInfo(movie.movieId,movie.title,movie.description,movieDetail.author,movieDetail.publisher,movieDetail.rate)
                }
            }
        }

    }
}

위와 같이 MovieInfoUseCase를 보면 networkHandling으로 감싸서 새로운 Domain Model을 NetworkResult로 Mapping 해서 return 해주고있다.

networkHandling 코드를 보면 아래와 같다.

suspend fun <T> networkHandling(block : suspend () -> T) : NetworkResult<T> {
    return try {
        NetworkResult.Success(block())
    } catch (e : Exception){
        when(e){
            is JwtRefreshException -> {
                JwtRefresh.isJwtRefresh = true
                Log.d("Network Exception","JwtRefreshException")
                networkHandling { block() }
            }
            is TokenExpireException -> {
                Log.d("Network Exception","TokenExpireException")
                NetworkResult.TokenExpired
            }
            is ServerFailException -> {
                NetworkResult.Fail(e.message?:"")
            }
            else -> {
                Log.d("Network Exception","UnknownException")
                NetworkResult.Exception(e)
            }
        }
    }
}

try catch로 Exception이 발생하지않았다면 block을 실행시켜 block에 대한 실행 값(즉 새로운 Domain Model)을 NetworkResult.Success로 Mapping 해서 반환한다. Exception(JwtRefreshException , TokenExpireException, ServerFailException 그외 HttpException 등등)이 발생했을 때는 Exception을 catch 해서 jwtRefresh 요청을 한다거나 알맞은 NetworkResult 로 반환 해준다.

MovieInfoUseCase networkHandling 안에있는 block을 보면 NetworkResult.map 을 통해 결과를 도출하는 것을 볼 수 있다. map 코드는 아래와 같다.

fun <T> NetworkResult<T>.toModel() : T =
    (this as NetworkResult.Success).data

suspend fun <T,R> NetworkResult<T>.map(getData : suspend (T) -> R) : R{
    val data = toModel()
    return getData(data)
}

map 의 반환값을 보면 NetworkResult가 없어진걸 볼 수 있는데 이와같은 이유는 networkHandling이 block에서 return 되는 값을 NetworkResult로 Mapping 해주기때문에 NetworkResult를 걷어내지않으면 NetworkResult<NetworkResult> 이런식으로 반환되기때문에 map을 통해 NetworkResult를 걷어낸 일반 객체로 변환한다.

왜 UseCase에서 이런작업을 하는지 궁금할 수 있을 것이다. 그 이유에 대해서 설명하자면 MovieInfoUseCase와 같이 api 2개를 호출해서 새로운 domain model을 만들어주는 경우를 보자 한 api에서 오류가 발생한다면 새로운 domain model로 만들 수가 없을 것이다. 그렇기 때문에 UseCase에서 작업을 해준다면 try catch 그것들에 대한 Exception 처리를 일괄적으로 처리해줄 수 있기 때문에 UseCase에서 networkHandling 작업을 해주게 되었다.

전체소스

https://github.com/JY-Dev/NetworkErroHandling.git

profile
Programmer

5개의 댓글

comment-user-thumbnail
2022년 1월 13일

이 집 잘하네...

답글 달기
comment-user-thumbnail
2022년 10월 6일

잘 읽었습니다!
혹시 mapNetworkResult 확장 함수를 suspend로 준 것에 대한 이점이 있을까요?

1개의 답글
comment-user-thumbnail
2022년 12월 4일

좋은 글 감사합니다.
다만 계층 나눠진 클릭아키턱처에서 UseCase에서 네트워크 에러라는 구체적인 데이터 Layer의 에러 유형을 처리하는게 맞을까요? UseCase에 해당하는 에러 유형이 더 적절하지 않나요? 그리고 Domain Model이 Entity 같은데, 이것이 바로 Presentation Layer에 전달되는게 맞을까요?

1개의 답글