Android

[Android] REST API 예외처리

Sangyoon98 2023. 2. 10. 22:43

REST API를 사용해서 통신을 하다보면 통신에 실패했을 때에 대한 예외 처리도 해주어야 한다.

오른쪽 위에 200 OK라고 뜬 것이 통신 코드이다.

통신에 성공하면 저렇게 내용도 뜨고 통신 코드도 잘 나온다.

하지만 통신에 실패하면 아래와 같은 통신 코드와 에러 내용을 받게 된다.

클라이언트에서 REST API로 통신을 했는데 에러를 받으면 정상적으로 내용이 나오지 않을 것이다.

이것에 대해 예외 처리를 해주는 것이다.

보통은 예외 종류에 따라 에러 메세지를 출력해주게 만든다.

 

쉽게 말해 클라이언트에서는 사용자가 통신에 실패한 것을 두 눈으로 확인할 수 있게 출력해주는 처리를 해야하는 것이다.

 

프로젝트에서 REST API 통신은 레트로핏을 이용하였고, 코루틴을 사용해서 통신 처리를 했다.

예외 처리는 Coroutine Exception Handler을 사용해서 예외처리를 적용했다.

 

자세한 내용은 아래 통신을 구현한 포스팅을 참고하면 된다.

 

[Android] Retrofit + Kotlin Coroutine 서버 통신하기

지난번에 포스팅 했던 Retrofit을 사용해서 서버와 통신하는 글을 보고 오시는 것을 추천합니다. [Android] Retrofit을 사용해서 서버와 통신하기 Retrofit이란? Rest API 통신을 위해 구현된 라이브러리 Squa

sangyoon98.tistory.com


Coroutine Exception Handler

//Coroutine Exception Handler
private val exceptionHandler = CoroutineExceptionHandler{ _, throwable ->
	throwable.printStackTrace()

	when(throwable){
		is SocketException -> Log.d("logFish", "SocketException")
		is HttpException -> {
			Log.d("logFish", "HttpException: code = ${throwable.code()}, message = ${throwable.message()}, error = ${throwable.response()?.errorBody()?.string()}")
		}
		is UnknownHostException -> Log.d("logFish", "UnknownHostException")
		else -> Log.d("logFish", "Exception")
	}
}

Coroutine Exception Handler를 보면 when 구문에 예외별로 에러처리를 하도록 구성되어있다.

throwable 객체에는 code = 통신코드, message = 에러메세지, errorBody = 에러응답객체가 포함되어있어서 해당 값을 불러와 상황에 맞게 예외 처리를 하면된다.

 

Exception

SocketException, HttpException, UnKnownHostException 등이 있다.

throwable 반환 값에 해당된다.

 

Http 상태코드

401에러, 403에러, 404에러 등등 흔히 아는 HTTP 상태 코드가 있다.

throwable.code 반환 값에 해당된다.

 

커스텀 Exception 응답 객체

에러 Body 내용을 서버에서 지정해주었다면 해당 Body를 불러와 사용할 수 있다.

이번 프로젝트에서는 에러 응답 객체에 커스텀 에러 코드가 담겨있어 에러 응답 객체를 불러와 사용할 것이다.

throwable.response()?.errorBody() 반환 값에 해당된다.

 

하지만 아래처럼 에러코드의 양이 많으면 어떻게 처리해야할까 고민을 해봤다.

통신하는 코드마다 Coroutine Exception Handler를 추가하고 해당하는 에러마다 토스트다 다이얼로그를 띄워야 하는데 이렇게 코드를 작성하면 중복되는 코드도 많고 코드가 엄청 길어질 것이다.

 

따라서 Singleton pattern을 사용해서 Coroutine Exception Handler를 모듈화 시키기로 했다.

 

여기서 코틀린의 장점이 나오는데, 자바에서 Singleton pattern을 제작하려면 클래스를 새로 생성하고 Boilerplate code를 작성해야 한다. (생성자나 getter setter 같은 것들...)

하지만 코들린은 Object 키워드 단 하나만으로 Singleton pattern을 구현할 수 있다!

 

object CoroutineException {
    fun exception(context: Context, throwable: Throwable) {
        when (throwable) {
            is SocketException -> Log.d("logFish", "SocketException")

            is HttpException -> {
                val code = throwable.code()
                val message = throwable.message()
                val errorBody = throwable.response()?.errorBody()?.string()
                val errorBodyList = errorBody?.split(",", ":")
                val errorBodyCode = errorBodyList?.get(1)
                val messageList = errorBodyList?.get(3)?.split("\"")
                val errorBodyMessage = messageList?.get(1)
                Log.d("logFish", "HttpException: code = $code, message = $message")
                Log.d("logFish", "HttpException ErrorBody: code = $errorBodyCode, message = $errorBodyMessage")

                if (errorBodyCode == "10400") Toast.makeText(context, "리프레시 토큰이 유효하지 않습니다.", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10401") Toast.makeText(context, "인증 정보가 없습니다.", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10402") Toast.makeText(context, "서버에서 처리중입니다.", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10403") Toast.makeText(context, "요청 파라미터 부족", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10404") Toast.makeText(context, "API 동의 확인", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10405") Toast.makeText(context, "계정 제재", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10406") Toast.makeText(context, "API 권한 없음", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10407") Toast.makeText(context, "요청 헤더 확인", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10408") Toast.makeText(context, "종료된 API", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10409") Toast.makeText(context, "이미지 용량 초과", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10410") Toast.makeText(context, "타임 아웃", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10500") Toast.makeText(context, "요청 오류", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10503") Toast.makeText(context, "서비스 점검중", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10411") Toast.makeText(context, "아이디 중복", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10412") Toast.makeText(context, "닉네임 중복", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10413") ExceptionDialog(context, "정보 불일치","비밀번호가 일치하지 않습니다.").showDialog()
                if (errorBodyCode == "10414") ExceptionDialog(context, "미인증 아이디","미인증된 아이디입니다.\n학생 인증은 회원가입일로부터 영업일 기준 1~3일 정도 소요됩니다.").showDialog()
                if (errorBodyCode == "10415") ExceptionDialog(context, "멤버 찾기 실패","멤버 찾기 실패").showDialog()
                if (errorBodyCode == "10416") ExceptionDialog(context, "아이디 찾기 실패", "일치하는 정보가 없습니다.").showDialog()
                if (errorBodyCode == "10417") ExceptionDialog(context, "비밀번호 찾기 실패","일치하는 정보가 없습니다.").showDialog()
                if (errorBodyCode == "10418") ExceptionDialog(context, "회원가입 실패", "이미 가입한 핸드폰 번호입니다.").showDialog()
                if (errorBodyCode == "10419") Toast.makeText(context, "인증번호 발송 실패", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10420") Toast.makeText(context, "인증번호 불일치", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10511") Toast.makeText(context, "카테고리 없음", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10512") Toast.makeText(context, "게시판 없음", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10513") Toast.makeText(context, "이미 좋아요 누른 게시판", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10514") Toast.makeText(context, "좋아요 없음", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10515") Toast.makeText(context, "이미 스크랩한 카테고리", Toast.LENGTH_SHORT).show()
                if (errorBodyCode == "10516") Toast.makeText(context, "스크랩 안한 카테고리", Toast.LENGTH_SHORT).show()
            }

            is UnknownHostException -> Log.d("logFish", "UnknownHostException")

            else -> Log.d("logFish", "Coroutine Exception Handler Error.")
        }
    }
}

따라서 View의 context와 Coroutine Exception Handler의 throwable을 인자로 받는 excaption 함수를 구현해 해당 object에서 모든 에러처리를 작성했다.

에러마다 토스트를 띄우거나 커스텀 다이얼로그를 띄우도록 제작했다.

 

//Coroutine Exception Handler
private val exceptionHandler = CoroutineExceptionHandler{ _, throwable ->
	throwable.printStackTrace()
	context?.let { CoroutineException.exception(it, throwable) }
}

//Coroutine 통신 구현 부분
lifecycleScope.launch(exceptionHandler) {
	...
}

통신을 구현한 클래스에다 object로 제작한 객체만 넣어주고 통신할때마다 exceptionHandler만 불러오면 알아서 해당 뷰에 에러코드에 맞게 에러 처리를 해준다.

이 방법의 장점은 에러코드가 수정 혹은 추가 되더라도 해당 object 에서 모든 에러코드를 관리하기 때문에 저부분만 손보면 위 캡쳐사진 처럼 자동으로 view에 알맞게 들어간다.

 

이로써 Kotlin + Coroutine + Coroutine Exception Handler를 사용해서 통신하는데 있어서 기초 공사를 끝냈다.

앞으로는 기계처럼 통신 코드 찍어내기만 하면 된다ㅎㅎ 화이팅~