05. Retrofit Return Type 커스텀

개요

Android Project를 진행하면서 API를 연결할 때 보통 Retrofit을 사용하게 된다.

다만 Retrofit으로 받을 때, Return Type으로 Response를 붙이거나, 바로 Return Type을 명시할 수도 있는데

Response Type Response로 감싸는 방법

Response Type 바로 Return Type을 명시하는 방법

Return Type을 바로 명시할 경우, 400과 같은 통신 에러, 혹은 Return Type이 다르면 바로 Crash가 발생하기 때문에

Response로 Return Type을 감싸고, isSuccessful로 통신이 성공적이었는지를 확인하는 로직을 통해 분기처리를 진행하게 된다.

isSuccessful isSuccessful을 통한 분기 처리

다만 Response Type으로 감싼 것의 Data는 body()를 통해서 가져올 수 있는데,
문제는 이렇게 가져온 body()의 Type은 Nullable 하다는 것이다.

Nullable한 Body의 Return Type 분명 Response로 감싼 Return Type은 String이었지만, String?이 반환되는 것을 알 수 있다.

이러한 이유로 작은 프로젝트의 규모라면 큰 문제가 아닐 수 있지만, 규모가 작지 않을 경우 매 통신마다

  • isSuccessful을 이용한 분기처리
  • ?.let 등의 방법을 이용한 Null Check

를 진행해야 한다.

이를 따로 함수로 만들어서 보일러 플레이트 코드를 줄일 수도 있겠지만,
애초에 Response Code가 200일 때는 A로, 400일 때는 B와 같이
에러가 발생했을 때 Exception 설정이라던지 등을 위해서는
위 방법으로는 한계가 존재한다.

또한 통신 성공 유무에 따라 다른 Response Type으로 돌려주는 경우도 있으며,
서버에서 던진 Error 데이터를 적극적으로 이용하여 에러 처리를 하면 좋을 것이다.

이를 해결하기 위해 Retrofit의 CallAdapterFactory, CallAdapter, Call을 Custom하여
우리가 원하는 응답으로 변환할 것이다.

코드의 양이 매우 방대하고, 설명을 주석에 자세하게 달아놓았으므로 꼭 fork 등을 통해 파일을 직접 보면서 설명을 듣는 것을 권장한다.

  • 목표

    • 성공적으로 API 통신이 된 경우, CommponResponse라는 Type 내의 Data 속성에 요청한 데이터를 담아 보낼 것이다.
    • 제대로 통신이 되지 않은 경우, ErrorResponse라는 Type으로 errorCode와 message를 담아서 보내줄 것이다.
    • 에러가 발생하더라도, 호출하자마자 터지는 것이 아니라 Kotlin의 Result를 이용하여, getOrThrow()
      직접 호출한 시점에 데이터 추출 or 에러가 터지도록 하여 에러가 예상 가능한 범위에서 발생하도록
      발생한 위치를 보다 파악하기 쉽게 할 것이다.

    결론적으로 String의 데이터를 요청하는 API 였다면 Result<CommonResponse<String>> 꼴로 Return Type이 결정될 것이다.

  • 실습 코드

    • Android 코드

    • 서버 코드

CallAdapterFactory

Call Adapter Instance를 생성하는 팩토리이다.

returnType을 통해서 Type이 의도한 타입이 맞는지를 확인하고, 맞는 경우 Call 객체를 입맞에 맞게 변환하는 CallAdapter를 return 한다.

Nullable한 Body의 Return Type

Retrofit 2.6.0 버전부터는 suspend 키워드를 붙이면 내부적으로 Call로 return 타입을 감싼 일반 함수로 동작하여 처리된다고 한다.
Retrofit suspend 함수 Call 변환

그래서 아래 꼴로 돌아간다고 하며,

// file: "Example.kt"

// 이렇게 선언하면
@GET("/")
suspend fun getUser(): User
         
// 내부적으로 이렇게 처리됨
fun getUser(): Call<User>

CallAdapter 구현 일부 CallAdapter 구현 일부

정상적인 요청이라면 Call<우리가 설정한 Return Type>꼴로 resultType에 들어오기 때문에
제너릭 타입을 포함하는지, Call이 맞는지를 확인하고,
Call 내부의 제너릭이 Result가 맞는지를 다시 확인하고,
맞다면 Adapter를 반환한다.

CallAdapter

Call의 내부 타입을 다른 타입으로 변환해주는 인터페이스이다. CallAdapter

responseType은 서버에서 응답받은 데이터의 Type을 return하면 되고,
adapt는 Custom한 Call 객체를 return하여 커스텀 한대로 통신을 처리한다.

Call

Call 객체를 구현하면서 중요한 것은 통신에 실패했더라도 Response는 success로 넘겨, Crash가 발생하지 않도록 하는 것이다.

통신에 실패했을 경우 서버에서 받은 ErrorResponse 객체를 이용하여,

code와 에러 메세지를 받고 이를 가지는 Custom한 RetrofitException을 Result.failure에 넘김으로써,
추후 API를 호출한 후 getOrThrow를 호출했을 때 에러가 발생하도록 한다.

  • 성공했을 때

    성공했을 때

  • 실패했을 때

    실패했을 때

Exception Handler

Exception Handler

실행부

suspend 함수를 호출하기 위한 Coroutine 호출 시 launch에 앞서 구현해둔 Exception Handler를 넣어준다.

이를 통해 lifecycle 내부에서 에러 발생 시 작성해둔 Handler에서 에러를 관리하게 되고,
API 호출 시 에러가 발생하더라도 Crash가 나지 않고, 의도한대로 처리할 수 있다.

본 예제에서는 성공적으로 수행된 API의 경우 내용을 TextView에 띄우는 로직과,
에러가 났을 경우 Toast로 전달받은 에러의 코드와 메세지를 출력하도록 진행하였다.

실행부

실행 결과

example 첫번째 정상적인 API는 성공하여 tv_title에 서버에서 건네준 값이 나타났고,
두번째 비정상적인 API는 Exception Handler에 걸려, 서버에서 전달한 에러 메세지를 띄운 것을 볼 수 있다.

회고

성격이랑 맞지 않게 프로젝트의 촉박한 기간상 확실하게 코드를 이해하기보다 구현에 급급해서 소홀히 넘어갔던 부분을 돌아보았다.

본인은 이를 이용하여 Clean Architecture 기반 Multi Module로 App을 설계하였을 때, DataLayer에서 발생하는 예외를 Result로 감싸 Presentation Layer로 넘기는 식으로 핸들링을 하고, Exception Handler를 통해 한 곳에서 처리를 진행함으로써, 중복되는 에러 핸들링 코드를 줄이고, getOrThrow()와 서버에서 전달받은 데이터를 적극적으로 이용하여 나름 효율적으로 진행을 해보았었다.

혹여 잘못된 부분이 있거나, 개선 방안이 있을 시 의견을 남겨주시면 감사하겠습니다.


© 2025. Na2te All rights reserved.