ConnectException vs IOException

이지훈·2023년 4월 10일
0

최근에 앱의 서버가 간헐적으로 죽는 현상이 발생하여 로그인이 불가능한 경우가 있었는데, 다음과 같이 이슈가 올라왔었다.

서버가 죽은 것은 내 입장에서는 어쩔 수..없는거라 서버 개발자님들께 서버 확인을 부탁드렸고, 다시 서버를 살려서 로그인이 가능하도록 한 뒤 이슈를 종료하였다.

하지만 문제는 네트워크 에러처리에 있었는데, 서버가 죽었을 경우 API 통신 중 java.net.ConnectException 이라는 Exception이 발생하기 때문에

아래 코드에서 확인할 수 있듯이, ConnectException이 발생한 경우엔 로직상 Result.Error 의 else 분기문을 타게되어 '로그인이 실패했어요' 만 출력되어야 한다.

하지만 '로그인이 실패했어요 인터넷 연결을 확인해주세요' 라는 토스트가 출력되었고, 이는 IOException 분기를 탔다는 의미이며, 인터넷 연결엔 문제가 없는데 왜 인터넷 연결 실패의 대한 토스트 메세지가 출력되는지 확인을 해 볼 필요가 있었다.

   override fun slothLogin(authToken: String, socialType: String) = flow {
        emit(Result.Loading)
        val response = userAuthService.slothLogin(authToken, LoginSlothRequest(socialType = socialType)
        ) ?: run {
            emit(Result.Error(Exception(RESPONSE_NULL_ERROR)))
            return@flow
        }
        when (response.code()) {
            HTTP_OK -> {
                val accessToken = response.body()?.accessToken ?: DEFAULT_STRING_VALUE
                val refreshToken = response.body()?.refreshToken ?: DEFAULT_STRING_VALUE
                preferences.registerAuthToken(accessToken, refreshToken)

                emit(Result.Success(response.body()?.toEntity() ?: LoginSlothResponse.EMPTY.toEntity()))
            }

            else -> emit(Result.Error(Exception(response.message()), response.code()))
        }
    }.handleExceptions()
// handleExceptions 
fun <T> Flow<Result<T>>.handleExceptions(): Flow<Result<T>> {
    return this.catch { throwable ->
        when (throwable) {
            is IOException -> {
                // Handle Other Network Error
                emit(Result.Error(Exception(INTERNET_CONNECTION_ERROR)))
            }
            else -> {
                // Handle Other Error
                emit(Result.Error(throwable))
            }
        }
    }
}
// LoginViewModel
    fun slothLogin(accessToken: String, socialType: String) = viewModelScope.launch {
        slothLoginUseCase(authToken = accessToken, socialType = socialType)
            .onEach { result ->
                setLoading(result is Result.Loading)
            }.collect { result ->
                when (result) {
                    is Result.Loading -> return@collect
                    is Result.Success -> {
                        if (result.data.isNewUser) {
                            _navigateToRegisterBottomSheetEvent.emit(Unit)
                        } else {
                            createAndRegisterNotificationToken()
                        }
                    }
                    is Result.Error -> {
                        when {
                            result.throwable.message == INTERNET_CONNECTION_ERROR -> {
                                showToast(stringResourcesProvider.getString(R.string.login_fail_by_internet_err
                            }
                            else -> {
                                showToast(stringResourcesProvider.getString(R.string.login_fail))
                            }
                        }
                    }
                }
            }
    }

확인 해본 결과 Java.net.ConnectException은 IOException의 하위 클래스라는 것을 알 수 있었다.

따라서 ConnectException 이 발생했을 때에도 IOException 분기를 타고 들어가 결국 인터넷 연결을 확인해달라는 토스트메세지가 출력이 되는 것이었다.

코틀린에서는 하위 클래스의 인스턴스가 상위 클래스의 인스턴스이기 때문이다.


fun main() {
    val square = Square()

    when(square) {
        is Rectangle -> {
            print("하위 클래스의 인스턴스는 상위 클래스의 인스턴스이기도 하다")
        }
        else -> {
            print("바보")
        }
    }
    
    // Result: 하위 클래스의 인스턴스는 상위 클래스의 인스턴스이기도 하다
}

open class Rectangle {
    open fun draw() {
        /**/
    }
}

class Square(): Rectangle() {
    override fun draw() {
        super.draw()
        /**/
    }
}

따라서 다음 처럼 새로운 분기를 추가하여 ConnectException이 발생하였을 경우엔 ConntectException 분기를 타게 하였고, 인터넷 문제가 아닌 서버 에러가 발생했음을 전달해주어 해결할 수 있었다.

fun <T> Flow<Result<T>>.handleExceptions(): Flow<Result<T>> {
    return this.catch { throwable ->
        when (throwable) {
            is ConnectException -> {
                // Handle Connect Error
                emit(Result.Error(Exception(SERVER_CONNECTION_ERROR)))
            }
            is IOException -> {
                // Handle Other Network Error
                emit(Result.Error(Exception(INTERNET_CONNECTION_ERROR)))
            }
            else -> {
                // Handle Other Error
                emit(Result.Error(throwable))
            }
        }
    }
}
// LoginViewModel
    fun slothLogin(accessToken: String, socialType: String) = viewModelScope.launch {
        slothLoginUseCase(authToken = accessToken, socialType = socialType)
            .onEach { result ->
                setLoading(result is Result.Loading)
            }.collect { result ->
                when (result) {
                    is Result.Loading -> return@collect
                    is Result.Success -> {
                        if (result.data.isNewUser) {
                            _navigateToRegisterBottomSheetEvent.emit(Unit)
                        } else {
                            createAndRegisterNotificationToken()
                        }
                    }
                    is Result.Error -> {
                        when {
                            result.throwable.message == SERVER_CONNECTION_ERROR -> {
                                showToast(stringResourcesProvider.getString(R.string.login_fail_by_server_error))
                            }
                            result.throwable.message == INTERNET_CONNECTION_ERROR -> {
                                showToast(stringResourcesProvider.getString(R.string.login_fail_by_internet_error))
                            }
                            else -> {
                                showToast(stringResourcesProvider.getString(R.string.login_fail))
                            }
                        }
                    }
                }
            }
    }

하지만 이렇게 분기가 필요할때마다 분기를 추가하여 Exception을 핸들링 하는 것은 별로 좋은 코드인 것 같지는 않다. 휴먼 에러가 발생할 확률이 높다고 생각 때문이다. (모든 API 통신 코드에 달아준다고 생각하면 한 두개 빼먹기 쉽다.)

Data Layer 의 handleException 확장 함수의 경우는 함수의 변경이 필요할 경우 해당 확장 함수만 수정해주면 되긴 하지만 말이다.

LoginViewModel의 함수내의 Error 처리 부분도 확장함수로 모듈화를 하든, 다른 Exception 핸들링 방식을 사용하는 방식을 사용하는게 좋을 것 같다는 생각이 든다.

다른 Exception 핸들링 방식은 자료들을 확인해보며 학습을 해보도록 하겠다.

변경)
현재는 SocketTimeoutException 을 통해 서버 에러를, ConnectException을 통해 인터넷 연결 에러를 catch 하고 있다.

참고)
https://kotlinlang.org/docs/typecasts.html

https://kotlinlang.org/docs/inheritance.html#overriding-rules

profile
실력은 고통의 총합이다. Android Developer

0개의 댓글