-
Notifications
You must be signed in to change notification settings - Fork 1
네트워크 예외 처리
2taezeat edited this page Dec 14, 2023
·
3 revisions
- 작성자 : 2taezeat
- 작성일자 : 23.12.10(일)
- 멀티 모듈과 클린 아키텍처를 적용한 상태한 안드로이드 앱이다.
-
네트워크 라이브러리로,
okHttp3
와,Retrofit2
를 사용하고 있다. -
직렬화 라이브러리로
kotlinx.serialization
을 사용하고 있다. - 서버 개발자와 http response에 대한, request를 충분히 논의하고 합의할 수 있는 상태이다.
네트워크 예외 처리를 할 때, 참여하는 모든 안드로이드 개발자가 일관되고 가독성이 높게 개발하려면 어떻게 해야 할까?
- 예외 처리 코드의 Indent 가 깊지 않고, 읽기 쉽게 한다.
-
data layer 와 domain layer의 예외(Exception) 가 Presentation layer 에서 catch 가능하게 한다.
- 이유: 유저에게 Error Event를 보여줘야 함으로
- data layer의 예외는 data layer에서, throw 하게 한다.
- 예외 이벤트의 이유를 세분화하여, 명확히 그 이유를 사용자에게 보여준다.
- 중복 코드를 최대한 줄인다.
-
서버 개발자와 협의 하여, success 한 상태가 아닌 상황에 대한 커스텀 예외와 Error 타입을 정의한다.
-
OkHttp Interceptor
를 활용해서, 발생 가능한, data layer의네트워크 예외(IOException)
를 throw 한다. -
viewModel에서,
CoroutineExceptionHandler
를 선언하고, 여기서 일관되게 Error에 대한 Event를 발생시킨다.- 유저에게 Event를 보여주는 것 외에, 다른 작업이 필요한 경우는 그에 맞게 다르게 처리해야 한다.
-
Error Event 에 대해서, 유저에게 보여주는 방법은 일관되게 한다.
- Fragment or Activity들 에서 Event 처리함수를 동일하게 사용하여, Error 이유를 유저에게 Toast or SnackBar로 보여준다.
ErrorResponse
(data layer)
@Serializable
data class ErrorResponse(
val message: String,
val errorCode: Int = 0,
val statusCode: Int = 0
)
SuccessResponse
(data layer)
@Serializable
data class PlaylistResponse(
val playlistId: Int,
val playlistTitle: String,
val trackSize: Int,
val thumbnailUrl: String?
) {
internal fun toDomain(): Playlist {
return Playlist(
id = playlistId,
title = playlistTitle,
thumbnailUrl = thumbnailUrl ?: "",
trackSize = trackSize
)
}
}
Api or Service
interface, (data layer)
@GET("playlists")
suspend fun getPlaylists(): List<PlaylistResponse>
@POST("playlists")
suspend fun postPlaylist(
@Body title: PlaylistRequest
)
RepositoryImpl
(data layer)
override fun getPlaylists(): Flow<List<Playlist>> = flow {
val playlistResponse = playlistApi.getPlaylists()
emit(playlistResponse.map { it.toDomain() })
}
override suspend fun postPlaylist(title: String) {
playlistApi.postPlaylist(PlaylistRequest(title = title))
}
Repository
(domain layer)
interface PlaylistRepository {
fun getPlaylists(): Flow<List<Playlist>>
suspend fun postPlaylist(title: String)
}
ViewModel
(presentation layer)
fun fetchPlaylists() { // flow 를 사용한 경우
playlistRepository.getPlaylists().onEach { playlists ->
_uiState.update { it.copy(playlists = playlists) }
}.launchIn(viewModelScopeWithExceptionHandler)
}
fun createPlaylist(playlistTitle: String) { // suspend 함수를 사용한 경우
viewModelScopeWithExceptionHandler.launch {
playlistRepository.postPlaylist(playlistTitle)
}
}
Fragment or Activity
(presentation layer)
private fun observeEvents() {
repeatOnStarted {
viewModel.events.collect { event ->
when (event) {
is PlaylistsEvent.ShowMessage -> {
showMessage(event.error.toMessageId())
}
}
}
}
}
fun showMessage(@StringRes messageId: Int) {
Snackbar.make(this.requireView(), messageId, Snackbar.LENGTH_LONG).show()
}
-
BaseViewModel 를 추상 클래스로 선언하고, 추상 함수(onError) 을 상속 받은 viewModel 을 통해 중복된 코드를 줄일 수도 있다.
-
네트워크 예외가 아닌, domain layer의 Usecase의 비지니스 로직에서 예외하는 것 까지 모두 일관되게 처리할 수 없다.
- 이 경우는 viewModel의
exceptionHandler
나 viewModel의 domain 호출 코드에서 예외처리를 그에 맞게 처리 해야 한다.
- 이 경우는 viewModel의
- https://square.github.io/okhttp/features/interceptors/
- https://square.github.io/okhttp/5.x/okhttp/okhttp3/-interceptor/index.html
- https://kotlinlang.org/docs/exception-handling.html#supervision-scope
- https://dongsik93.github.io/til/2022/07/05/til-kotlin-coroutine-exception-handling/
- https://tourspace.tistory.com/154?category=797357
- 프로젝트 생성
- 프로젝트 구조
- PR에 대한 단위 테스트 자동화
- 역/직렬화 라이브러리 비교
- Github Release 자동화
- Firebase App 배포 자동화
- 플러그인을 이용하여 공통 설정 없애기
- Timber 라이브러리를 사용한 이유
- 네트워크 예외 처리
- Kotest 도입기