코루틴의 개념을 살펴보기 전에 우선, 상반되는(반드시 상반된다고는 할 수 없지만..) 서브루틴에 대해서 한번 짚고 넘어가 보겠습니다. 참고로 서브루틴의 상반되는 개념은 코루틴이 아닌 메인루틴(main-routine, 그냥 루틴이라고도 함)이라 할 수 있습니다.
서브루틴
서브루틴은 반복되는 특정 기능을 모아 별도로 묶어 놓아 이름을 붙여 놓은 것으로 메인루틴을 보조하는 역할을 합니다. 보통 언어에서는 함수나 메소드 등으로 불리며 사용됩니다. 어떤 특정 기능을 모아놓고 이름을 붙였다는 것으로 매크로와 비슷하지만 매크로의 경우 컴파일시에(C 언어에서와 같이) 매크로를 호출하는 부분을 모두 매크로 본문으로 대체해 버리므로 메모리 사용이 비효율적입니다. 반면에 서브루틴은 별도의 메모리에 해당 기능을 모아 놓고 있어, 서브루틴이 호출될 때마다 저장된 메모리로 이동했다가 return 을 통해 원래 호출자의 위치로 돌아오게 됩니다. 호출할 때마다 매번 같은 위치로 이동하기 때문에 여러번 사용될 수 있으므로 매크로에 비해서 훨씬 효율적이라 할 수 있겠지요.
코루틴
코루틴도 서브루틴처럼 기능들을 별도의 공간에 모아 놓고 있다는 점에서는 동일합니다. 차이점이라 할 수 있는 것은, 서브루틴의 경우에는 메인루틴에서 특정 서브루틴의 공간으로 이동한 후에 리턴에 의해 호출자로 돌아와 다시 프로세스를 진행하는데 반해 코루틴의 경우에는 루틴을 진행하는 중간에 멈추어서 특정 위치로 돌아갔다가 다시 원래 위치로 돌아와 나머지 루틴을 수행할 수 있습니다. 또 한가지 차이점은 서브루틴은 진입점과 반환점이 단 하나밖에 없어 메인루틴에 종속적이지만, 코루틴은 진입지점이 여러개이기 때문에 메인루틴에 종속적이지 않아 대등하게 데이터를 주고 받을 수 있다는 특징이 있습니다. 코루틴은 주로 동시성을 필요로 하는 UNITY 등의 게임프로그래밍에서 많이 사용하는 개념이라고 합니다.
파이썬에도 코루틴이 있습니다. 코루틴의 특징과 흐름을 살펴보면 다음과 같습니다.
- 파이썬에는 yield 문이라는 특수한 구문이 있습니다. return 처럼 동작하지만, 사실은 입력으로 동작합니다.(메인루틴에 종속적이 아니라 대등한 상태이기 때문에)
- next(coroutine)은 coroutine 함수의 첫번째 yield 까지 호출한다음 대기합니다. 두번째 next(coroutine)을 호출하면, 첫번째 yield 다음의 나머지 부분을 수행하고 다시 돌아와 그 다음 yield 까지 호출합니다. iteration 이 가능한곳까지 next 함수가 수행된 뒤에는 StopIteration 에러가 발생하게 됩니다.
- 만약 yield 문이 특정 변수에 할당된다면, 만들어진 코루틴 객체에서 coroutine.send(value)를 호출해 주어야 합니다. 첫번째 coroutine 지점(yield)에 멈춰있는 상태에서 변수에 할당 되어야 하는데 아무런 값도 들어오지 않는다면 에러가 발생하게 됩니다. 즉, yield 를 통해서 메인루틴과 서브루틴간에 서로 값이 이동하면서 특정 로직을 수행하게 되는 것입니다.
그럼 예를 살펴보도록 하겠습니다.
첫번째 예제는 yield 값만 리턴하는 경우입니다. 코루틴 객체를 생성한 후 next() 함수를 통해서 첫번째 yield 문에 도달합니다. yield 문을 통해 메인루틴에 값을 전달한 후 코루틴 함수는 대기합니다. 다음 next()함수가 호출되면, 멈춰져 있는 yield 부분 다음의 로직을 수행한 다음 한바퀴 돌아 yield 부분에서 또다시 멈추게 됩니다. 아래의 예제에서는 next()를 무한정 호출할 수 있지만, 특정한 조건을 지정해주면 정해진 만큼만 next()를 호출할 수 있습니다. 더이상 호출할 수 없는 코루틴 함수를 next()로 호출하면 예외가 발생하게 됩니다.
def test1(i):
print('start test1 coroutine')
while True:
yield i
i += 1
a = test1(5)
next(a) # start test1 coroutine 출력 후 5출력, yield i 부분에서 멈춰있다.
next(a) # 멈춰진 yield i 부분 다음줄의 5 += 1(i=6)을 수행한후 다음 6을 출력하고 yield i에서 멈춘다.
next(a) # 7을 출력하고 yield i에서 멈춘다.
next(a)
두번째 예제는 코루틴과 메인루틴이 서로 통신하는 형태입니다. 위의 test1()과 다른점은 yield 구문을 특정 변수에 할당합니다. 이렇게 되면 처음 next()를 호출한 뒤 send(value) 함수를 호출하면 전달된 파라미터가 value 에 할당됩니다. 즉, b = test2(5), next(b)를 수행한 뒤 b.send(3)을 호출하면 8 이 출력된다는 말이죠. 그 다음은 next()함수와 마찬가지로 그 다음 yield 에 가서 멈춰 있게 됩니다. 위의 예제와 다른 점은 위 같은 경우에는 코루틴에서 메인루틴으로 일방적으로 값을 전달해줬다면, 아래 예제에서는 메인루틴과 서브루틴이 서로 값을 주고 받는 형태를 이루고 있습니다. 양방향 통신이 가능해진 것이죠. 하지만 실시간으로 서로 주고 받는 형태는 아니므로 반이중 통신이라 할 수 있겠습니다.
def test2(i):
print('start test2 coroutine')
while True:
value = yield i
i += value
b = test2(5)
next(b) # start test1 coroutine 출력 후 5출력, yield i 부분에서 멈춰있다.
b.send(3) # yield를 통해 3을 전달하여 value가 3이 된다. 이후 i += value 줄을 거쳐 i=8이되고 한바퀴 돌아 8을출력, yield에서 멈춘다.
b.send(5) # 5를 더해 i는 8이되고 8을 출력한다.
아직 저도 코루틴을 실제 프로젝트에서 많이 다뤄보지는 못했기에 활용법에 대해서 잘은 모릅니다. 검색을 통해 찾아 보았던 것은 수많은 파이썬 비동기 모듈에서 대부분 코루틴을 다루고 있다는 것입니다. 양 방향으로 통신이 가능하다는 것은 여러 함수를 동시에 호출하면서 서로간에 통신도 가능하다는 말이니 적절한 사용처가 아닐까 생각합니다. 또 생각해 볼 수 있는 것은 정해진 순서가 아닌 사용자의 입력에 따라서 동작이 바뀌는 기계학습 분야에도 많이 활용될 것 같다는 느낌이 드는군요.