코틀린 스터디

코루틴 스코프 만들기

막이86 2023. 11. 24. 17:58
728x90

코틀린 코루틴을 요약한 내용입니다.

CoroutineScope 팩토리 함수

  • CoroutineScope는 coroutineContext를 유일한 프로퍼티로 가지고 있는 인터페이스
  • interface CoroutineScope { val coroutineContext: CoroutineContext }
  • CoroutineScope 인터페이스를 구현한 클래스를 만들고 내부에서 코루틴 빌더를 직접 호출 가능
    • 잘 사용되지 않음
    • cancel, ensureActive 같은 다른 CoroutineScope의 메서드를 직접 호출하면 문제 발생 할 수 있음
    class SomeClass: CoroutineScope {
    	override val coroutineContext: CoroutineContext = Job()
    
    	fun onStart() {
    		launch { 
    			// ....
    		}
    	}
    }
    
  • 코루틴 스코프 인스턴스를 프로퍼티로 가지고 있다가 사용하는 방법을 선호
  • class SomeClass { val scope: CoroutineScope = ... fun onStart() { scope.launch { } } }
  • 코루틴 스코프 객체를 만드는 가장 쉬운 방법은 CoroutineScope 팩토리 함수를 사용
  • public fun CoroutineScope( context: CoroutineContext ): CoroutineScope = ContextScope( if (context[job] != null) { context } else { context = Job() } ) internal class ContextScope( context: Cor )

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

  • 사용자에게 보여주는 부분을 ViewModels나 Presenters와 같은 객체로 추출
    • 일반적으로 코루틴이 가장 먼저 시작되는 객체
  • 유스 케이스나 저장소와 같은 다른 계층에서는 보통 중단 함수를 사용
  • 안드로이드의 어떤 부분에서 코루틴을 시작하든지 간에 코루틴을 만드는 방법은 비슷
  • BaseViewModel에서 스코프를 만들면, 모든 뷰 모델에서 쓰일 소크포를 단한 번으로 정의
  • abstract class BaseViewModel: ViewModel() { protected val scope = CoroutineScope(TODO()) } class MainViewModel( private val userRepo: UserRepository, private val newsRepo: NewsRepository ): BaseViewModel { fun onCreate() { scope.launch { val user = userRepo.getUser() view.showUserData(user) } scope.launch { val news = newsRepo.getNews().sortedByDescending { it.data } view.showNews(news) } } }
  • 안드로이드에서는 메인 스레드가 많은 수의 함수를 호출
    • 기본 디스패처를 Dispatchers.Main으로 정하는 것이 좋음
    abstract class BaseViewModel: ViewModel() {
    	protected val scope = CoroutineScope(Dispatchers.Main)
    }
    
  • 스코프 취소 가능하게 만들기
    • 스코프를 취소 가능하게 하려면 Job이 필요
      • CoroutinScope 함수가 잡을 추가하므로 따로 추가하지 않아도 상관 없음
      abstract class BaseViewModel: ViewModel() {
      	protected val scope = CoroutineScope(Dispatchers.Main + Job())
      
      	override fun onCleard() {
      		scope.cancel()
      	}
      }
      
    • 자식 코루틴만 취소하는 것이 더 좋은 방법
    • abstract class BaseViewModel: ViewModel() { protected val scope = CoroutineScope(Dispatchers.Main + Job()) override fun onCleard() { scope.coroutinContext.cancelChildren() } }
  • 해당 스코프에서 코루틴이 독립적으로 작동해야 할 필요도 있음
    • Job을 사용하고 에러가 발생하여 자식 코루틴 하나가 취소된 경우 부모와 다른 자식 코루틴 모두가 함께 취소가 됨
    • Job대신 SupervisorJob을 사용
    • abstract class BaseViewModel: ViewModel() { protected val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) override fun onCleard() { scope.coroutinContext.cancelChildren() } }
  • 잡히지 않는 예외를 처리하는 방법
    • BaseActivity에 예외 처리 핸들러를 한 번만 정의
      • 뷰 모델에서 전달하는 방법으로 사용
    • 잡히지 않는 예외가 있는 경우 CoroutinExceptionHandler를 사용
    abstract class BaseViewModel(
    	private val onError: (Throwable) -> Unit
    ): ViewModel() {
    	private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> 
    		onError(throwable)
    	}
    	private val context = Dispatchers.Main + SupervisorJob() + exceptionHandler
    	protected val scope = CoroutineScope(context)
    
    	override fun onCleard() {
    		context.cancelChildren()
    	}
    }
    

viewModelScope와 lifecycleScope

  • viewModelScope 또는 lifecycleScope를 사용 가능
    • lifecycle-viewmodel-ktx 2.0.0 이상 버전에서
  • Dispatchers.Main과 SupervisorJob을 사용하고, 뷰 모델이나 라이프사이클이 종료되었을 떄 잡을 취소시킨다는 점에서 우리가 만들었던 스코프와 거의 동일
  • public val ViewModel.viewModelScope: CoroutineScope get() { val scope: CoroutineScope? = this.getTag(JOB_KEY) if (scope != null) { return scope } return setTagIfAbsent( JOB_KEY, CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) } internal class CloseableCoroutineScope( context: CoroutineContext ): Closeable, CoroutineScope { override val coroutineContext: CoroutineContext = context override fun close() { coroutineContext.cancel() } }
  • 스코프에서 특정 컨텍스트가 필요 없다면 viewModelScope와 lifecycleScope를 사용하는 것이 좋음
  • class ArticlesListViewModel( private val produceArticles: ProduceArticlesUseCase ): ViewModel() { private val _progressBarVisible = MutableStateFlow(false) val progressBarVisible: StateFlow<Boolean> = _progressBarVisible private val _articlesListState = MutableStateFlow<ArticlesListState> = _articlesListState fun onCreate() { viewModelScope.launch { _progressBarVisible.value = true val articles = produceArticles.produce() _articlesListState.value = ArticlesLoaded(articles) _progressBarVisible.value = false } } }

백엔드에서 코루틴 만들기

  • 프레임워크에서 중단 함수를 기본적으로 지원
    • 스프링 부트는 컨트롤러 함수가 suspend로 선언되는 것을 허용
  • 따로 스코프를 만들 필요는 거의 없음
  • 따로 만들어야 할 경우
    • 스레드 풀(또는 Dispatchers.Default)을 가진 커스텀 디스패처
    • 각각의 코루틴을 독립적으로 만들어 주는 SupervisorJob
    • 적절한 에러 코드에 응답하고, 데드 레터를 보내거나 발생한 문제에 대해 로그를 남기는 CoroutineExceptionHandler
    @Configuration
    public class CoroutineScopeConfiguration {
    
    }
    

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

  • 스코프를 중단하기 위한 목적으로 사용하는 경우 SupervisorScope를 사용하는 것으로 충분
  • val analyticsScope = CoroutineScope(SupervisorJob())
  • 모든 예외는 로그를 통해 볼수 있으므로 예외를 관제 시스템으로 보내고 싶다면 CoroutineExceptionHandler를 사용
  • private val exceptionHandler = CoroutineExceptionHandler = { _, throwable -> FirebaseCrashlytics.getInstance().recordException(throwable) } val analyticsScope = CoroutineScope( SuervisorJob() + exceptionHandler )
728x90

'코틀린 스터디' 카테고리의 다른 글

셀렉트  (0) 2024.01.04
채널  (0) 2023.12.04
코루틴 기반 동시성 프로그래밍  (0) 2023.11.24
코틀린 - 예외처리, Type System, 컬렉션  (0) 2023.11.10
코틀린 - 클래스, 인터페이스, 상속  (0) 2023.11.10