[Android] Coroutine을 공부하며..

2021. 10. 31. 13:24android

1. 코루틴은 Callback을 제거하여 sequential 한 코드를 작성할 수 있도록 도와준다. 


// retrofit에서 이용하는 KAPIService
 interface KAPIService {

    @GET("problem/class")
    suspend fun getProblemsOfClass(): List<KProblemsOfClass>   
}
 
 ...
 fun getClass() {
      viewModelScope.launch {
         try {
              _classList.value = KAPIGenerator.getInstance().getProblemsOfClass()
         } catch (e: Exception) {
              Timber.e(e.message.toString())
         }
	}
}

 

1. KAPIGenerator는 retrofit 객체를 생성하여 APIService를 이용할 수 있도록 도와주는 Singleton Object이다.

2. KAPIService에서 suspend function인 getProblemsOfClass()를 호출 시,  별도의 콜백처리 없이 sequential하게 결과 값을 얻을 수 있다. 

3. viewmodelScope의 Dispatchers는 Dispatchers.Main에서 실행되지만 suspend function은 해당 function이 실행되는 thread를 blocking하지 않고 suspended하기 때문에 마치 Main에서 비동기 통신이 되는 것처럼 코드를 작성할 수 있다. 

4. retrofit에서 내부적으로 enqueue를 구현하여 해당 suspend function이 동작하게 되고 오류 발생 시, cancel이 가능하기 때문에 main-safe한 방식이다. 

 

CallBack을 사용하는 방식은?

       KAPIGenerator.getInstance().getProblemsOfClass().enqueue(object :
                    Callback<List<KProblemsOfClass>> {
                    override fun onResponse(call: Call<List<KProblemsOfClass>>,
                        response: Response<List<KProblemsOfClass>>) {
                        
                        _classList.value = response.body()
                        
                    }
                    override fun onFailure(call: Call<List<KProblemsOfClass>>, t: Throwable) {

                    }

    	})

 

1. 기본적으로 콜백은 async의 결과 값을 caller에게 전달하는 용도로 사용될 수 있다. 콜백을 통해 결과를 리턴하는 작업은 훌륭한 방식이지만, background thread에서 다시 Main UI thread로 전환시키는 작업을 동반해야 한다.

2. 코드 작성이 간결하지 못하다. 또한 중첩 콜백을 사용하는 경우 코드 가독성이 떨어진다. 

3. 중첩적으로 콜백을 사용하는 경우 복잡한 코드 구성을 하게된다. 

 

2. launch Coroutine Builder를 사용해 Exception을 캐치하자


만약 non-coroutine(코루틴 내에서 호출되는 suspend function 같은 경우. 즉 coroutine을 직접 호출하지 않고 coroutine에 의해 호출되는 메서드)에서 uncaught Exception이 발생한다면 이를 캐치하기 위해서 launch 빌더를 사용해야 한다.

launch 빌더를 사용하면 uncaught Exception 발생 시, 자동으로 uncaught exception을 caller coroutine에게 에러를 던지기 때문에 적절히 핸들링이 가능하다.   

 

반면에 async Coroutine Builder로 구성된 코루틴에서 non-coroutine 코드 실행 시, uncaught Exception이 발생하더라도 await code가 실행되기 전까지 caller coroutine에게 exception을 알리지 못한다. 

 

* Repository class

class Repository() {
    private val networkService = object : NetworkService() {
        override suspend fun getSolvedProblems(userId: String): SolvedAlgorithms {
            return KAPIGenerator.getInstance().getSolvedProblemList(userId)
        }
    }
    
    suspend fun getSolvedProblems(userId: String): SolvedAlgorithms {
        return networkService.getSolvedProblems(userId)
    }
}

 

*ViewModel의 method

  private fun getSolvedProblems() {
        viewModelScope.launch {
            try {
                _solvedAlgorithms.value = repository.getSolvedProblems("hello0818")
            } catch (e:Exception){
                Timber.e(e.message.toString())
            }
       }
  }

1. repository의 getSolvedProblems()를 non-coroutine라고 한다. 즉 coroutineSope는 정의되어 있지 않지만, coroutineSope에서 호출되어야 하는 method를 말한다.

2. ViewModel에서 getSolvedProvblems()에서 viewModelScope를 사용하여 repository의 non-coroutine method를 호출한다. 이때 코루틴 빌더가 launch일때 non-coroutine method에서 예상치 못한 에러를 바로 캐치할 수 있다. 

3. 만약 코루틴 빌더가 async 였다면 await()가 호출되기 전까지 에러가 발생하는지 알 수 없다. 

 

3. suspend keyword는 thread를 blocking하는 것이 아닌 suspend 한다


Dispachers.Main은 UI thread를 의미한다. 따라서 Coroutine Builder의 dispacher가 main인 경우에는 Coroutine Scope 내에서 네트워크 통신, 무거운 작업 등은 별도의 Coroutine scope를 만들거나 withContext를 사용하여 thread를 전환 후 작업을 진행하고 결과를 반환하는 방식으로 만들어야 한다. 

 

4. Retrofit과 Room은 main-safe 하다 


1.Room은  query와 transaction 사용 시 내부적으로 Executor를 사용하여 별도로 suspend function을 구현하고 이를 사

용한다.

 

2. Retrofit는 내부적으로 콜백을 생성하고 이를 호출하는 방식으로 suspend function을 구현 해 사용한다. 

 

따라서 Dispachers.Main에서 해당 라이브러리의 메서드를 호출하면 Main에서 실행되는 것처럼 보이지만 내부적으로는 별도의 스레드에서 동작하여 이에 대한 결과를 반환하기에 main-safe한 코드를 작성할 수 있다. 

 

5. Job 객체를 통해 cancel을 전달할 수 있다.


비동기 통신 도중 유저가 앱을 꺼버리는 등 다른 행동으로 현재 작업 중인 결과 값이 필요없을 수가 있다. 때문에 해당 작업을 cancel하여 작업 중인 내용을 멈추게 만들도록 할 수 있다. 

 

1. 동기화 작업을 진행하고 있을 때

 

: worker thread에서 execute()같은 동기화 작업 시, execute 결과 값이 오기 전까지 절대 다음 내용을 수행하지 않는다. 이처럼 필요없는 작업을 끝까지 기다리지 않도록 coroutine을 cancel 하여 safe하게 만들 수 있다. 

fun main() = runBlocking {
    val job = launch {
       val data = execute()
        
       println("data is $data")
    }
    
    delay(1300L)
    job.cancelAndJoin()
    println("main: Now i can quit")
}

suspend fun execute(): Int {
    delay(3000L)
    return 10
}

1. execute()를 동기적으로 데이터를 불러오는 method처럼 구현했다. 즉 3초 이후에 10을 리턴한다.

2. main()에서는 1.3초 이후 job을 cancel한다. 그렇기에 execute()를 호출하여 리소스가 묶여있는 job은 execute의 결과값을 기다리지 않고 즉시 취소된다. 

3. 따라서 data를 표시하는 println은 찍히지 않고 main에 있는 println만 로그로 남게된다. 

 

 

2. 반복문과 같은 리소스를 점유하고 있는 작업을 할 때 

 

: while, for 처럼 리소스를 점유하여 해당 작업이 끝난 후 다른 작업이 처리되는 코드 일때 cancel 요청이 안될 수 있다. 

이를 해결하기 위한 방법으론 yield method를 이용하는 방식, isActive를 통해 현재 coroutine이 유지되는지 주기적으로 체크하여 리소스를 점유하고 있는 작업을 탈출 할 수 있도록 만들어줌으로써 핸들링이 가능하다. 

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()

    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i =0;

        while(i < 5){
            if(System.currentTimeMillis() >= nextPrintTime){
                println(i++)
                nextPrintTime +=500L
            }
        }
    }
    
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now i can quit")
}

 

-> while처럼 리소스를 잡아먹고 있는 반복문이 실행될 때, cancel 요청이 제대로 되지 않을 수 있다. 실제 결과 값 또한 모두 while은 중간에 cancel되지 않았다. 

 

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()

    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i =0;

        while(isActive){
            if(System.currentTimeMillis() >= nextPrintTime){
                println(i++)
                nextPrintTime +=500L
            }
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now i can quit")
}

-> while에서 사용된 isActive는 오직 job이 현재까지 cancel되지 않았는지를 체크하여, 유효하면 true를 cancel요청이 들어왔다면 false를 반환한다. 따라서 job.cancelAndJoin()이 요청되면 isActive = false이기에 반복문을 탈출한다. 이로 인해 job cancel이 가능하다. 

'android' 카테고리의 다른 글

LiveData 구조 및 원리  (0) 2021.11.09
디자인 패턴의 차이점  (0) 2021.10.11
2021 네이버웍스 인턴 후기  (3) 2021.09.13
코루틴이란?  (2) 2021.04.29
Glide란?  (0) 2021.01.18