본문 바로가기

안드로이드/코틀린

[코틀린]Kotlin Coroutine 사용하기

반응형

안드로이드 프로그래밍에서 비동기 프로그래밍은 서버 통신 및 디비 접근과 같이 많은 부분에서 사용되는데, 그 부분을 조금 더 유연하고 간편하게 사용하도록 도와주는게 코루틴인것 같습니다.

비동시 프로그래밍시 스레드를 신중하게 다뤄야 하는데 스레드 생성 및 해제, Context-Switching시 CPU의 메모리를 소모해 많은 수의 스레드를 갖는데 어려움이 있고, 메인 스레드에서 일정시간 한가지 TASK를 수행하면 ANR에러가 발생하기 때문입니다.

그에반해 코루틴은 스레드가 아닌 서브 루틴을 일시정지(suspend)시키는 방식으로 Context-Switching비용이 발생하지 않아 라이트한 스레드라고도 불립니다.

context-swiching : 스레드 실행 혹은 종료시 스레드의 상태를 저장하고 복구하는 프로세스로, 두개의 스레드에서 cpu가 매번 스레드를 점유했다 놓아주었다를 반복할때 발생

>생성하기

코루틴은 제어 범위를 뜻하는 Scope와 실행 범위를 뜻하는 builder를 사용해 생성합니다. 실행될 스레드를 정하고 코루틴의 결과 값을 리턴받을지나 delay를 사용할지도 고려 해야합니다.

	GlobalScope.launch {
		....
	}

위 코드는 코루틴을 생성한 간단한 예로 세부적인 설정 값은 아래 설명들에서 다루도록 하겠습니다.

>Scope

Scope는 코루틴의 제어 범위를 뜻하며 GlobalScope, CoroutineScope 2가지 종류가 있습니다. 

GlobalScope는 어플리케이션이 종료될때까지 실행되는데 Activity안에서 실행한 GlobalScope 코루틴은 Activity가 종료되도 어플리케이션이 종료되지 않으면 작업이 끝날때 까지 동작합니다.  ( 별도 중지를 하지 않을 경우 ) 

파일 다운로드 같이 화면과 상관 없이 실행되는 작업이나 별도의 생명주기가 뚜렷하지 않은 receive에서 사용하면 적절할 것 같습니다.

그외 ViewModel이나 View(Activity, Fragment)의 생명주기에 맞춰 제어하거나 특정한 목적을 지니는 경우 CoroutineScope를 이용하는데 이용시 목적에 맞는 Dispachers(코루틴이 실행될 스레드)를 지정해야 합니다. 

>Dispatchers

        GlobalScope.launch {
            Log.d(TAG,"global")
        }

        CoroutineScope(Dispatchers.IO).launch {
            Log.d(TAG,"global")
        }
        

위 코드 처럼 CoroutineScope 생성시 Dispatchers를 목적에 맞게 선택합니다. 

  • Dispatchres.Default :CPU 를 많이 사용하는 무거운 작업을 할때 사용합니다.
  • Dispatchers.IO : 네트워크 통신이나, 내부디비를 다루는 가벼운 작업시 사용합니다. 메인스레드에 영향을 주지 않기 때문에 UI를 그리는일 외 대부분 작업을 실행할때 사용합니다.
  • Dispatchers.Main :메인스레드에서 동작하기 때문에 대기 시간이 있을 경우 앱이 ANR이 발생할 수 있어 UI업데이트와 같이 즉각적인 작업을 실행할때 사용합니다.

다수의 코루틴이 작동할때 Dispatcher에따른 쓰레드의 변화나 작업 순서를 이해하기 위해선 delay와 함께 로그를 출력해 결과를 확인하는게 도움됩니다.

        CoroutineScope(Dispatchers.IO).launch {
            Log.d(TAG,"1")
            launch {
                delay(2000)
                Log.d(TAG,"2")
            }
            launch {
                delay(1000)
                Log.d(TAG,"2.5")
            }
            Log.d(TAG,"3")
            withContext(Dispatchers.IO){
                delay(2000)
                Log.d(TAG,"3.5")
            }

            val value2 = async {
                delay(1000)
                Log.d(TAG,"4")
                1+2
            }
            Log.d(TAG,"5")
            val value = async {
                Log.d(TAG,"6")
                1+2
            }.await()

            Log.d(TAG,"7 value="+value)
        }

출력값 

위 코드는 제가 코루틴을 이해하는데 사용했던 예제 코드입니다. 앞서 언급하지 않았던 withContext, async, await등은 밑에서 추가 설명하도록 하겠습니다.

코루틴을 최초 생성할땐 scopebuilder(launch등)이 필수로 필요하지만 선언된 scope안에선 builder만으로 코루틴을 생성할 수 있습니다.

>Coroutine Builder

코루틴의 작업 범위를 지정할때 사용하며 launch, async, runBlocking, withContext 4가지가 있습니다.

launch 

  • job 객체를 return 합니다.
  • job은 코루틴의 작업 단위를 뜻하며 job을 이용해 실행중인 코루틴을 취소할수 있고, join()을 이용하여 job이 실행이 완료될때 까지 기다리게 할수 있습니다. 
        CoroutineScope(Dispatchers.IO).launch {
            Log.d(TAG,"1")
            var job = launch {
                delay(3000)
                Log.d(TAG,"2")
            }
            job.join()
            Log.d(TAG,"3")

출력 값

위 코드 결과 값을 보면 3초 딜레이를 후에 차례대로 출력되는걸 알 수 있습니다. 시간을 지연하는 join, delay는 코루틴 안에서만 사용 가능합니다. 

async

  • 실제 값을 반환합니다.
  • await()를 사용해 실행중인 스레드를 멈추고 결과를 반환할때까지 기다립니다. (job.join과 동일). await() 예제는 위의 코드중에 있습니다.

runBlocking

  • 별도 설정이 없어도 결과값을 반환할때까지 스레드를 중지시킵니다. 
  • Scope지정 없이도 코루틴을 생성한다.
  • 스레드를 중지시키기 때문에 메인스레드에서 사용할땐 주의해야 합니다.
        Log.d(TAG,"1")
        runBlocking {
            Log.d(TAG,"2")
            delay(3000)
        }
        Log.d(TAG,"3")
        

결과 값

withContext

  • 코루틴에서 Dispacher를 변경할때 사용한다. ( 이때 스레드도 변경된다. ) 
  • 결과 값을 반환할때 까지 기다린다. ( async + awaite()와 동일 ) 

4가지의 빌더 중에서 상황에 적합한걸 골라서 사용하면 됩니다. 

>job

코루틴을 제어하기 위해 제공되는 객체로, job을 통해 Activity나 Fragment 생명주기에 맞게 관리할 수 있습니다. job을 통해 코루틴을 관리하는 방법은 크게 2가지 입니다. 

미리 job 객체를 선언하고 코루틴 생성시 Dispatcher와 함께 사용 하는 방법과 job을 리턴하는 builder(launch)를 변수로 생성해서 관리하는 방법이 있습니다. 2가지를 구분지어 말했지만 혼용해도 무방합니다.

    
    private var job = Job() // job 객체 생성 
    
    ...
    
    CoroutineScope(Dispatchers.IO + job).launch { 
    // 코루틴 생성시 dispatchers 와 job같이 넣어준다 
    }
      
    ...
    
    job.cancel()// 필요에 의해 캔슬한다. ex) 엑티비티 종료, viewmodel 종료 

활용 

아직 저도 사용하는데 익숙하진 않지만 처음 이 글을 읽고 입문하시는 분들을 위해 공유드립니다. 

코루틴을 사용할때 매번 스코프를 생성하기보단 크게 UI용 스코프와 IO용 스코프를 변수로 초기화해서 사용하면 조금 더 깔끔한 느낌(?) 이더라구요 

open class BaseVM : ViewModel(){

    val job = Job()
    var ioScope = CoroutineScope(Dispatchers.IO + job)
    var uiScope = CoroutineScope(Dispatchers.Main + job)

    override fun onCleared() {
        job.cancel()
        compositeDisposable.clear()
        super.onCleared()
    }
}

mvvm 패턴으로 작업중인 코드에서 base viewmodel의 코드 부분 입니다. 

>suspend

'유예하다'라는 뜻을 가진 키워드로 함수 앞에 붙여서 사용하는데 해당 키워드를 붙인 함수는 코루틴 외부에서 사용했을때 컴파일 에러가 발생해 꼭 코루틴안에서만 사용해야하는 함수에 붙여서 사용하면 실수를 줄일 수 있습니다.

 

적용후 조금 더 괜찮은 방법들이 생긴다면 정리해서 포스팅하겠습니다. 감사합니다.

 

 

 

  

반응형