Kotlin Coroutines

[Kotlin Coroutines] 코루틴 스코프 만들기

easyhz 2024. 5. 31. 17:41

안드로이드에서 스코프 만들기

대부분의 안드로이드 애플리케이션에는 MVC 모델을 기반으로 한 MVVM이나 MVP 아키텍처가 사용되고 있다. 이러한 아키텍처에서는 사용자에게 보여 주는 부분을 `ViewModels` 나 `Presenters`와 같은 객체로 추출한다. (일반적으로 코루틴이 가장 먼저 시작되는 객체)

`UseCase` 나 `Repository`와 같은 다른 계층에서는 보통 중단 함수를 사용한다.

 

다음 코드는 `BaseViewModel`을 아래의 조건을 만족하여 만든 것이다.

- 모든 뷰모델에서 쓰일 스코프를 단 한 번으로 정의

- 메인 디스패처로 컨텍스트 정의

- 스코프를 취소 가능하게 만들되, 스코프가 가지고 있는 자식 코루틴만 취소

- 스코프에서 시작된 각각의 코루틴이 독립적으로 작동

- 잡히지 않는 예외 처리

 

abstract class BaseViewModel : ViewModel() {
    private val _failure: MutableLiveData<Throwable> = 
        MutableLiveData()
    val failure: LiveData<Throwable>
        get() = _failure
    
    private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> 
        _failure.value = throwable
    }
    
    // Dispatcher.Main으로 컨텍스트 정의 + 스코프에서 시작된 각각의 코루틴이 독립적으로 작동 + 잡히지 않는 예외 처리
    private val context = Dispatchers.Main + SupervisorJob() + exceptionHandler
    
    // 모든 뷰모델에서 쓰일 스코프를 단 한 번으로 정의
    protected val scope = CoroutineScope(context)

    override fun onCleared() {
        context.cancelChildren() // 스코프를 취소 가능하게 만들되, 스코프가 가지고 있는 자식 코루틴만 취소
    }
}

 

 

viewModelScope와 lifecycleScope

위에 처럼 스코프를 따로 정의할 수 있지만 `viewModelScope` 또는 `lifecycleScope` 를 사용할 수 있다. (대부분 이를 사용할 것이다.)

구현부를 보면,

1) `Dispatchers.Main`과 `SupervisorJob`을 사용한다는 점

2) 뷰모델이나 라이프 사이클이 종료되었을 때 잡을 취소시킨다는 점

에서 위에서 만들었던 `BaseViewModel`과 거의 동일하다고 볼 수 있다.

viewModelScope의 구현부

 

스코프에서 `CoroutineExceptionHandler`와 같은 특정 컨텍스트가 필요 없다면 `viewModelScope` 와 `lifecycleScope`를 사용하는 것이 편리하다.

 

 

백엔드에서 코루틴 만들기

스프링부트는 컨트롤러 함수가 suspend로 선언되는 것을 허용한다. 

그러나 따로 스코프를 만들어야 한다면 다음과 같은 것들이 필요하다.

 

- 스레드 풀(또는`Dispatchers.Default`)을 가진 커스텀 디스패처 

- 각각의 코루틴을 독립적으로 만들어 주는 `SupervisorJob`

- 적절한 에러 로그를 남기고 처리하는 CoroutineExceptionHandler

 

생성자를 통해 커스텀하게 만들어진 스코프를 클래스로 주입되는 방법이 가장 많이 사용된다.

스코프는 한 번만 정의되면 수많은 클래스에서 활용될 수 있으며, 테스트를 위해 다른 스코프로 쉽게 대체할 수도 있다.

@Configuration
class CoroutineScopeConfig {
	
    @Bean("coroutineDispatcher")
    fun coroutineDispatcehr(): CoroutineDispatcher = Dispatchers.IO.limitedParallelism(5)
    
    @Bean("coroutineExceptionHandler")
    fun coroutineExceptionHandler(): CoroutineExceptionHandler 
    	= CoroutineExceptionHandler { _, e -> 
        	logger.error(e)
        }
        
    @Bean
    fun coroutineScope(
    	coroutineDispatcher: CoroutineDispatcher,
        coroutineExceptionHandler: CoroutineExceptionHandler,
    ) = CoroutineScope(
    	SuperVisorJob() + coroutineDispatcher + coroutineExceptionHandler
    )
}

 

추가적인 호출을 위한 스코프 만들기

추가적인 연산이 필요할 경우 메서드 내에서 생성하여 함수나 생성자의 인자를 통해 주입하는 방식을 사용한다.
스코프를 호출을 중단하기 위한 목적으로만 사용하려는 경우 `SupervisorScope`를 사용하는 것만으로 충분하다.

val analyticsScope = CoroutinesCope(SupervisorJob())

 

 

마무리

현업에서 코루틴을 사용할 때 스코프를 만드는 건 중요하다. 

지금까지 배운 것으로 작고 간단한 애플리케이션을 만드는 데는 충분하지만, 좀 더 거대한 프로젝트를 진행한다면 적절한 동기화테스트라는 주제에 대해 알아야 한다.

 

참고자료

마르친 모스카와(2023), 코틀린 코루틴 Kotlin Coroutines : Deep Dive - 안드로이드 및 백엔드 개발자를 위한 비동기 프로그래밍, 인사이트