JAVA/비동기 처리

동시성 프로그래밍과 코루틴 (Coroutine)

수달하나 2025. 1. 8. 23:31

코루틴이 대체 뭔데

최근 컴퓨터 밑바닥의 비밀 에 대한 책을 읽으면서 고급 프로그래밍을 위한 공부를 하던 도중 동시성 프로그래밍에 대한 내용과 코루틴에 대한 부분이 등장했고 해당 챕터를 읽으면서 이해를 하는데 있어서 꽤나 애를 먹었다.

 

동시성 프로그래밍을 달성하기 위한 개념적인 것들과 코루틴에 대한 기본적인 이해는 어렵지 않다고 하지만 실질적인 동작 흐름을 시간단위로 쪼개서 생각을 했을 때 과연 어떠한 방식으로 동작하는 것인가 에 대한 부분을 정확하게 정의하고 넘어가야 겠다는 생각을 하게 되었고 시간 단위로 쪼개서 프로세스의 실행흐름을 살펴보는 과정을 가지기로 했다.

 

우선 코루틴을 이해하기 위해서 가장 중요한 부분은 동시성 프로그래밍과 병렬성 프로그래밍에 대한 이해에서 부터 시작된다고 생각한다.

 

동시성 프로그래밍 VS 병렬성 프로그래밍

동시성 프로그래밍과 병렬성 프로그래밍은 애초에 개념적으로 다른 이야기이다.

 

동시성 프로그래밍

일반적으로 메인 실행흐름에서 함수를 호출하면 아래와 같은 실행 흐름의 구조를 갖게 된다.

메인 실행흐름에서 함수 A 를 호출하면 메인실행 흐름은 함수 A 가 완료되기 전 까지 블로킹 상태에 진입하게 된다. 마찬가지로 함수 A 에서 함수 B를 호출했다면 함수 B 가 수행되는 시간 동안에는 함수 A는 어쩔 수 없이 블로킹 상태를 갖게 된다.

이러한 과정은 하나의 스레드가 여러개의 실행 흐름을 갖는다면 어쩔 수 없이 발생하는 필수불가결한 상황이다.

 

그럼 우리는 여러가지의 실행 흐름을 블로킹 상태 없이 동시적으로발생시키기 위해서 다른 방법을 사용했고 그 방법이 멀티 쓰레드를 이용하는 방법이다.

 

함수를 호출하는 것이 새로운 실행 흐름을 갖는 것이라고 생각하면 새로운 스레드를 발생시킴으로써 새로운 작업 단위를 만드는 것이다.

메인 스레드의 실행 흐름과 서브 스레드의 실행 흐름이 동시에 진행되고 있으니 위 작업은 동시성 프로그래밍이다.

물론 각 스레드에 새로운 실행흐름이 발생한다면 블로킹 상태를 유지해야 하지만 그 때 마다 또 다른 스레드의 호출을 통해 새로운 작업 단위를 만들면 하나의 실행 흐름은 새로운 스레드를 통해서 동시에 처리되니 문제는 없다.

 

* 수 많은 스레드를 생성하는 것에는 매우 큰 위험요소를 동반하지만 동시성 프로그래밍의 이해를 위해 그 부분은 무시한다.

 

병렬성 프로그래밍

각각의 스레드에서 병렬적으로 처리 되었으니 병렬성 프로그램이라고 할 수 있지 않나? 라는 생각을 할 수도 있지만 실제로 병렬 프로그램은 물리적인 개념이 포함되어 있어야 한다.

 

컴퓨팅 파워의 발전을 통해서 엄청난 연산을 매우 빠르게 할 수 있게 된 요즘 멀티스레드를 통해서 작성된 프로그램은 병렬 처리되는 것 처럼 느껴진다. 함정은 사실 매우 빠른 연산이 매 순차적으로 * 컨텍스트 스위칭 을 통해 처리되고 있다는 것이다. 따라서 매우 빠른 연산이 두 스레드를 번갈아 가면서 실행했고 논리적으로 병렬 적으로 느껴졌던 처리는 사실 매우빠르게 동시적으로 처리되고 있었던 것이다.

 

* 컨텍스트 스위칭 (Context Switching) : CPU/코어에서 실행 중이던 프로세스/스레드가 다른 프로세스/스레드로 교체됨.

따라서 오른쪽 그림처럼 물리적 연산이 동시에 진행 되야지 병렬성 프로그래밍이라고 할 수 있다.

 

코루틴의 실행 흐름 

그렇다면 코루틴은 어떤 개념에 속한 것 일까?

코루틴은 동시성 프로그래밍의 개념에 속하고 그 동작 과정을  살펴 본다면 아래와 같다.

메인 실행흐름에서 코루틴을 호출하게 되면 코루틴을 호출함과 동시에 메인 실행 흐름으로 반환되며 메인 실행 흐름과 코루틴을 동시에 처리하게 된다. 해당 처리는 하나의 스레드에서 발생하는 동시성 프로그래밍 기법이다.

 

말이 안된다. 하나의 스레드는 하나의 작업을 실행하는 단위 이므로 새로운 실행 흐름이 발생한다면 당연히 기존 흐름은 블로킹 상태를 유지해야 한다. 해답은 스레드가 코루틴을 어떻게 처리하는 가에 있다.

 

쓰레드는 한 번에 하나의 처리만 할 수 있다. 

하지만 매우 빠르게 여러가지 실행 흐름을 번갈아 가면서 한다면 어떨까?

 

동시성 프로그래밍이 여러 스레드의 컨텍스트 스위칭을 통해 번갈아가면서 스레드 연산을 진행했다면 하나의 스레드에서도 여러개의 실행흐름을 번갈아가면서 수행할 수 있지 않을까에 대한 생각을 통해 코루틴을 이해해 볼 수 있다.

 

간단한 예시를 통해서 코루틴의 실행 흐름을 직접 확인 해 볼 수 있다.

 

fun main() {
    val person = Person("용준")
    goCompany(person)
    * 실행1
    * 실행2
    * 실행3
}

fun goCompany(person: Person) {
    var 잠든용준 = person
    try {
        val 비몽사몽한용준 = wakeUp(잠든용준)
        val 깨끗한용준 = takeShower(비몽사몽한용준)
        val 옷입은용준 = putOnShirt(깨끗한용준)
        val 버스를탄용준 = getOnBus(옷입은용준)
        val 출근한용준 = finish(버스를탄용준)

        출근한용준.doWork()
    } catch (e: Exception) {
        fail()
    }
}

 

위와 같이 main 함수에서 일반 함수 goCompany 를 호출했다는 가정을 하면 실행 흐름은 아래와 같은 순서로 진행된다.

시간 흐름      
1 main 시작    
2 goCompany 호출 / block goCompany 시작  
3 block wakeUp 호출  / block wakeUp 시작
4 block block 해제 wakeUp 종료
5 block takeShower 호출  / block takeShower 시작
6 block block 해제 takeShower 종료
7 block putOnShirt 호출  / block putOnShirt 시작
8 block block 해제 putOnShirt 종료
9 block getOnBus 호출  / block getOnBus 시작
10 block block 해제 getOnBus 종료
11 block finish 호출  / block finish 시작
12 block block 해제 finish 종료
13 block doWork 호출 / block doWork 시작
14 block block 해제 doWork 종료
15 block 해제 goCompany 종료  
16 * 실행 1    
17 * 실행 2    
18 * 실행 3    
19 main 종료    

 

따라서 반드시 goCompany 가 반환 된 이후에 main 함수 block 이 해제 되고 이후 처리를 할 수 있다. 


fun main() = runBlocking {
    val person = Person("용준")
    goCompany(person)
    * 실행 1
    * 실행 2
    * 실행 3
}

suspend goCompany(person: Person) {
    var 잠든용준 = person
    try {
        val 비몽사몽한용준 = wakeUp(잠든용준)
        val 깨끗한용준 = takeShower(비몽사몽한용준)
        val 옷입은용준 = putOnShirt(깨끗한용준)
        val 버스를탄용준 = getOnBus(옷입은용준)
        val 출근한용준 = finish(버스를탄용준)

        출근한용준.doWork()
    } catch (e: Exception) {
        fail()
    }
}

 

하지만 위와 같이 코루틴을 사용하는 방식으로 변경 한다면 아래와 같은 실행 흐름을 갖게 된다.

시간 흐름      
1 main 시작    
2 goCompany 호출 goCompany 시작  
3 * 실행 1 wakeUp 호출 / block wakeUp 시작
4 * 실행 2 block 해제 wakeUp 종료
5 * 실행 3 takeShower 호출 / block takeShower 시작
6 main 종료 block 해제 takeShower 종료
7   putOnShirt 호출 / block putOnShirt 시작
8   block 해제 putOnShirt 종료
9   getOnBus 호출 / block getOnBus 시작
10   block 해제 getOnBus 종료
11   finish 호출 / block finish 시작
12   block 해제 finish 종료
13   doWork 호출 / block doWork 시작
14   block 해제 doWork 종료
15   goCompany 종료  

 

일반 함수와의 차이는 main 이 block 되지 않고 동시에 실행되는 것 처럼 보인다는 것이며 실제로 main 함수의 반환이 코루틴의 반환 시점과 무관하다는 차이점이 발생한다. 

 

위와 같은 상태로 하나의 스레드에서 여러개의 실행흐름이 동시에 처리되는 것 처럼 보여질 수 있으며 실제로 무거운 스레드를 사용 하는 것 보다 좀 더 경량화된 코루틴을 통하여 동시성 프로그래밍을 구현 할 수 있다는 것에 큰 의미를 가질 수 있다.