서버통신 시리즈(레트로핏) 1.서버통신시 에러메시지를 받아보자!!
요즘 글 쓰는 주기가 부쩍 늘어났다 공부하는 내용들이 어려워서 금방금방 정리가 안된다. 그래도 정신차리고 쪼개서라도 써보자
또한 앞으로 레트로핏 관련 오류처리 공부를 시리즈로 적어볼까 생각중이다.
(한번에 쓸려다가 너무 방대해서 아예글을 안쓸거 같아서 쪼개서 써야겠다)
그 중 첫번째 내용으로 오류 메시지를 뽑아봤던 경험을 정말 거지같이(시간에 쫓기며) 뽑은거에서부터
예쁘게 코드를 정돈해서 리펙토링해서 뽑은것까지에 대한 내용이 이번글의 주제이다.
이런 관련 공부를 하게된 계기는 또 스파크로부터 비롯 되었다.
일단 공부하게된 계기는 이렇다
스파크 기능을 구현하던중 코드로 방입장이라는 기능이 있었다.
어떠한 기능이냐하면 코드를 입력하면 서버통신 성공시 방에대한 정보가 오게 되는데
성공시 정보
{
"status": 200,
"success": true,
"message": "대기방 정보 확인 완료",
"data": {
"roomId": 1,
"roomName": "미라클 모닝",
"creatorName": "힛이",
"creatorImg": "https://storage.googleapis.-------------------/...",
"profileImgs": [
"https://storage.googleapis.-------------------/...",
"https://storage.googleapis.-------------------/...",
"https://storage.googleapis.-------------------/...",
],
"totalNums": 4
}
}
//뭔가 base url 공개하기 뭐해서 가렸다
성공시 이러한 방에대한 정보가 와서 띄워주는 상황이고
잘못된 코드를 사용자가 입력해도 단순히 로그를 찍는다던가 아니면 뭔가 다른 오류처리를 해주는것이 아닌
서버에서 보낸 중복된 코드입니다 혹은,참여할수없는 코드입니다 같은 코드에대해 왜 못들어가는지 서버에서 메시지를 보내주는 상황으로
서버통신에 실패할시 그 실패한 이유를 서버에서 분기처리해서 넘겨주었고 그부분을 메시지로 띄워주어야하는 상황이였다.
실패시 정보
이러한 정보들이 온다
그래서 post맨에다 실패시 저대로 올려나? 하고 찍어봤고 똑같이 저런형식 그대로 나오는 모습을 볼수있었다
그래서 코드를 짜며 오류시에도 들어오는 response의 메시지를 꺼내서 메시지로 띄워주면 되겠다는 판단을했다.
근데 앱잼(시간이 너무없어서 이렇게 짠거니 비난의 화살은 멈춰주세요 당신이 2주동안 밤새봤어?, 농담입니다 사실 부끄러워요❤️) 당시에 일반적으로 해야했던 서버통신은 메시지를 띄워주고 할일이 없었어서 몇가지 맹점을 가지고 갔는데
코루틴을 사용하고있었기에 레트로핏 call에 붙어있는 아주 아름다운 오류처리는 사용하지 않고있었고
다른 일반적인 400이나 500대가 오는 HttpException 오류가 안날꺼라는 가정을하고(사실 기도를하며) 오류처리는 runCatching 을 통해 간단하게 로그 찍어주는 정도로만 오류 처리를 했다.
다른 일반적 통신 예시
private fun getHomeNoticeRedDot() {
viewModelScope.launch {
homeRepository.getHomeNoticeRedDot()
.onSuccess {
_noticeRedDot.postValue(it.data.newNotice)
}
.onFailure {
Timber.tag("Home_main_error_get_home_notice_red_dot").d(it.message.toString())
}
}
}
그냥 보면 onFailure에서 어떤 오류가 났고 어디서 난건지만 로그로 찍어주는 정도의 그냥 앱아 제발 터지지만 말아다오같은 무책임한 오류처리였다.
지금은 매우반성하며 오류처리에대한 많은 검색 그리고 공부를 하는중이다 나중에 오류처리를 한번쭉 정리해볼 생각이다.
어찌됐건 본론으로 돌아가서
당장 우리팀의 구원자 연주에게 가서 이런건 어떻게해야하냐고 괴롭혔다. 그래서 얻은 답변 그런 오류메시지 꺼낼때는 tryCatch 를 쓰면 된다 였다.
그래서 흐음 트라이 캐치를 쓰면 뭔가 다르게 신기한 오류처리를 할수있나보네 하면서 구글링을 해본결과 어짜피 답없는건 마찬가지였다.
(물론 이제는 그때 왜 무슨 뻘짓을했는지 다이해했다.)
그래서 결국 했던 행동이 tryCatch 쓰면 뭔가 방법이있다고하니까 tryCatch 로 작성해놓고 어쨋든 오류관련 내용일테니 catch 에 exception으로 넘어오겠거니 생각하며 그냥 일부러 오류를 내고 안에있는것들을 디버깅을 통해 모든것들을 하나하나 다 살펴봤다.
사실 그래도 답이 잘안나와서 도데체 왜 이렇게 오류를 어디다 꽁꽁 숨겨놨을까 생각하다가
exception에 response 란게있고 그안에 errorBody라는게 있으니 여기있을거에요 하고 냄새가 나서 그냥 거기다가 toString도 해보고 자동완성되는 함수로 별의별짓을 하면서 결국 소 뒷걸음질 치듯 찾아낸게
e.response()?.errorBody()?.byteString().toString()
요렇게 errorBody를 byteString으로 바꿧다가 toString으로 바꾸면 에러 즉 내가 원했던 json이 string 값으로 그냥 날것 그자체로 넘어온다는것을 알아냈다.
그래서 그때 당시에는 또 gson으로 파싱해볼까라는 생각은 해보지도 못하고
그냥 문자열나오니까 적당히 그냥 잘라서 에러메시지만 뽑아야겠다 그리고 언젠간 이부분은 문제가있으니 돌아와서 리펙토링해야지 라는 생각을 갖고 이런식으로 코드를 써놨었다.
fun getJoinCodeRoomInfo(code: String) {
viewModelScope.launch {
try {
val response = joinCodeRoomInfoRepository.getJoinCodeRoomInfo(code)
_roomInfo.postValue(Event(response.data!!))
} catch (e: HttpException) {
val rawData = e.response()?.errorBody()?.byteString().toString()
val processedData = rawData.slice(IntRange(47, rawData.length - 4))
_errorMessage.postValue(processedData)
}
}
}
rawData로 json 받아놓고
그냥 slice로 잘라서 에러메시지만 받는거다.
이방법에 대해서
어쨋든 뭐 에러메시지가 오는게 형식이 그닥 바뀔거같지는 않지만 뭔가 그냥 문자열을 잘라서 쓴다는거 자체가 좀 불순물? 이 섞여들어 갈까봐 애초에 좋은 방법이 아니라고 생각했고 당장 기능은 원하는대로 굴러갔기에 이상태로 앱잼을 넘겼다.
그로 부터 한참후
뭔가 그냥 문득 내가 서버통신을 쓰고있으면서도 과연 레트로핏,okhttp을 잘알고있을까? 라는 생각이 들었고(추후 글을 작성할 예정
서버통신 시리즈 2편)
그리고 앱잼때 멘토님이 조언해주셨던 코루틴으로 오류처리를 하는것보다
suqare사 에서 아름답게 만들어놓은 call객체로 그냥 callBack 써서 아름답게 오류처리를 하는게 오히려 좋을수도 있다는 조언을 듣고
(이것도 글을 쓸예정 서버통신 시리즈 3편 -> 레트로 핏에서 받아올때 Call,Response,혹은 원하는 데이터 자체중 어느것을 받아와야 효율적인 서버통신이 가능한가 + 코루틴을 쓰는 상태에서 Call 에서 제공하는 오류처리를 다 커버할수있는 방법?)
과연 난 서버 통신에 대해 얼마나 알고있는가 라는 의문과
오류처리를 어떻게 해야 과연 잘하고있고 앱품질을 높일수있을까라는 생각이 들었고
불현듯 그때 오류 메시지!!!! 하면서 오류 메시지를 예쁘게 뽑을 방법을 찾아보고 리펙토링을 하려는 시도를 시작해보았다.
그러면서 오류처리관련 공부를 하며 callback 으로 오류처리하는 것과 tryCatch 로 오류처리 하는것 그리고 마지막으로 runCatching 으로 오류 처리하는 방법을 쭉 공부해보았다.
일단 callBack으로 어디 부분에서 오류처리를 해줬는지 그리고 errorBody가 json으로 넘어오는것을 어떻게 코틀린 데이터 클래스로 파싱할건지 두가지 방법에대해서 알아보고
그 이후에 그작업을 runCatching 과 tryCatching 으로는 어떻게 같은 작업을 할수있는가에 대해 이야기해보려고한다.
1.CallBack
우선 callBack 중 오류 메시지 뽑는 부분만 작성해놨고 나머지 부분은 공부를 위해서 어느시점에 분기처리되는지 보기위해 로그만 찍어놨다.
joinCodeRoomInfoRepository.getJoinCodeRoomInfo(code)
.enqueue(object : Callback<BaseResponse<JoinCodeRoomInfoResponse>> {
override fun onResponse(
call: Call<BaseResponse<JoinCodeRoomInfoResponse>>,
response: Response<BaseResponse<JoinCodeRoomInfoResponse>>
) {
if (response.isSuccessful) {
if (response.body()?.success == true) {
if (response.body()?.data != null) {
Timber.e("네트워크 요청 성공 -> 원하는 데이터 : ${response.body()!!.data!!}") //1
} else {
Timber.e("네트워크 요청 실패! 5") // 5
}
} else {
Timber.e("네트워크 요청 실패! 4") // 4
}
} else {
Timber.e("네트워크 요청 실패! 3") // 3
//Json 파싱을 레트로핏내부에 들어있는 컨버터를 이용해서 파싱하기
val data = response.errorBody()?.let {
retrofit.responseBodyConverter<NoDataResponse>(
NoDataResponse::class.java,
NoDataResponse::class.java.annotations
).convert(
it
)
}
//사실 난 Gson 을 사용하고있으니까 그냥 Gson 객체를 생성해서 그걸로 파싱할래 ㅎㅎ
val data = Gson().fromJson(response.errorBody()?.string(),NoDataResponse::class.java)
if (data != null) {
Timber.e(data.message)
}
}
}
override fun onFailure(
call: Call<BaseResponse<JoinCodeRoomInfoResponse>>,
t: Throwable
) {
Timber.d("네트워크 요청 실패! 2") //2
}
})
우선 통신 할때 오류 분기처리가 어떻게 되는지 일단 보고 그다음 우리가 에러 메시지를 뽑을수있는지점인 3번 지점에서 어떤 방식으로 에러 메시지를 뽑을수있는지 알아보자
분기처리를 보기전 참고사항
- 1xx(정보) : 요청을 받았으며 프로세스를 계속 진행합니다.
- 2xx(성공) : 요청을 성공적으로 받았으며 인식했고 수용하였습니다.
- 3xx(리다이렉션) : 요청 완료를 위해 추가 작업 조치가 필요합니다.
- 4xx(클라이언트 오류) : 요청의 문법이 잘못되었거나 요청을 처리할 수 없습니다.
- 5xx(서버 오류) : 서버가 명백히 유효한 요청에 대한 충족을 실패했습니다.
우선 쭉 분기처리한것을 보자면
1. //1은 모든분기처리를 통과하고 통신이 성공하여 내가 원하는 데이터가 나에게 주어져 그걸로 작업을 하면되는 상황이다.
2. //2는 서버와의 통신이 아예 실패한 상황이다 인터넷이 연결이 안됐다거나 서버가 닫혔을수가 있고 TimeOut 같은 서버에서 아예 응답을 받지 못하는 상황이지 경로를 틀린다던가해서 서버에서 뭐가 날아오기는 할때는 이쪽으로 분기처리되지 않는다.
3.//3은 Response에서 코드가 200이상 300 미만이 아닌경우이다. 즉 뭐 서버에서 뭐가 날아오긴 했는데 클라이언트 오류이거나,서버오류,혹은 100번대인데 사상 거의 왠만해서 400아니면 500을 받는 상황이 오지 않을까 싶다.
-> 여기서 에러 메시지를 뽑는다. 이 이후 에러 분기처리는 응답은 잘오긴했는데 뭔가 부족한 아이들이다.
4.//4는 response로 넘어오는 success값이 false일때인데 서버개발자가 개발한 로직상 실패했을때이고 실상 isSuccessful 이 true인데 이쪽으로 넘어올일은 거의 없다고 봐도 무방하다.
5.//5는 서버통신을 성공했는데 데이터가 없는경우이다. 가끔 데이터가 필요없어 코드만 200~300 으로 넘어오는 응답이 있는데 그럴때 이쪽으로 넘어온다.
-> 실상 2,3번 상황만 Retrofit 라이브러이에서 결정해주는 실패사항이고 나머지 오류처리는 서버형식에 따라 달라질수도있고 아예 불필요하다 생각하면 없애버릴수도있는 부분이다.
오류 처리에 대한 참고글 + 공부한 내용의 출처
(데이터만 순수하게 챙겨오자는 내용이지만 전반적인 통신에대해 잘설명한 명주의 글)
에러메시지 를 뽑아보자!!
어쩃든 이러한 상황이니 3번 에다가 에러메시지 뽑는 코드를 냅다 갈기면된다. 이제 에러메시지를 뽑는 코드를 자세히 살펴보자
위에 코드에는 2가지 방법을 다 혼재되게 넣어놔서 헷갈리는데 둘다 같은것을 배출하니 둘중하나만 이용하면 될것이다.
1.레트로핏에 들어있는 컨버터 이용하기
우리는 레트로핏에 컨버터를 하나씩 넣어놨을것이다. 그래서 레트로핏에 넣어논 컨버터를 가져와서 json을 파싱하는 방법이있다.(gson
우선 레트로핏을 받아서 사용하기위해 힐트로 주입받았다.
@HiltViewModel
class InputCodeFragmentDialogViewModel @Inject constructor(
private val joinCodeRoomInfoRepository: JoinCodeRoomInfoRepository,
private val retrofit: Retrofit
) : ViewModel() {
//어쩌구 저쩌구 많은 코드들
}
그리고 사용해야할 지점에서
val data = response.errorBody()?.let {
retrofit.responseBodyConverter<NoDataResponse>(
NoDataResponse::class.java,
NoDataResponse::class.java.annotations
).convert(
it
)
}
이렇게 response body 자체를 레트로핏안에 있는 responseBodyConverter 에 넘겨서 파싱을 하는데 이때
어떻게 파싱될지 타입 데이터 클래스를 넣어줘야하는데 여기서 데이터가 없는 답변이 왔을때 썻던 model인 NoDataResponse를 사용했고
이부분에 대해서는 더 공부해야겠지만 두번째 인자로 어노테이션을 넘겨줘야했다.
그래서 convert 하면 string 값으로 있던 데이터가 NoDataResponse 형식의 데이터 클래스로 뿅하고 값이 담겨서 나온다 그럼 쉽게쉽게 데이터를 원하는대로 가져다 쓸수있는것이다.
문자열을 자르는것보다 이거 훨씬 문제가될 가능성도 적고 코드가 좋아보인다.
이제 두번째 방법을 살펴보자
2. 걍 난어짜피 Gson 쓴다고 뭔 레트로핏에 있는걸 또 뺴서써 그냥 Gson 가져와서 바로 파싱할래
이방법은 그냥 컨버터 물론 다른거 쓸수도있겠지만 난 gson 쓰고있으니까 그냥 gson 가져와서 파싱해 버릴래 라는 방법이다.
val data = Gson().fromJson(response.errorBody()?.string(),NoDataResponse::class.java)
이렇게 그냥 Gson 객체를 만들어주고 fromJson 이라는 메서드를 통해서 첫번째 인자에 Json(string)으로 되어있는 그리고 두번째 인자로 그걸 받을 데이터클래스를 타입으로 넘겨주면 NoDataResponse형태로 객체를 만들어서 뿅준다.
참고: 아까 최초에 Json 스트링으로 받겠다고 막 byteString을 toString으로 바꾸고 생난리였는데
e.response()?.errorBody()?.byteString().toString()
그냥 string() 을 통해 단순하게 문자열 형태의 json을 받을 수 있었다(약간허탈)
response.errorBody()?.string()
어쩃든 이렇게 두방법을 통해서 내가 원하는 형태로 객체화된 (포스트맨에서 봤던) 에러 메시지, 에러코드 등을 뽑아낼수있어서 오류 처리하는데 예쁘고 유용하게 사용할수 있게 되었다.
그럼 이제 이걸 코루틴을 사용했을때는 어떠한 형태로 오류처리를 통해서 뽑아낼지에 대해서 알아보자
2.TryCatach
추후에 서버통신 관련 오류처리 글을 쓰려고하니 이글에선 가볍게 이야기 하려고한다.
기본적으로 코틀린에서 오류처리할때는 try catch 를 사용해왔고 내가보기엔 뭐 runCatching 이랑 별다를것 없지 않나 생각한다. runCatching 에대한 내용은 글의 뒷부분에서 살펴볼테니 이만하고
우선 try catch 의 기본 사용법은 모두다 알것이다.
그냥 try{}에다가 코드를 작성해놓고 그 안에서 오류가 나면 catch {}의 코드가 실행되며 그떄 catch 에 넘어가는 인자 즉 exception의 종류로 분기처리를 할수있다. 즉 다양한 exception 의 catch 를 하나의 try블록에다가 달아놓으면 오류가 무엇인가에 따라서 그것에 맞는 catch 블록을 실행한다.그냥 분기처리하기도 귀찮고 그냥 에러나면 catch{} 하나로가면 그냥
catch (e : Exception) {
println(e)
}
이렇게 퉁쳐서 쓰면된다.
그리고 추가적으로 선택사항인 finally{} 를 붙여놓으면 오류가 나든 안나든 즉 try{},catch{} 중 어느것이 실행되었다고 하더라도
마지막에 finally{} 는 실행된다.
그래서 사실상 기본적인 오류 처리이고 이걸로도 오류처리를 다할수있으니 trycatch 로도 오류처리를 해보도록 하겠다.
viewModelScope.launch {
try {
val response = joinCodeRoomInfoRepository.getJoinCodeRoomInfo(code)
_roomInfo.postValue(Event(response.data!!))
} catch (e: HttpException) {
val data = Gson().fromJson(
e.response()?.errorBody()?.string(),
NoDataResponse::class.java
)
_errorMessage.postValue(data.message)
}
}
사실상 하는 행동은 gson으로 문자열을 NoDataResponse 객체로 만들어서 뽑아주는것으로 똑같다
여기서 핵심은 catch 의 exception 이 HttpException으로 분기처리가 되어있어
인자로 들어오는 exception 을 통해서 response 에 접근해서 errorbody를 받아서 처리할수 있는 것이다.
만약 인자로 들어오는 exception의 자료형을 HttpException 이 아닌 그냥 Exception 이런거로 해놓았을시에는 들어오는 인자에서 애초에 response 에 접근할수없게 되어있다 즉 HttpException 이니까 가지고있는 response에 접근할수있는것이다.
그래서 이렇게 HttpException을 따로 분기처리해줘서 json 값을 뽑아내서 처리하면 될것이다.
3.RunCatching
이제 마지막으로 runCatching 을 볼것이다 코틀린에서만 제공하는것이고 좀더 코드의 문맥을 잘볼수있도록 실행부와 성공했을시 적용할코드 오류가 났을시 적용할 코드의 영역을 뚜렷이 나눠놓은것이지 실상 코드 내부를 뒤집어보면 그냥 tryCatch 로 이루어져있어 tryCatch 랑 똑같다.
@InlineOnly
@SinceKotlin("1.3")
public inline fun <R> runCatching(block: () -> R): Result<R> {
return try {
Result.success(block())
} catch (e: Throwable) {
Result.failure(e)
}
}
runCatching코드 자세히 보면 tryCatch 로 이루어져있다.
그래서 runCatching 의 코드를 보고 runCatching 만의 장점과 사용법을 알아볼것이다.
kotlin.runCatching{ 오류 처리를 하고싶은 코드 }
.onSuccess {
성공했을시 할 행동
}
.onFailure {
실패 했을시 할행동
}
이렇게 runCatching 은 tryCatch 와는 다르게 명확하게 오류처리를 하고싶은 부분과 성공했을시 할 행동을 분리해서 작성할수있어 코드의 가독성이 더좋아지는 장점이있다. 그리고 추가적으로 Result 타입에서 retrun 값을 가져올때 뭐 부가적으로 할수있게 getOr어쩌구 함수들도 제공하고 map,mapCatching,recover,recoverCatching 도 제공하므로 다양하게 이용해 볼 수 있을것이다.
그래서 runCatching 을 이용해서 오류 메시지를 가져올수있을까?
어짜피 tryCatch 처럼 하는일은 똑같이 Gson 을 가져다가 dataClass 형태로 바꿔주는 행동은 같다.
그런데 onFailure에서 넘어오는 인자인 e 에다가 냅다 response 가 있는지 보면 당연히 없다
위에 코드를 보면 catch 에 넘어오는 exception 인자가 그냥 Throwable이니 있을수가 없다.
그래서 response 로 접근해주기 위해서 두가지 방법을 이용해보았다.
1. as 로 강제 캐스팅하기
인자로 넘어오는 exception 을 HttpException으로 강제 캐스팅하여 response 에 접근할수있게 해주는것이다.
항상 HttpException만 난다면 이 방법으로도 문제 없이 사용할수 있을 것이다. 하지만 HttpException 이 아닌 다른 오류가 들어온다면 아마 문제가 생길것이다. 생각도 하기싫네 ㅠㅠ 어쨋든 이방법으로도 HttpException만 나는건 거뜬히 해결하긴했다.
viewModelScope.launch {
kotlin.runCatching { joinCodeRoomInfoRepository.getJoinCodeRoomInfo(code) }
.onSuccess {
Timber.d("뭐라도 나와봐")
}
.onFailure { e ->
e as HttpException
val data = Gson().fromJson(
e.response()?.errorBody()?.string(),
NoDataResponse::class.java
)
if (data != null) {
_errorMessage.postValue(data.message)
}
}
}
그래서 더 좋은 방법이 없을까 생각하다가 두번째 방법인 is로 어떤 exception 인지 검사해서 분기처리하는것을 찾았다.
2. is 를 통해 exception 분기처리
is를 통해 exception이 어떤 exception인지 체크하고 각각 상황마다 분기처리를 해준다면 여러 오류가 들어와도 문제 없이 처리해줄수있을 것이다. 그래서 이방법을 앞으로 사용해야겠다고 생각했다.
viewModelScope.launch {
kotlin.runCatching { joinCodeRoomInfoRepository.getJoinCodeRoomInfo(code) }
.onSuccess {
Timber.d("뭐라도 나와봐")
}
.onFailure { e ->
when (e) {
is HttpException -> {
val data = Gson().fromJson(
e.response()?.errorBody()?.string(),
NoDataResponse::class.java
)
if (data != null) {
_errorMessage.postValue(data.message)
}
}
}
}
}
어쩃든 이렇게 is 를 사용하면 스마트 캐스팅이 되기때문에 예쁘게 runCatching 을 통해서 오류 처리를 할수있게 되었다.
앞으로는 이방법을 제일 애용할거같다.!!
이렇게 쓰고싶은말을 다써봤다. 글이 좀 오락가락하기는 하는데 그래도 공부했던거를 많이 풀어냈던것같다.
이로써 서버통신시 오류메시지를 어떻게 꺼내올수있는가에 대해서 다뤄봤다.