Coroutine

SMJ·2024년 10월 28일

Kotlin

목록 보기
1/2
post-thumbnail

Coroutine이란?

작업을 중단 했다가 다시 실행할 수 있는 컴포넌트로, 비동기 작업을 쉽게 관리할 수 있다.

Coroutine을 사용해야 하는 이유

  • 메인 스레드 보호

    안드로이드에서는 하나의 앱에서 뷰를 다루는 스레드가 단 하나만 존재하기 때문에 Main 스레드는 블로킹되지 않는 것이 중요하다.

    따라서, 안드로이드 UI 작업은 MainThread에서만 실행되야 한다. 메인 스레드가 블록되면 ANR (Application Not Responding) 오류가 발생할 수 있다.

    안드로이드 앱은 일반적으로 초당 60 프레임의 랜더링을 목표로 하고 1 프레임을 1/60초 (약 16ms) 마다 그려야 한다.

  • 비동기 작업 간소화 및 순차 실행

코루틴 중단

  • 컴퓨터 게임의 체크포인트처럼, 특정 지점에서 작업을 멈추고 나중에 다시 시작할 수 있음

Routine, Subroutine, Coroutine

  • Routine -> 특정한 작업을 처리하기 위한 함수
  • Subroutine
    • 루틴의 하위에서 실행되는 또 다른 루틴 (함수 내부에서 호출되는 함수)
    • 서브 루틴은 하나의 진입점을 가짐
    • 한번 호출되면 작업 완료할 때까지 멈추지 않고 쭉 실행되므로 스레드가 다른 작업을 할 수 없음
  • Coroutine
    • 일시 중단/재가가 가능한 작업 단위
      • (특정 지점에서 작업을 멈추고, 필요할 때 재개)
    • 중단된 곳에서 다시 시작 가능 -> 여러개의 진입점 가짐
    • 작업하지 않을 때는 스레드 사용 권한을 양보하며 서로 협력적으로 실행됨

CPS (ContinuationPassingStyle)

  • Continuation : 각 중단 시점마다 코루틴의 현재 상태 (실행 지점, 로컬 변수 등)와 다음에 무슨 작업을 해야 할지 기억하는, 확장된 콜백 역할 하는 객체
    • 다음에 수행할 중단/재개 시점을 Context에 저장하여 관리
    • 비동기 코드를 동기 코드처럼 보이게하는 코루틴의 핵심 메커니즘
  • Suspend 함수 내부에는 Continuation이 숨겨져 있어 재개가 가능
  • Continuation 객체의 타입을 지정할 수 있으며 resume을 통해 반환 되는 값은 반드시 같은 타입이다.

CoroutineContext

  • 코루틴 작업을 어떤 쓰레드에서 실행할 것인지에 대한 동작을 정의하고 제어하는 요소
  • Job → 코루틴의 고유한 식별 및 코루틴 제어
    • cancel → 스코프 안에 여러 코루틴 존재하면 하위 코루틴 모두 멈춤
    • delay, yield → 코루틴 실행 양보
    • join → 코루틴 순차적으로 실행할 수 있음
  • Dispatchers
    • 코루틴이 어떤 스레드에서 실행할 것인지에대한 동작 지정
    • Dispatchers.Main
      • UI 작업, 작고 가벼운 작업 실행 (UI 구성, suspend 함수, LiveData 수정 사항 가져오는 함수 등)
    • Dispatchers.IO
      • 네트워크, 디스크 IO 실행에 최적화
        • Retrofit, File, Room 데이터 읽고 쓸 때 사용
    • Dispatchers.Default
      • CPU 사용량 많은 무거운 작업 처리에 최적화 되어있음
        • 데이터 가공, 복잡한 연산, JSON 파싱
    • Dispatchers.Unconfined
      • GlobalScope와 함께 사용, 사용 X 권고3

CoroutineBuilder

  • CoroutineScopeCoroutineContext를 통해 코루틴을 실행시켜주는 함수
    • CoroutineScope는 코루틴을 실행할 수 있는 환경을 제공
    • CoroutineContext는 코루틴이 실행되는 컨텍스트(예: 디스패처, Job 등)를 정의
    • CoroutineContext를 통해 CoroutineScope를 만들고, 이를 사용해 코루틴을 실행
  • launch, async라는 함수는 coroutine을 생성하는 coroutine builder
    • coroutine scope 내에서만 호출될 수 있다
    • launch → Job 객체이며, 결과값을 반환 X
      • 실행 후 결과값이 필요 없는 모든 작업은 launch를 사용하여 실행할 수 있음
      • 실행 후 결과가 필요 없는 작업에 사용 → ex)네트워크 호출 후 UI를 업데이트 작업
    • async → Deffered 객체이며, 결과값을 반환
      • await() 함수를 사용하여, 코루틴 작업의 최종 결과값을 반환
      • 보통 병렬 작업이나 비동기 작업의 결과가 필요한 경우 사용
    • withContext
      • 특정 CoroutineContext에서 코루틴을 실행하고 결과를 반환
      • async와 동일하게 결과값을 반환, async와 차이점은 await()을 호출할 필요가 없다는 것
      • 인자로 넘어온 suspend 작업을 실행할 CoroutineContext를 교체하고 실행
      • 보통 I/O 작업이나 CPU 집약적인 작업을 다른 디스패처에서 실행하고 싶을 때 사용

suspend function

  • 코루틴 안에서만 실행할 수 있는 코루틴 전용 메소드
    • 함수가 호출되면 현재 코루틴은 중단되며, suspend 함수가 완료되면 다시 실행(resume)됨
  • withContext
    • 코루틴 내에서 특정 CoroutineContext(Dispatchers.IO 등)로 코루틴의 실행 컨텍스트를 변경하고, 해당 컨텍스트에서 주어진 suspend 함수를 실행하는 함수
      1. CoroutineContext 변경 ****(Dispatchers.IO 등)
      2. suspend 함수 실행 (컨텍스트 내에서 제공된 suspend 함수를 실행)
      3. suspend 함수가 완료된 후 결과를 반환
        1. 이후 withContext 호출 전의 컨텍스트로 다시 돌아옴

CoroutineScope

LifecycleScope (lifecycleScope.launch)

  • Dispathers.Main(UI 스레드)에 바인딩 되 있음
  • 컴포넌트(ActivityFragment, Service 등)의 생명주기에 연동된 코루틴 스코프
  • 정지/종료 코드 없으면 onDestoryed 되기 전까지 계속 실행됨
    • 앱이 백그라운드 상태, onPause 일 때도 코루틴이 계속 실행됨
  • repeatOnLifeCycler (거의 주로 사용함)
    • 주어진 생명주기 상태에서 반복적으로 코루틴을 실행
    • 호출하는 coroutine을 suspend 시키고, lifecycle 이 target state 에서 벗어나면 재시작
    • Flow가 자동으로 Lifecycle 상태에 맞춰 실행되고 중단됨 (STARTED, RESUMED 등)
  • launchWhenStarted
    • LifecycleScope.launchWhenCreated, Started, Resumed 스코프가 있음
    • 해당 스코프는 when 이후 접미어에 해당하는 생명주기에 맞춰 실행이 되고, 생명주기의 상태가 충족되지 않으면 정지가 되는 함수

lifecycleScope.launch {
    // 1. API 호출 - 백그라운드 스레드에서 수행
    val apiResponse = withContext(Dispatchers.IO) {
        apiService.getSomeData() // 이 함수는 suspend 함수
    }

    // 2. UI 업데이트 - 메인 스레드에서 수행
    withContext(Dispatchers.Main) {
        // DTO에서 값을 꺼내서 XML의 뷰에 바인딩
        textView.text = apiResponse.someProperty
        imageView.setImageResource(apiResponse.imageResource)
    }
}

private fun observeViewModel() {
    lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            customerViewModel.getMemberProfile(memberId)
            customerViewModel.memberProfileResponse.collect() { res ->
                binding.managementPageMember = res.body()
                val membership = res.body()?.membership
                if (membership != null) { checkMembershipRadioButton(membership) }
            }
        }
    }
}

CoroutineScope

ViewModel 및 lifecycle owner(Activity 또는 Fragment) 이외에 Coroutine을사용시 CoroutineScope를 사용

예를 들어, 클래스의 수명주기가 명확하지 않거나 코루틴의 수명을 명시적으로 관리하고 싶을 때 CoroutineScope를 직접 정의하여 사용

ViewModelScope

ViewModel 에서 비동기 네트워크 통신시 사용되는 CoroutineScope

ViewModel과 생명주기 공유
뷰모델 제거되면(onCleared) 모든 코루틴 작업 자동 취소됨

Dispatchers.Main이 기본 CoroutineContext 로 설정되있음

private val _spotPositionResponse = MutableStateFlow(Response.success(SpotPositionResponse()))
    val spotPositionResponse: StateFlow<Response<SpotPositionResponse>> = _spotPositionResponse

fun getSpotList(regionId: Int) {
        viewModelScope.launch {
            try {
                mainApiRepository.getSpotList(regionId).collect {
                    _spotPositionResponse.value = it
                }
            } catch (e:Exception) {
                Log.e("ViewModel getSpotList Error", e.message.toString())
            }
        }
    }

RunBlocking

  • CoroutineScope를 제공하지만 block 기능이 있는 scope 빌더
  • 현재 스레드를 block 하고 runBlocking 내부의 코드가 실행될 때까지 기다림
  • 보통 테스트나 짧은 예제에서 사용됨, 메인 스레드나 UI 스레드에서는 사용하지 않는 것 좋음

GlobalScope

  • 앱이 실행되고 종료될 때까지 실행할 수 있는 scope

Retrofit과 Coroutine 간의 ViewModel에선 Dispacher.IO로 전환해야 할까?

  • 결론적으로 위의 행동은 아무런 영향을 주지 못한다.

이유는 Retrofit 라이브러리는 네트워크 요청을 실행하는 동안 내부적으로 자체적인 비동기 처리를 수행하기 때문이다.

Coroutine을 이용할 경우 내부적으로 enqueue를 진행해주고 있으며, enqueue는 retrofit에서 내부적으로 비동기 처리를 해주고 있으니 Dispatchers.IO를 사용하지 않아도 된다.

Room DAO 쿼리 메서드 suspend 사용하면 자동으로 Dispatchers.IO 수행한다.

0개의 댓글