2024년 8월 1일 작성

Couroutine - 상호 협력하는 Routine

Coroutine을 사용하여 비동기 작업을 더 간결하고 효율적으로 처리할 수 있습니다.

Coroutine : Co + Routine

  • Coroutine은 상호 협력하는 routine이라는 의미를 가지며, 동시성 programming(concurrency programming)을 지원하는 개념입니다.
    • Coroutine을 사용하여 비동기 작업 역시 더 간결하고 효율적인 방식으로 처리할 수 있습니다.
  • Coroutine은 일반적인 함수나 routine과 유사하지만, 실행을 중단하고 다른 작업을 수행할 수 있는 지점에서 중단된 상태를 기억할 수 있다는 점에서 다릅니다.
    • 일반 함수와 달리 호출 시 즉시 완료되지 않고, 필요한 시점에서 중단할 수 있으며, 다시 호출되었을 때 중단된 지점부터 실행을 재개합니다.
  • Coroutine은 비슷한 기능을 제공하는 Rx(ReactiveX)보다 가벼우며, 더 간결하고 배우기 쉽습니다.
    • RxJava가 점점 Kotlin의 Coroutine으로 대체되고 있는 가장 큰 이유는 RxJava의 학습 곡선(learning curve)이 가파르기 때문입니다.
  • Coroutine을 사용하면 비동기 작업을 직관적이고 가독성 높게 작성할 수 있어, 복잡한 비동기 흐름을 쉽게 관리할 수 있습니다.
    • Coroutine code도 동기 code(전통적인 routine)처럼 작업을 순차적인 순서로 작성할 수 있기 때문에 일반적인 비동기 code보다 가독성이 좋습니다.
  • UI를 개발할 때 비동기 처리는 필수적이며, 이때 Coroutine을 적용하면 비동기 logic을 쉽게 구현할 수 있습니다.
    • 예를 들어, Android에서는 UI rendering 작업과 data fetching 작업을 비동기적으로 동시에 수행하는 경우가 많습니다.

Coroutine의 “Co-“ : “함께”, “공동으로”, “동시에”

  • Coroutine은 서로 협력적(cooperative)으로 실행됩니다.
    • Coroutine은 자신이 실행되는 동안 언제든지 다른 Coroutine에게 제어권을 넘길 수 있습니다.
    • 이는 Coroutine이 명시적으로 양보(yield)하는 방식으로 이루어집니다.
    • 즉, Coroutine은 자신의 실행 흐름을 중단하고 다른 Coroutine이 실행될 수 있도록 협력합니다.
  • Coroutine은 동시성(concurrency)을 지원합니다.
    • Coroutine은 여러 작업이 병행하여 실행되는 것처럼 보이지만, 실제로는 하나의 thread에서 교대로 실행됩니다.
    • 전통적인 multithreading과 달리 thread 간의 context switching overhead가 없고, 더 가벼운 resource를 사용합니다.

Coroutine의 “Routine” : “특정한 일을 실행하기 위한 일련의 명령”

  • routine은 program의 일부분으로, 특정 작업을 수행하는 code block을 의미합니다.
    • routine은 일반적으로 함수나 method를 의미하며, 한 번 호출되면 끝날 때까지 계속 실행됩니다.
    • routine은 시작부터 끝까지 일련의 순차적인 작업을 수행하는 데 집중합니다.
  • routine은 일반적으로 동기적으로 실행되지만, Coroutine은 비동기적으로 실행될 수 있습니다.
    • 일반적인 routine과 달리, Coroutine은 중단과 재개가 가능하여 비동기 작업을 처리하는 동안에도 다른 작업을 수행할 수 있습니다.
      • Coroutine은 중단 지점에서 다른 Coroutine에게 제어권을 넘길 수 있습니다.
  • Coroutine은 routine의 일종(비동기적으로 실행될 수 있는 확장된 형태의 routine)으로, 비동기 작업을 순차적으로 수행할 수 있게 해줍니다.
    • routine의 논리적 단계를 유지하면서도, 비동기 작업을 효율적으로 처리할 수 있게 합니다.
    • 일련의 명령을 협력적이고 동시성 있게 실행할 수 있는 방식으로 확장합니다.

Coroutine의 특징 및 장점

  • Coroutine은 비선점적 multitasking과 동시성 programming을 지원하는 program 구조로, 가독성 좋은 비동기 처리 logic을 구현할 수 있다는 장점을 가지고 있습니다.
    • Coroutine을 활용하여 복잡한 비동기 작업을 효율적으로 처리하고, system 자원을 절약하며, 개발 생산성을 크게 향상시킬 수 있습니다.

Routine 간 협력을 통한 비선점적 Multitasking

  • Coroutine은 비선점적 multitasking을 통해 여러 작업을 동시에 처리할 수 있도록 합니다.
    • 이는 실행 중인 Coroutine이 스스로 제어권을 넘길 때까지 실행을 계속하며, 다른 Coroutine이 강제로 실행을 중단시키지 않는다는 의미입니다.
    • 비선점적 multitasking : 각 작업이 자신이 실행될 시점을 스스로 제어하는 방식.
  • 협력적 multitasking은 예측 가능한 실행 흐름을 제공하며, 복잡한 상태 관리 없이도 서로 간섭 없이 작업을 수행할 수 있게 합니다.

  • Coroutine은 자신이 할 일을 다 하면 명시적으로 제어권을 다른 Coroutine에 넘깁니다.
    • 이를 통해 system 자원의 낭비를 최소화하고 효율적인 실행을 보장합니다.
  • routine 간 협력을 통해 자연스럽게 multitasking이 이루어지므로, 서로 간의 의존성이 높은 작업들을 효과적으로 관리할 수 있습니다.

Context Switching 없는 경량 Thread

  • Coroutine은 경량 thread(light-weight thread)로, 일반적인 thread와 달리 context switching(문맥 교환)이 필요하지 않습니다.
    • 이는 Coroutine이 실행을 중단하고 다시 시작할 때, CPU 상태를 저장하고 복원하는 작업이 필요하지 않다는 것을 의미합니다.
  • context switching이 필요 없기 때문에, memory와 system 자원을 훨씬 적게 사용합니다.
    • thread 간 전환 시 발생하는 context switching은 상당한 overhead를 유발합니다.
  • 이러한 특성 덕분에 Coroutine은 속도가 빠르고 memory 사용이 적으며, 수천 개의 Coroutine을 동시에 실행해도 system 자원에 큰 부담을 주지 않습니다.

동시성 Programming 지원

  • Coroutine은 동시성 programming을 자연스럽게 지원합니다.
    • 여러 Coroutine을 병렬로 실행하여 동시에 여러 작업을 처리할 수 있습니다.
    • 동시성 programming : 여러 작업이 동시에 진행되는 것처럼 보이게 하는 기술.
  • Coroutine을 사용하면 I/O Bound 작업이나 network 요청 등의 비동기 작업을 효율적으로 처리할 수 있습니다.

가독성 좋은 비동기 처리 Logic

  • Coroutine의 가장 큰 장점 중 하나는 가독성 좋은 비동기 처리 logic을 작성할 수 있다는 점입니다.
    • 비동기 작업을 동기 code처럼 순차적으로 작성할 수 있어, code가 직관적이고 이해하기 쉬워집니다.
  • callback 지옥(callback hell)이나 복잡한 상태 관리 없이도 비동기 작업을 쉽게 구현할 수 있게 해줍니다.
    • 전통적인 callback 방식이나 Promise 기반의 비동기 처리는 code가 복잡해지고 가독성이 떨어지는 문제가 있습니다.
    • Coroutine을 사용하면 비동기 logic을 마치 순차적으로 실행되는 code처럼 작성할 수 있어, code의 흐름을 쉽게 이해할 수 있습니다.
  • 가독성이 좋아지면 debugging과 유지 보수가 용이해지고, 개발자의 생산성 역시 높아집니다.

일반 함수 호출과 Coroutine 비교

  • 일반 함수 호출 : main routine이 subroutine을 호출하고, subroutine이 종료되면 main routine으로 돌아옵니다.

  • Coroutine : main routine과 subroutine이 대등한 관계로, 특정 시점에 상대방의 code를 실행하며 협력하는 구조를 가집니다.

    • 특정 지점에서 실행을 일시 중지하고 나중에 재개할 수 있어 비동기/동시성 programming이 가능합니다.

함수 호출 (Main routine과 Subroutine)

def add(a, b):
    c = a + b
    print(c)
    print("add function")

def calc():
    add(1, 2) 
    print("calc function")
    
calc()
sequenceDiagram

participant m as main routine (calc)
participant s as subroutine (add)

activate m
note left of m : main routine 실행
m ->> s : add 함수 호출 (subroutine 시작)
deactivate m

activate s
note right of s : subroutine 실행
s ->> m : add 함수 후 값 반환 (subroutine 종료)
deactivate s

activate m
note left of m : main routine 실행
deactivate m
  • calc 함수가 add 함수를 호출하고, add 함수가 종료된 후 다시 calc 함수로 돌아옵니다.
  • add 함수가 종료되면 해당 함수의 변수와 계산식은 모두 사라집니다.

Coroutine

def sum_coroutine():
    total = 0
    while True:
        x = (yield total)
        total += x

def sum_func():
    co = sum_coroutine()
    next(co)
    result1 = co.send(10)
    print(f"result1 : {result1}")
    result2 = co.send(20)
    print(f"result2 : {result2}")

sum_func()
sequenceDiagram

participant f as sum_func
participant c as sum_coroutine

activate f
note left of f : 실행
f ->> c : Coroutine 최초 실행
deactivate f

activate c
note right of c : 실행
note left of f : 대기
c ->> f : 0 전달
deactivate c

activate f
note left of f : 실행
note right of c : 대기
f ->> c : Coroutine으로 10 보냄
deactivate f

activate c
note right of c : 실행
note left of f : 대기
c ->> f : 10 전달
deactivate c

activate f
note left of f : 실행
note right of c : 대기
f ->> c : Coroutine으로 20 보냄
deactivate f

activate c
note right of c : 실행
note left of f : 대기
c ->> f : 30 전달
deactivate c

activate f
note left of f : 실행
deactivate f
  • sum_coroutine은 한 번 실행된 후 종료되지 않고, 값을 주입받아 대기 상태로 유지됩니다.
  • main 함수와 sum_coroutine의 객체 co가 상호 협력적으로 값을 주고받으며 작동합니다.
    • main routine과 subroutine이 대등한 관계로, 특정 시점에 상대방의 code를 실행하며 협력하는 구조를 가집니다.

Rx vs Coroutine

  • Rx(Reactive Extensions)와 Coroutine은 모두 비동기 programming을 쉽게 하기 위한 도구이지만, 그 접근 방식과 철학이 다릅니다.
    • 복잡한 event stream을 처리해야 하는 경우 Rx가 더 적합하고, 순차적이고 간단한 비동기 작업을 효율적으로 관리해야 하는 경우 Coroutine이 더 적합합니다.
  • Coroutine은 비동기 작업을 순차적인 code처럼 작성할 수 있어 code 가독성이 높아집니다.
  Rx Coroutine
설명 Reactive Programming의 한 형태로, data stream과 그 stream을 처리하는 연산자에 중점을 둠.
event 기반 비동기 programming model을 제공하며, data를 비동기적으로 생성하고 소비함.
비동기 code를 작성하기 쉽게 해주는 구조체로, 함수의 실행을 일시 중단하고 나중에 다시 시작할 수 있는 기능을 제공함.
비동기 함수 호출을 동기 code처럼 작성할 수 있게 함.
장점 강력한 연산자 세트.
복잡한 data 흐름을 쉽게 표현.
다양한 언어 지원.
직관적인 code 작성.
기존 동기 code와 유사하게 작성 가능.
성능 효율성 높음.
단점 학습 곡선이 가파름.
모든 상황에 적합하지 않음.
code 가독성이 떨어질 수 있음.
복잡한 data stream 처리가 어려움.
Rx처럼 강력한 연산자 set가 부족함.
사용 사례 event stream 처리(UI event, network 응답, database 변경 등).
복잡한 비동기 data 흐름 관리.
data를 filtering/변환/결합하는 복잡한 연산 수행.
간단한 비동기 작업(network 호출, file I/O, 지연 작업 등).
순차적인 비동기 작업 실행.
동시성(concurrency) 관리를 통한 성능 최적화.
요약 복잡한 data stream과 event 기반 비동기 작업에 적합하며, 연산자를 사용한 data 변환과 결합 기능이 강력함. 간단한 비동기 작업을 동기 code처럼 작성할 수 있게 해주며, 순차적인 비동기 작업과 성능 최적화에 유리함.

Rx (Reactive Extensions) : Data Stream과 비동기 Event Sequence를 다루기 위한 Library

val observable = Observable.just(1, 2, 3, 4, 5)
observable
    .map { it * 2 }
    .filter { it > 5 }
    .subscribe(
        { println(it) },
        { error -> println("Error : $error") },
        { println("Completed") }
    )
  • Observable : 시간에 따라 발생하는 data stream을 나타냅니다.
    • Observable은 data를 생성하고 구독자에게 data를 push합니다.
  • Observer : Observable을 구독(subscribe)하여 data stream을 소비하는 객체입니다.
    • Observer는 Observable로부터 data event, 오류 event, 완료 event를 수신합니다.
  • Subscription : Observer가 Observable을 구독하면 반환되는 객체입니다.
    • 구독을 취소할 수 있는 mechanism을 제공합니다.
  • Operator : data stream을 변환, filtering, 결합하는 함수들입니다.
    • 예를 들어, map, filter, merge, flatMap 등의 연산자가 있습니다.
  • Scheduler : Rx 연산이 실행되는 thread를 제어하는 데 사용됩니다.
    • 주로 IO 작업, 계산 작업, UI update 등을 적절한 thread에서 수행하도록 합니다.

Coroutine : 비동기 작업을 순차적인 Code처럼 작성할 수 있게 해주는 개념

fun main() = runBlocking {
    val deferred = async {
        delay(1000L)
        "Hello"
    }
    println(deferred.await())
}
  • Coroutine : 비동기 작업을 수행하는 경량 thread입니다.
    • blocking 없이 일시 중단하고 재개할 수 있습니다.
  • Coroutine Scope : Coroutine이 실행되는 범위입니다.
    • Coroutine Scope는 모든 자식 Coroutine의 생명 주기를 관리합니다.
    • 대표적으로 GlobalScope, CoroutineScope 등이 있습니다.
  • Suspend Function : 일시 중단할 수 있는 함수입니다.
    • Kotlin에서는 suspend keyword를 사용하여 정의합니다.
    • 다른 Coroutine을 blocking하지 않고 비동기 작업을 수행할 수 있습니다.
  • Job : Coroutine의 생명 주기를 제어하는 객체입니다.
    • Job을 사용하여 Coroutine을 취소하거나 완료 상태를 확인할 수 있습니다.
  • Dispatcher : Coroutine이 실행되는 thread를 제어합니다.
    • 주로 Dispatchers.Main, Dispatchers.IO, Dispatchers.Default 등이 사용됩니다.
  • Deferred : 비동기 작업의 결과를 나타내는 객체입니다.
    • await를 사용하여 결과를 기다릴 수 있습니다.

Reference


목차