마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

코루틴 api 2개 호출 및 네트워크 끊었을때

0 추천

 

class MainActivity : AppCompatActivity() {
    private lateinit var retService: AlbumService

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        retService = RetrofitInstance
            .getRetrofitInstance()
            .create(AlbumService::class.java)
        getRequestWithQueryParameters()
        //getRequestWithPathParameters()

    }


    private fun getRequestWithQueryParameters() {
        val coroutineScope = CoroutineScope(Dispatchers.Main)

        coroutineScope.launch {
            try {
                val response = async {
                    retService.getSortedAlbums(3)
                }
                val response1 = async {
                    retService.getSortedAlbums(3)
                }
                successView(response, response1)
            }catch (e : Exception) { }
        }
    }

    private suspend fun successView(first : Deferred<Response<Albums>>, second : Deferred<Response<Albums>>) {
        first.await().body()?.let {
            val albumsList = it.listIterator()
            Log.e("ljy", "res1 " + albumsList)
        }
        second.await().body()?.let {
            val albumsList = it.listIterator()
            Log.e("ljy", "res2 " + albumsList)
        }
    }

}

.현재 이렇게 하면 두개가 다들어오는건 됩니다만 네트워크를 끊었을때(와이파이, 모바일데이터 껐을때) 앱이 죽습니다

rxjava는 doOnError 아무것도안쓴 레트로핏 그냥 기본은 onfailure을 타던데

코루틴은 어떻게 잡아야되는건가요..? 그리고 이렇게 2개 비동기호출하려면 이게맞나요?

수원통학러 (3,570 포인트) 님이 2021년 8월 1일 질문

1개의 답변

0 추천

먼저  Activity나 Fragment에는 Jetpack에서 이미 LifecycleScope을 이미 지원하고 있어서 별도로 중복해서 CoroutiineScope를 만드실 필요가 없구요. 그리고 lauch 빌더에 Dispatcher를 별도로 지정하지 않으시면 아마 Default가 사용되는 걸로 아는데, 님이 원하는 Network 호출은 Dispatchers.IO를 쓰시는 게 권장사항입니다. 

그리고 Scope안에서 async를 통해 여러개 네트워크 호출을 하는 부분은 맞습니다.

인터넷이 연결이 안될 경우는 제 테스트 결과로는 Retrofit이 관련된 exception을 던지므로 try catch를 써서 해당 Exception을 잡아주셔야 합니다. Kotlin을 사용할 경우는 랭귀자 디자인상 Exception을 던지는 것보다는 잡아서 Exception임을 알 수 있는 클래스로 변환해서 리턴해주는 것이 일반적입니다. 이 부분은 자바 개발자들이 Kotlin에 대해 불평을 하는 이유 중의 하나이긴 한데, 언어자체가 그렇게 디자인이 되어있습니다.

따라서 네트워크 호출을 담당하는 클래스를 하나 만드셔서 여기서 exception를 잡으신 다음에 적절한 클래스로 변환해서 리턴해주도록 하는 것이 좋습니다. 흔히 사용하는 패턴을 예를 들면,

// 에러와 성공을 나타내는 sealed class. 에러일 때는 NetworkResult.Error, 성공일 때는
// NetworkResult.Success 를 리턴합니다.
sealed class NetworkResult<T> {
    data class Error(val e: Exception): NetworkResult<Nothing>()
    data class Success(val data: T): NetworkResult<Nothing>()
}


// 함수나 클래스를 만들어 retrofit service 함수 try catch 안에서 실행합니다.
// call 매개변수는 으로 T타입을 매개변수로 가지고 R 타입을 리턴하는 suspend function입니다.
// 필요하다면 Extension function으로 만들어서 사용하실 수 있을 것 같습니다.
fun tryApiCall(request: T, call: suspend (T) -> R): NetworkResult<R> {
          try {
                val result = call(request)
                NetworkResult.Success(result);
          } catch (e: IOException) {
                NetworkResult.Error(e) 
          }
}

// 아래는 위의 함수를 이용하는 호출을 하는 코드입니다. 둘다 같은 코드입니다.
tryApiCall(request = 3, call = retService::getSortedAlbums)
tryApiCall(3) { albumId -> retService.getSortedAlbums(albumId) }

 

위와 같은 형태로 하실 수가 있을 겁니다. 키포인트는 Exception을 잡아서 적절한 형태의 Error와 Success를 표현할 수 있는 타입으로 리턴하는 겁니다. 그리고 일반적으로는 네트워크 호출은 data layer라고 부르는 별도의 패키지를 독립적으로 두고 거기에서 처리합니다. 뷰는 네트워크 호출이 어떻게 이루어지는지 알 필요가 없습니다.

네트워크 호출을 하시기 전에 인터넷 연결 상태를 체크해서 연결이 안된 상태는 호출을 하지 않고 에러메세지를 던지도록 추가할 수도 있겠죠.

spark (224,800 포인트) 님이 2021년 8월 1일 답변
질문 사항이 있습니다
일단 api호출 2개를 비동기로 처리해서 한번에 처리하는?형식으로 해보고싶어서
일단 똑같은 api를 두개 쓰긴했는데, 답변주신거처럼 맨아래 두줄을 쓰면 제가 원하는  aync해서 여러개를 호출할 수있는건지.. 그리고 적용을 해보려했는데
잘 이해가 안되서 맨아래 두줄 api호출하는 코드를 지금 제가 쓴 launch 안에 넣어줘야되는게 맞나요? Main대신 Io로 고치고? 그리고 제가 쓴 successView 메소드 대신 답변주신 tryApiCall로 처리해야되는건지.. 답변주신거를 어떻게 두개 엮는건지를 잘 모르겠습니다
private fun getRequestWithQueryParameters() {
        val coroutineScope = CoroutineScope(Dispatchers.IO)

        coroutineScope.launch {
            try {
                val response = async {
                    tryApiCall(request = 3, call = retService::getSortedAlbums)
                }
                val response1 = async {
                    tryApiCall(request = 3, call = retService::getSortedAlbums)
                }
                successView(response, response1)
            }catch (e : Exception) { }
        }
    }
이렇게 쓰는게 맞는건지궁금합니다
우선 아래처럼 외부 async를 사용할 때 async 바깥에서 try-catch를 하면 아마 Exception이 잡히지 않을 겁니다. (제 기억으로는 그런데, 테스트를 해보세요)

private fun getRequestWithQueryParameters() {
        val coroutineScope = CoroutineScope(Dispatchers.Main)

        coroutineScope.launch {
            try {
                val async1 = async { retService.getSortedAlbums(3) }
                val async2 = async { retService.getSortedAlbums(3) }
                ...
            }catch (e : Exception) {
   
            }
        }
    }


private fun getRequestWithQueryParameters() {
        val coroutineScope = CoroutineScope(Dispatchers.Main)

        coroutineScope.launch {
                val async1 = async {
                      try {
                           retService.getSortedAlbums(3)
                      } catch(...) {}
                }
                val async2 = async {
                      try {
                         retService.getSortedAlbums(3)
                       } catch(...) {}
                }
                ...
        }
    }
 처럼 가장 내부의 scope에 try-catch를 하시거나 CoroutineScope 생성할 때 SupervisorJob를 인자로 넘기셔야 할 거예요. 이게 코루틴의 직관적이지 못한 부분 중의 하나예요.

따라서 retService.getSortedAlbum(3)를 직접 호출하시 마시고 try-catch가 적용된 함수를 호출하시는 게 더 편하실 거예요. 아래처럼 하실 수도 있구요.

fun safeGetSortedAlbum(flag: Int): ReturnType {
   try {
      return retService.getSortedAlbum(flag)
   } catch (e: Exception) {
      doSomething()
   }
}

private fun getRequestWithQueryParameters() {
        val coroutineScope = ...
        coroutineScope.launch {
             val async1 = async { safeGetSortedAlbum(3) }
             val async2 = async { safeGetSortedAlbum(3) }
        }
    }

try-catch를 구체적으로 어떻게 할지는 님의 선택입니다. 저는 기본 아이디어만 제공해 드린거구요. 테스트가 안된 코드라 그대로 사용하시지는 마세요.  키포인트는 가장 내부의 suspend함수 내에서 try-catch를 먼저 하고 이걸 호출해서 사용하라는 겁니다.
...