본문 바로가기
Kotlin/Coroutine

(Coroutine) 0. 개요 및 특징

by jaesungLeee 2021. 12. 20.

1. 루틴? 코루틴?

1.1. 루틴, 메인 루틴, 서브 루틴

루틴(Routine)은 어떤 작업을 정의한 명령어의 집합을 의미한다. 간단하게 우리가 작성하는 코드들의 집합이라고 볼 수 있다. 따라서 하나의 프로그램은 보통 크고 작은 여러 가지 루틴들을 조합하여 만들어진다고 볼 수 있다. 

루틴은 다시 메인 루틴(Main Routine)과 서브 루틴(Sub Routine)으로 나뉜다. 일반적으로 main 함수에 의해 수행되는 프로그램의 흐름을 메인 루틴이라고 한다. 반면, main 함수 안에서 실행되는 개별 함수들에 의해 수행되는 흐름을 서브 루틴이라고 한다. 메인 루틴과 서브 루틴의 차이는 아래의 코드를 통해 쉽게 확인할 수 있다.

 

fun main() {
    val addValue = plusOne(10)  // 메인 루틴
}

fun plusOne(initial: Int) : Int {  // 서브 루틴
    val ONE = 1
    var result = initial

    result += ONE

    return result
}

 

1.2. 서브 루틴 vs 코루틴

서브 루틴은 진입하는 지점과 빠져나오는 지점이 명확하게 존재한다. 메인 루틴에서 서브 루틴을 호출하여 함수를 시작하면, 서브 루틴의 맨 처음 부분에 진입하여 코드를 실행하고 return을 만나면 함수의 실행이 종료되어 결과값이 호출자에게 돌아간다. 즉, 서브 루틴은 단일 지점에서 시작되고 특정 지점에서 종료된다고 볼 수 있다.

코루틴(Coroutine)도 방식은 비슷하다. 서브 루틴은 진입하여 함수의 마지막까지 실행한 후 최종적으로 return 하는 반면, 코루틴은 왼쪽 그림처럼 Block안에 진입하여 코드를 실행하다가 중간에 빠져나갈 수 있다. 이러한 형태가 반복되는데 즉, 함수에 진입할 수 있는 진입점도 여러 개이고 빠져나갈 수 있는 탈출점도 여러 개다. 단일 지점에서 시작하고 특정 지점에서 종료되는 것은 서브루틴과 동일하지만 임의 지점에서 멈출 수 있고 해당 지점에서 재개될 수도 있다는 특징이 있다.


2. Dream Code : Google I/O'19

https://www.youtube.com/watch?v=BOHK_w09pVA&t=205s 

조금 오래된 영상이지만 2019년 Google I/O에서 코루틴이 필요한 이유에 대해 발표했었다. 아래에 코루틴이 등장하게 된 배경에 대해 간단하게 요약하였다.

 

2.1. Dream Code v1

val user = fetchUserData()
textView.text = user.name

 

위 코드는 우리가 이상적으로 작성하고 싶은 코드이다. 네트워크 작업을 통해 User의 데이터를 가져오고 텍스트뷰에 name을 set 하고 싶다. 위 코드를 Activity나 Fragment에 작성하게 되면 fetchUserData( )에서 NetworkOnMainThreadException 예외가 발생하게 된다. fetchUserData( ) Method는 네트워크 작업을 수행하기 때문이다. NetworkOnMainThreadException은 Main Thread에서 네트워킹 처리를 시도할 경우 발생하는 예외이다.

 

2.2. Dream Code v2

thread {
    val user = fetchUserData()
    textView.text = user.name
}

 

위 문제점을 해결하기 위해 우리는 별도의 Thread를 생성하여 작업을 처리하도록 생각할 수 있다. 하지만 이 코드에서는 Main Thread 이외에 별도의 Thread에서 UI를 제어하기 때문에 CalledFromWrongThreadException 예외가 발생하게 된다. 오직 Main Thread에서만 UI를 제어해야 한다.

 

2.3. OK Code?

아래 코드처럼 우리는 UI제어를 Main Thread에서만 하도록 코드를 작성할 수 있다. 

 

// Callback fun
fetchUserData { user ->
    textView.text = user.name
}

 

fetchUserData( ) Method를 Callback 형태로 작성하면 된다. 네트워크 작업을 통해 데이터를 가져오는 것은 별도의 Thread에서 진행하고 필요한 데이터를 가져왔을 때 Callback을 통해 UI 업데이트를 수행하도록 작성할 수 있다. 현재까지는 나름 이상적이 코드로 보인다. 하지만 이런 단순한 작업 외에 조금 복잡한 비동기 작업이 수행되어야 하는 경우 콜백이 콜백을 부르는, 일명 콜백 지옥의 위험이 존재한다. 또한 UI를 업데이트하는 작업에서 user 객체가 계속해서 바뀌거나 삭제, 재생성과 같은 변경사항이 발생하게 되면 OutOfMemoryError가 발생할 가능성이 존재한다. 보통 OutOfMemoryError는 이미지와 같은 멀티미디어 처리에서 발생하기 쉬운 에러인데 이 문제에서는 RxJava, LiveData와 같은 라이브러리를 사용하여 해결이 가능하다.

 

2.4. Working Code & Unmaintainable Code

val subscription = fetchUserData { user ->
    textView.text = user.name 
}

override fun onStop() {
    subscription.cancel()
}

 

위 코드와 같이 subscription을 만들어 작업을 수행하고 onStop시에 subscription을 취소하게 할 수 있다. 하지만, 위의 예시는 하나의 작업에 대한 subscription이지만 여러 작업이 존재하는 경우 아래와 같은 문제가 발생할 수 있다.

 

override fun onStop() {
    subscription.cancel()
    subscription1.cancel()
    subscription2.cancel()
    subscription3.cancel()
    ..
    ..
}

 

모든 작업에 대한 subscription에 대해 onStop에서 해제를 해야 하기 때문에 좋지 않은 코드이다. 실제로 Google I/O'19에서 발표자도 위와 같은 코드가 Google 코드에도 존재한다고 말했다. 어디에서나 이렇게 작성한 불편한 코드들이 존재한다는 것이다. 

 

위와 같은 코드상의 문제들을 조금 더 간편하고 한 번에 해결 가능하게 만들고자 고안해낸 것이 바로 코루틴이다.


3. 코루틴은 Callback을 대체하여 비동기 코드를 단순화한다!

Google I/O'19에서 발표한 내용들을 그림으로 간단하게 정리한다.

3.1. 문제 상황

위에서도 언급하였듯이 Main Thread에서 네트워크 작업을 처리하는 것은 UI가 Blocking 되기 때문에 하면 안되는 작업이다. 아래 그림처럼 Main Thread에서 fetchUser( )를 호출하면 UI를 그리는 onDraw( )가 Blocking 되는 위험이 있다.

 

3.2. 해결 방법 1 - Callback

아래 그림처럼 fetchUser( )를 Callback 형식으로 처리하면 충분히 쉽게 해결이 가능하긴 하다. fetchUser( )에서 네트워크 작업을 위한 별도의 Thread를 생성하여 수행하게 되면 Main Thread Blocking 없이 해결 가능하다.

 

3.2. 해결 방법 2 - Coroutine

코루틴을 사용하게 되면 Callback을 사용한 것과 스타일이 비슷하다. 하지만 fetch 되는 결과는 즉각 사용할 수 있으며 Callback을 사용할 필요도 없다. UI의 업데이트는 여전히 Main Thread에서 하고 있으며, 네트워크 요청 또한 별도의 Thread에서 처리된다.

 

위 그림에서 중점적으로 봐야 할 부분은 바로 함수 선언 앞에 붙어있는 suspend 키워드이다. suspend는 코루틴의 핵심 메커니즘이다. suspend된 코루틴은 일시 중단된 것이며 resume되면 중단된 위치에서 다시 시작한다. 1장에서 작성한 코루틴의 특징을 suspend 키워드로 잘 나타낸다. 

 

 


 

4. 그렇다면 Thread와 다른 점은?

코루틴과 Thread는 비동기 작업을 보장하기 위해 사용한다는 공통점이 있다. 하지만 둘 사이에는 근본적인 차이점이 존재한다.

4.1. Thread

Thread에서 작업(Task)의 실행 단위는 Thread이다. 또한, Thread는 각 작업에 해당하는 메모리 영역인 Stack을 할당한다. 즉, 다수의 작업을 위해 각각 Thread를 할당하는데, 이 Thread는 자체 Stack 메모리 영역을 가진다.

또한, 하나의 작업이 Scheduler로부터 CPU를 할당받았을 때 Scheduler가 다시 CPU 사용을 해제할 수 있다. 따라서 Thread는 선점형 멀티태스킹 (Preemptive Multitasking) 방식을 따른다. 예를 들어, A와 B라는 작업이 있을 때, A 작업 조금 B 작업 조금씩 수행하여 최종적으로 A 작업과 B 작업을 모두 완료하는 것이다.

출처 : https://aaronryu.github.io/2019/05/27/coroutine-and-thread/

위 그림은 CPU Single Core일 때를 가정한다. Thread A에서 Task 1을 수행하고 있는데 Task 2의 작업이 필요할 경우 이를 비동기적으로 호출한다. 이때, 진행 중이던 Task 1을 잠시 멈추고(Blocked) Thread B에 의해 Task 2가 수행되는데 CPU에서는 연산을 위해 Thread A에서 Thread B로 전환하는 Context Switching이 일어난다. Task 2가 완료되면 결과를 Task 1에 반환하고 다시 Task 1이 실행된다. 또한, 동시에 수행되어야 할 작업인 Task 3과 Task 4도 각각 Thread C와 D에 할당될 수 있다. 

 

4.2. Coroutine

코루틴에서 작업의 실행 단위는 Coroutine Object이다. 즉, 다수의 작업 각각에 Coroutine Object를 할당하는데 이 Object들은 Stack이 아닌 Heap 메모리 영역에 적재된다. 

출처 : https://aaronryu.github.io/2019/05/27/coroutine-and-thread/

위 그림에서 작업의 단위는 Coroutine Object이므로 Task 1이 수행 중에 비동기 작업인 Task 2가 발생하더라도 Task 1을 수행하던 Thread에서 Task 2를 수행할 수 있다. 즉, 하나의 Thread에서 다수의 Coroutine Object를 수행할 수 있는 것이다. 중요한 점은 Task 1과 Task 2의 전환에 있어 단일 Thread인 Thread A에서 Coroutine Object만 교체하여 이뤄지기 때문에 CPU의 Context Switching은 존재하지 않는다. 이렇게 한 Thread에서 다수의 Coroutine을 수행할 수 있고 Context Switching이 필요하지 않기 때문에 코루틴을 Light-Weight Thread라고 부른다.

 

4.3. Thread, Coroutine 차이점 간단 정리

1. 메모리 구조의 차이

Thread

: Stack을 할당받아 작업 전환 시 CPU의 Context Switching이 필요

 

Coroutine

: 프로세스의 Heap 메모리를 공유하여 사용

2. 수행 방식의 차이

Thread

: 선점형 멀티태스킹(Preemptive Multitasking) 방식, 멀티 코어를 사용해 동시에 여러 Thread를 처리할 수 있어 병행성을 제공한다.

 

Coroutine

: 비선점형 멀티태스킹(Non-preemptive Multitasking) 방식, CPU를 시분할하여 사용하기 때문에 복수의 작업을 동시에 처리할 수 없어 병행성은 제공하지 않음. 대신, 전환 속도가 빠르기 때문에 동시에 처리되는 것처럼 인식되어 동시성을 제공


5. 코루틴 기본 

코루틴은 하나의 Thread, Process에서 동시에 수백 개를 실행할 수 있다는 장점이 있다. 하지만, 기본적으로 코루틴의 작업이 끝났는지에 대한 추적은 하지 않기 때문에 메모리 누수가 발생할 수도 있다. Kotlin은 이 문제를 해결하기 위해 코루틴을 특정한 Scope에서 실행하도록 한다.

이후 글부터는 코루틴을 사용하기 위한 개념들에 대해 포스팅한다.


References

https://medium.com/@sunminlee89/코틀린-코루틴-coroutine-기초-1342ae6916ce

https://aaronryu.github.io/2019/05/27/coroutine-and-thread/

https://stackoverflow.com/questions/1934715/difference-between-a-coroutine-and-a-thread

https://cliearl.github.io/posts/android/coroutine-principle/

'Kotlin > Coroutine' 카테고리의 다른 글

(Coroutine) 5. Scope  (0) 2022.11.01
(Coroutine) 4. CoroutineContext, Dispatcher  (0) 2022.09.14
(Coroutine) 3. async  (0) 2022.09.14
(Coroutine) 2. Light weight Thread, Job  (0) 2022.09.12
(Coroutine) 1. Coroutine Basic  (0) 2022.09.12