안드로이드 CoroutineContext 와 CoroutineScope 란? (2)

쓰리원·2022년 6월 6일
0

Coroutine 정리

목록 보기
3/4
post-thumbnail

아직 글의 완성이 덜 되었습니다.

1. CoroutineContext 란?

@SinceKotlin("1.3")
public interface CoroutineContext {
    /**
     * Returns the element with the given [key] from this context or `null`.
     */
    public operator fun <E : Element> get(key: Key<E>): E?

    /**
     * Accumulates entries of this context starting with [initial] value and applying [operation]
     * from left to right to current accumulator value and each element of this context.
     */
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R

    /**
     * Returns a context containing elements from this context and elements from  other [context].
     * The elements from this context with the same key as in the other one are dropped.
     */
    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
            context.fold(this) { acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    // make sure interceptor is always last in the context (and thus is fast to get when present)
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }

    /**
     * Returns a context containing elements from this context, but without an element with
     * the specified [key].
     */
    public fun minusKey(key: Key<*>): CoroutineContext

    /**
     * Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
     */
    public interface Key<E : Element>

    /**
     * An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
     */
    public interface Element : CoroutineContext {
        /**
         * A key of this coroutine context element.
         */
        public val key: Key<*>

        public override operator fun <E : Element> get(key: Key<E>): E? =
            @Suppress("UNCHECKED_CAST")
            if (this.key == key) this as E else null

        public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
            operation(initial, this)

        public override fun minusKey(key: Key<*>): CoroutineContext =
            if (this.key == key) EmptyCoroutineContext else this
    }
}
  1. get() : 연산자(operator) 함수로써 주어진 key 에 해당하는 컨텍스트 요소를 반환합니다.
  1. fold() : 초기값(initialValue)을 시작으로 제공된 병합 함수를 이용하여 대상 컨텍스트 요소들을 병합한 후 결과를 반환합니다. 예를들어 초기값을 0을 주고 특정 컨텍스트 요소들만 찾는 병합 함수(filter 역할)를 주면 찾은 개수를 반환할 수 있고, 초기값을 EmptyCoroutineContext 를 주고 특정 컨텍스트 요소들만 찾아 추가하는 함수를 주면 해당 요소들만드로 구성된 코루틴 컨텍스트를 만들 수 있습니다.
  1. plus() : 현재 컨텍스트와 파라미터로 주어진 다른 컨텍스트가 갖는 요소들을 모두 포함하는 컨텍스트를 반환합니다. 현재 컨텍스트 요소 중 파라미터로 주어진 요소에 이미 존재하는 요소(중복)는 버려집니다.
  1. minusKey() : 현재 컨텍스트에서 주어진 키를 갖는 요소들을 제외한 새로운 컨텍스트를 반환합니다.

위 이미지는 우리가 GlobalScope.launch{} 를 수행할 때 launch 함수의 첫번째 파라미터인 CoroutineContext 에 어떤 값을 넘기는지에 따라서 변화되어 가는 코루틴 컨텍스트의 상태를 보여줍니다.

각각의 요소를 + 연산자를 이용해 연결하고 있는데 이는 앞서 설명한 것처럼 CoroutineContext가 plus 연산자를 구현하고 있기 때문입니다. Element + Element + … 는 결국 하나로 병합 된 CoroutineContext (e.g. CombinedContext)를 만들어냅니다.

2. CoroutineScope 란?

CoroutineScope는 CoroutineContext 하나만 멤버 변수로 정의하고 있는 인터페이스 입니다.

1. CoroutineScope

2. GlobalScope

GlobalScope은 싱글톤으로 생성되기 때문에, 잘못 활용한다면 프로그램 전체에 악영향을 미칠 수 있습니다.

3. CoroutineScope with Android Components

1. ViewModelScope

AAC의 Lifecycle Component 라이브러리의 ViewModel 내부에서는 viewModelScope 라는 코루틴 스코프를 이용할 수 있습니다. 그래서 CoroutineScope의 객체를 직접 만들어서 사용하는 것은 안드로이드 개발에서 잘 하지 않습니다. viewModelScope는 onCleared()에 자동으로 작업을 취소시켜주는 역할을 합니다. 그래서 뷰모델의 생명주기에 코루틴 작업을 맞출 수 있습니다.

viewModelScope 내부 입니다. Scope의 context는 Dispatchers.Main 입니다.

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}

구글 공식문서의 예로는 위와 같이 사용이 가능합니다.

class MyViewModel: ViewModel() {

	fun MainThread() {
    	viewModelScope.launch {
        	liveData.value = getDataFromNetwork()
    	}
	}

	fun WorkerThread() {
    	viewModelScope.launch {
        	withContext(Dispatchers.Main) {
            	liveData.value = getDataFromNetwork()
        	}
    	}
	}

	suspend fun getDataIO() : Int = withContext(Dispatchers.IO) {
    	kotlinx.coroutines.delay(5000)
	}
}

setValue()는 메인 쓰레드에서 LiveData의 값을 변경합니다. 메인 쓰레드에서 바로 값을 변경해주기 때문에 setValue() 함수를 호출한 뒤 바로 밑에서 getValue() 함수로 값을 읽어오면 변경된 값을 가져올 수 있습니다.

setValue()는 메인 쓰레드에서 값을 dispatch 하기 때문에 백그라운드에서 setValue()를 호출한다면 에러가 납니다. 위의 viewModelScope는 자동으로 Dispatchers.Main에서 시작하기 때문에 LiveData의 setValue()를 사용할 수 있습니다. 그게 아닌 경우에는 withContext로 Context 스위칭을 해야합니다.

2. LifecycleScope

LifecycleScope는 각 Lifecycle 객체에서 정의됩니다. 이 범위에서 실행된 코루틴은 Lifecycle이 끝날 때 제거됩니다. lifecycle.coroutineScope 또는 lifecycleOwner.lifecycleScope 속성을 통해 Lifecycle의 CoroutineScope에 액세스할 수 있습니다.

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        viewLifecycleOwner.lifecycleScope.launch {
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) {
                PrecomputedTextCompat.create(longTextContent, params)
            }
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        }
    }
}

위 예는 공식문서에서 lifecycleOwner.lifecycleScope를 사용하여 미리 계산된 텍스트를 비동기적으로 만드는 방법을 보여줍니다.

3. liveData

LiveData를 사용할 때 값을 비동기적으로 계산해야 할 수 있습니다. 예를 들어 사용자의 환경설정을 검색하여 UI에 제공하려고 할 수 있습니다. 이러한 경우 liveData 빌더 함수를 사용해 suspend 함수를 호출하여 결과를 LiveData 객체로 제공할 수 있습니다.

아래 예에서 loadUser()는 다른 곳에서 선언된 정지 함수입니다. liveData 빌더 함수를 사용하여 loadUser()를 비동기적으로 호출한 후 emit()를 사용하여 결과를 내보냅니다.

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

4. reference

https://developer.android.com/topic/libraries/architecture/coroutines

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/

https://myungpyo.medium.com/reading-coroutine-official-guide-thoroughly-part-1-7ebb70a51910

https://silica.io/understanding-kotlin-coroutines/5/

https://thdev.tech/kotlin/2020/12/22/kotlin_effective_16/

https://medium.com/hongbeomi-dev/%EC%BD%94%ED%8B%80%EB%A6%B0%EC%9D%98-%EC%BD%94%EB%A3%A8%ED%8B%B4-4-coroutine-context-and-dispatchers-1eab8f175428
https://kotlinlang.org/docs/coroutines-basics.html

profile
가장 아름다운 정답은 서로의 협업안에 있다.

0개의 댓글