안드로이드 코루틴 개념, CPS, ScopeBuilder 정리

SSY·2022년 12월 15일
0

Coroutine

목록 보기
1/7
post-thumbnail

목차
1. 코루틴 기본 개념
2. ScopeBuilder
3. LauncherBuilder
4. CPS
5. Job

1. 코루틴 기본 개념

Coroutine?
ㄴ> Co + Routine의 합성어

함께하는(=Co) 작업(=Routine)이라는 뜻으로 여러가지의 일을 동시에 처리할수 있게 해주는 라이브러리다. 즉, 비동기 작업을 효율적으로 해줄 수 있는 라이브러리를 의미하며 이는 기존의 AsyncTask나 Handler를 사용하지 않고도 더 적은코드로 직관적인 프로그래밍이 가능해진다.

물론.. 지금은 AsycTask가 Deprecated되어서 못쓰긴 하지만~ 아무튼, Coroutine이란 무엇인지 알아가기 전 아래 그림을 보면 이해가 좀 쉬울거라 생각한다.

위 그림을 보면 Entry라는 일의 시작지점이 있다. 그리고 Return이라는 끝지점도 있다. 이때 이 일을 모두 끝마치기 위해선 A, B, C라는 일(=Job)이 필요하다. 그리고 이러한 일들을 원하는 타이밍에 실행시켜줘야 하는데 이를 도와주는 동시성 라이브러리가 바로 Coroutine이다.

2. ScopeBuilder

스코프빌더. 참~ 많다. 그래도 대표적으로 많이쓰는걸 뽑아보자면 다음의 정도가 있다.

ScopeBuilder 종류
1. launch
2. async
3. withContext
4. runblocking

1. launch

이는 호출하는 쪽의 스레드를 중단 없이 시작시킬 수 있으며(안드로이드라면 주로 UI스레드), 결과를 호출한 쪽에 반환하지 않는다. 쓰임새는 아래와 같다.

launcer쓰임새
일반 함수 내부 → launcher빌더 내부 → suspend함수 호출

private val myCoroutineScope = CoroutineScope(Dispatchers.Main)

fun startTask(view: View) {
	myCoroutineScope.launch(Dispatchers.Main) {
    	performSlowTask()
    }
}

private suspend fun performSlowTask() {
	Log.i("TAG", "performSlowTask before)
    delay(7_777)
    Log.i("TAG", "performSlowTask after")
}

의 형태로 사용한다.

2. async

launch와 마찬가지로 현재 스레드를 중단시키지 않고 실행시킨다는 공통점이 있다. 하지만 결과를 호출한쪽에 반환한다는 점이 차이이다. 또한 async빌더는 오로지 suspend함수 내부에서만 사용이 가능하다. =suspend함수 내부에서만 사용 가능하단 뜻은 다른 빌더로 또다시 감싸줘야만 한다는걸 의미한다.

async쓰임새
일반 함수 내부 → launch빌더 내부 → suspend함수 호출 → async빌더

private val myCoroutineScope = CoroutineScope(Dispatchers.Main)

fun startTask(view: View) {
	myCoroutineScope.launch(Dispatchers.Main) {
    	binding.myTextView.text = performSlowTaskAsync().await()
    }
}

private suspend fun performSlowTaskAsync(): Defferred<String> {
	myCoroutineScope.async(Dispatchers.Default) {
    	Log.i("TAG", "performSlowTask before")
        delay(7_777)
        Log.i("TAG", "performSlowTask before")
        return@async "Finished"
    }
}

3. withContext

async빌더와 상당히 유사하다. 단, withContext빌더는 Deferred형태의 객체를 반환하지 않고, T형태의 객체를 그대로 반환한다. 그리고 withContext도 async처럼 suspend함수 안에서 선언되어야만 하는데, 이때 부모 빌더와는 다른 coutine context를 사용하여 실행시킨다는 점이 또 다른 차이이다.

private val myCoroutineScope = CoroutineScope(Dispatchers.Main)

fun startTask(view: View) {
	myCoroutineScope.launch(Dispatchers.Main) {
    	binding.myTextView.text = performSlowTaskAsync().await()
    }
}
  
  private suspend fun performSlowTaskAsync(): String {
	withContext(Dispatchers.Default) {
    	Log.i("TAG", "performSlowTask before")
        delay(7_777)
        Log.i("TAG", "performSlowTask before")
        return@withContext "Finished"
    }
}

4.runblocking

실행시킨 코루틴 작업이 끝날때까지 호출한 곳의 메인스레드가 끝나길 대기한다. 안드로이드 관점에서 보자면, 메인스레드는 UI스레드를 의미한다.

그래서 UI스레드에선 해당 스코프 호출하려 한다면...?
다른 방법을 찾아보는게 좋은듯 합니다...

3. LauncherBuilder

안드로이드에서 대표적으로 쓰이는 LauncherBuilder는 크게 3가지가 있다.

안드로이드 대표 LauncherBuilder
1. coroutineScope
2. GlobalScope
3. ViewModelScope

1. coroutineScope

이는 병렬로 실행되는 코루틴이 모두 완료되어야만 하는 처리를할때 유용하다. 아래 사진과 같이 모든 코루틴 실행이 완료되어야만 하며, 하나라도 실패시 작업은 모두 취소된다. 아래 코드를 보자.

fun main {
	test()  
}
  
fun test() {
  runBlocking {
  	try {
  		failedConcurrentSum()
  	} catch (e: ArithmeticException) {
  		Log.i("코루틴테스트", "ArithmeticException!!")
  	}
  }
}
  
suspend fun failedConcurrentSum(): Int = coroutineScope {
	val one = async<Int> {
  		try {
  			delay(Long.MAX_VALUE)
  			42
  		} finally {
  			Log.i("코루틴테스트", "First child was cancelled")
  		}
  	}
  	val two = async<Int> {
  		Log.i("코루틴테스트", "예외 던져버리기 직전 ㅇㅇ")
  		throw ArithmeticException()
  	}
  	one.await() + two.await()
}

로그값을 보면 알 수 있다시피 두 번째 코루틴에서 에러를 던졌다. 그러더니 첫 번째 코루틴도 끝나버린걸 알 수 있다!

2. viewModelScope

viewModelScope의 경우는 ViewModel의 라이프사이클에 맞춰 Scope을 적절히 cancel시켜줄 수 있는 스코프이다. 예를들어, 사용자가 api를 호출하자마자 액티비티를 onDestroy했다고 가정해보자. 그럼 이때 ViewModel도 onCrear되는데, 이때 ViewModelScope도 이에 맞추어 비동기코드를 종료시키게 된다.

ViewModelScope의 신기방구한 점!
ViewModelScope는 ViewModel에 시행되고 비즈니스 로직 처리를 위한 용도라는것 쯤은 이 글을 읽으시는 분들은 다 아실거라 생각한다. 그럼 이녀석은 어느 Thread에서 돌아갈까~? 당연 IO나 백그라운드지 않을까...!? 근데 NO! 이녀석은 Main스레드에서 돌아간다는 사실! 이 부분에 대한 포스팅은 다음에!

3. GlobalScope

이름에서도 알 수 있다시피, 프로그램 전체에서 싱글톤으로 진행시킬 수 있는 스코프입니다.

3. CPS

CPS는 Continuation Passing Style의 약자로, 연속 전달 스타일이란 뜻이다. 다음과 같은 형태 변화를 통해 이해가 가능하다.

우선 코드 예시로 아래와 같이 api를 호출하는 일반 함수가 있다고 가정하자. 그리고 그 함수 내부엔 suspend함수가 2개 있다.

fun postItem(item: Item) {
    val token = requestToken() // suspend fun
    val post = createPost(token, item) // suspend fun
    processPost(post)
}

이러한 형태의 코드가 CPS과정을 거쳐 어떤 형태로 어떻게 변환되는지 코드로 탐구해볼까 한다. 우선 각각의 suspend펑션에 중단지점을 만들기 위해 각각의 함수 마지막 파라미터 부분에 람다 함수가 뚫린다.

fun postItem(item: Item) {
    equestToken() { token ->
      createPost(token, item) { post ->
          processPost(post)
      }
    }
}

그리고 위와 같은 형태로 컴파일 되기 위해선, 아래와 같은 방식으로 컴파일이 진행됩니다.

fun postItem(item: Item, cont: Continuation) {
    
    // 2. CPA스타일로 변환을 시작한다.
    val sm = cont as? ThisSM ?: object : ThisSM {
    
        // 5. 해당 메소드를 호출함으로써 재귀호출이 진행된다
        fun resume() {
            postItem(null, this)
        }
    } 

    // 1. 라벨링 작업을 한다(비동기 코드의 '중단지점'과 '재개지점'을 알기 위하여)
    switch (sm.label) { 
        case 0:
            sm.item = item
            sm.label = 1 // 3. 라벨을 1씩 증가시켜 case1 호출 준비를 한다
            requestToken(sm) // 4. 재귀호출을 시작한다
        case 1:
            sm.item = item
            createPost(token, item, sm)// 6. 0번부터 끝번호까지 호출을 반복한다.
    }
}

즉, CPS를 이해함으로써 서두에 밝혀두었던 사진이 더 잘 이해가 갈거라 생각한다. 이는 Coroutine의 비동기 작업 처리가 어떻게 직렬적으로 잘 이루어질 수 있는지 알 수 있다.

4. 잡

잡은 launch나 async등의 빌더를 호출함으로써 return받은 인스턴스를 의미한다. 이때 luanch는 Job을 리턴받으며 async는 Deferred를 리턴받습니다.

알아가면 좋을 꿀팁 상식

Deferred객체는 Job을 상속받는 객체므로, async도 결국 Job을 리턴받는다

그리고 이러한 Job은 코루틴의 상태를 가지고 있다. 아래 표처럼 6가지 상태를 가지고 있다.

생명주기처럼 외워두면 좋겠쥬~?

그리고 이러한 상태에 대한 플로우차트는 아래와 같다. 생명주기라 생각하고 단간하게 외워두자.

그리고 이러한 부분을 코드에서 다음과 같이 확인할 수 있다.

마치며
오늘 코루틴에 대해 많은걸 알아봤다. 하지만 개념적으로 이해했다고 끝난것이 아니다. 이제 이 개념을 실전에 어떻게 적용할 것인가에 대한 고민을 또 한번 더 해야한다는 점!

profile
불가능보다 가능함에 몰입할 수 있는 개발자가 되기 위해 노력합니다.

0개의 댓글