재미없는 대학 숙제같은 제목이지만, 최근에 책을 하나 읽으면서 Kotlin의 코루틴(Coroutine)을 사용하면서 풀리지 않던 찝찝함을 풀어서 코루틴과 비동기 프로그래밍의 관계에 대해 포스팅 해보려한다. 이 글은 코루틴, 루틴, 서브루틴, 비동기 프로그래밍, 스레드같은 관련 단어가 각각 따로 뭔지는 알겠는데 이것들의 관계에 대해 명확히 설명하지 못하는 사람이라면 도움이 될 것이다.

어떤 개념에 대해 이해하기 위해서는 그 개념이 왜 등장했는지, 이것이 해결하고자하는 문제점은 무엇인지에 대해 생각해보는게 좋은 접근이라는 것을 꽤 오래 잊고있었다. 그냥 "코루틴"이라고 구글에 검색해서 아티클을 몇가지 보면서 이해했다고 넘겼다. 하지만 정작 "코루틴"이 무엇인가? 그리고 비동기 프로그래밍과는 어떤 관계가 있는가에대해서 명확하게 설명할 수 없었다.
언어마다 차이는 있겠지만 일반적으로는 async
await
키워드로 사용하는 (Kotlin에서는 suspend
) 코루틴을 이용한 비동기 프로그래밍을 위한 문법이 등장하기 전에는 어떻게 비동기 프로그래밍을 구현했을까?
비동기 프로그래밍
비동기 프로그래밍은 특정 작업의 완료를 기다리지 않고, 다른 작업을 동시에 처리할 수 있게 하는 프로그래밍 방식이다. 비동기 프로그래밍은 현대 프로그램에서 거의 필수적으로 요구된다. 내가 친숙한 안드로이드를 예로 들면, 앱이 버벅이지 않게 하기 위해서 I/O 작업은 메인 스레드가 아닌 다른 스레드에서 처리하지 않으면 오류를 던질만큼 강제되고 있다. 이러한 비동기 프로그래밍을 구현하는 방법으로는 여러가지가 있지만 콜백(Callback)함수를 사용하는 방식이 아주 오래되었다.
콜백함수를 사용한 대표적인 비동기 프로그래밍 코드를 AI에게 요청했더니 이런 예시를 줬다.
function foo() { setTimeout(() => console.log("World!"), 1000); console.log("Hello!") }
setTimeout
안에 들어간 콜백함수는 즉시 실행되지않고, 1초 뒤에 태스크 큐에 넘어가게 된다. Javascript의 이벤트 루프는 계속해서 태스크 큐를 모니터링하면서, 호출 스택이 비어있을때(- 당장 실행할 함수같은것이 없는 여유로운 상황) 큐에서 태스크를 뽑아 처리하게 된다.
따라서 코드를 실행하면 "Hello!", "World!"가 콘솔에 차례로 찍힐 것이다.
실행 흐름
위 코드에서 2개의 실행 흐름을 볼 수 있다. foo
라는 함수와, setTimeout
안에있는 콜백함수가 바로 그것이다. 잠깐만, "실행 흐름"이라! 콜백함수도 분명히 (비동기 프로그래밍과 관계없이) 실행 흐름을 분리하는 방법중 하나다.
그리고 이 "실행 흐름"이라는 말은 코루틴을 공부하면서도 얼핏 마주쳤을 것이다.
코루틴은 협력적인 루틴(Cooperative Routine)의 약자로, 실행을 일시 중지하고 재개할 수 있는 함수로 알려져있다. 코루틴은 일반 함수들처럼 평범하게 작업을 진행하다가도 yield
같은 양보 키워드를 만나게 되면 일시중단하고, 함수의 현재 실행 상태(변수, 프로그램 카운터, 스택 프레임 등)을 힙(Heap)에 저장해둔다. 그리고 다음번에 코루틴이 호출되었을때 지난번에 일시중단했던 지점에서 이어서 작업을 진행하게 된다.
이러한 코루틴을 사용하면 콜백함수를 사용할때와 비슷하게, 단일 스레드에서 여러 실행흐름이 진행되도록 구현할 수 있다. 그러면 비동기 프로그래밍을 코루틴으로도 구현할 수 있지않을까? 그렇다. 코루틴을 이용해서도 비동기 프로그래밍을 구현할 수 있다. 그것이 바로 async/await
키워드로 비동기 프로그래밍을 할 수 있게된 배경이다.
그렇다면 현대 비동기 프로그래밍 문법에서 콜백함수를 제치고 코루틴이 사랑받는 이유가 무엇일까?
"콜백지옥(Callback Hell)"이라는 말을 다들 한번쯤은 들어보았을 것이다.
getData(function(a) { getMoreData(a, function(b) { getMoreData(b, function(c) { ...
콜백함수 딱 3개정도만 중첩해서 사용해봐도 사람이 정말 사용하기 어려운 스타일이라는걸 알 수 있다.
a = await getData() b = await getMoreData(a) c = await getMoreData(b)
이건 똑같은 상황을 코루틴으로 적어본 코드다. 비동기 프로그래밍을 동기적으로 표현할 수 있어 읽기 쉬우며, 오류 처리도 간단해진다는 점때문에 이렇게 널리쓰이게 된 것이 아닐까 싶다.
스레드랑은 무슨 관계죠?
그러면 비동기 프로그래밍에서 스레드와 코루틴의 관계는 어떻게 될까? 스레드는 프로세스 내에서 운영체제에 의해 독립적인 스택공간을 할당받아 만들어진다. 전용 스택공간이 따로 있기 때문에 각 스레드는 자신의 실행흐름을 가질 수 있다. 많이 들었고, 보았던 그리고..외웠던 개념일 것이다.
코루틴은 프로그래밍의 방법 중 하나, 스레드는 실행단위나 운영체제 자원 종류 중의 하나로 동등한 조건에서 비교할 수 있는 개념은 확실히 아니지만, 비동기 프로그래밍을 구현할때 둘 중에 선택하거나 둘을 섞어 사용하거나 할 수 있다. 그리고 어떤 것을 사용하느냐를 알기 위해서는 동시성, 병렬성에 대해서 먼저 짚고 넘어가야한다.
병렬성은 정말로-실제로! 여러개의 실행흐름이 병렬적으로 동작하는 것이고, 동시성은 여러 작업이 동시에 진행되는 것"처럼" 처리하는 방식이다. 우리가 앞에서 얘기했던 것을 토대로 본다면 코루틴을 이용하면 동시적으로, 스레드를 이용하면 병렬적으로 함수를 실행 할 수 있다고 할 수 있다.
이것만 놓고보면 "스레드가 최고네!"라는 생각이 들 수 있지만, 스레드는 코루틴에 비해 생성비용이 많이 든다. 스레드의 관리 주체는 운영체제이며, 병렬적으로 실행되는데 데이터 공간을 같이 사용함으로서 발생하는 동시성 이슈들도 스레드를 사용하기 까다롭게 만든다. 반면에 코루틴의 관리 주체는 사용자 레벨에 있고, 별도 설정을 하지않는다면야 단일 스레드에서 실행되기 때문에 동시성 이슈가 터지지 않는다. 별도의 스택 공간을 할당받을 필요도 없기때문에 컨텍스트 스위칭 비용도 적다.
이러한 이유로 컴퓨팅 자원이 문제가아니라 걸리는 시간이 문제일 경우에 (e.g. 네트워크 요청, 파일 읽기, 쓰기) 즉, I/O 작업은 코루틴으로 처리하는것이 훨씬 낫다. 반면에 정말 CPU 집약적 계산이 필요한 경우 별도의 스레드를 만들어 각 CPU 코어에서 동시에 처리하게 한다면 전체 실행시간에서 이점을 얻을 수 있을 것이다.
이렇게 "코루틴"에 대해서 검색하면 나오는 모든 용어들의 관계에대해서 줄글로 쭉-풀어보았는데, 안드로이드 개발자라면 마지막 한가지 의문이 또 하나 들 것이다. 바로, Kotlin에서 사용하는 코루틴은 단일 스레드에서 동작한다고 할 수 있을까?에 대한 의문이다. Dispatchers.어쩌고
시리즈는 ThreadPool에서 스레드를 할당받아 그 안에서 코루틴을 실행시키는 것이므로 단일 스레드에서 동작한다고는 확신할 수 없다.
CoroutineScope(Dispatchers.IO) { ... }
'프로그래밍 > General' 카테고리의 다른 글
[AWS] S3 버전 관리와 삭제마커 (0) | 2024.12.01 |
---|---|
Bruno > Postman Collection Import시 한글 깨짐 해결방법 (3) | 2024.10.12 |
유한상태머신(FSM)으로 텍스트 젤다의 전설 만들기 (1) | 2024.02.04 |
Android Studio Custom Shortcuts (0) | 2024.01.07 |
[VideoJS] 영상 타임라인에 프리뷰를 표시하는 방법 (1) | 2023.12.03 |